Skip to content

Commit 9418a83

Browse files
authored
Merge pull request #23 from wataruoguchi/support_requiring_policy
test(emmett-crypto-shredding-kysely): make policies required
2 parents 9d47515 + c28b939 commit 9418a83

7 files changed

Lines changed: 1044 additions & 491 deletions

File tree

docs/emmett-crypto-shredding-kysely.md

Lines changed: 193 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,110 +16,249 @@ npm install @wataruoguchi/emmett-crypto-shredding-kysely @wataruoguchi/emmett-cr
1616

1717
### 1. Run Database Migration
1818

19-
Copy the migration file from [`database/migrations/`](https://github.com/wataruoguchi/emmett-libs/blob/main/packages/emmett-crypto-shredding-kysely/database/migrations/1761627233034_crypto_shredding.ts) to your migrations directory.
19+
Copy the migration file from [`database/migrations/`](https://github.com/wataruoguchi/emmett-libs/blob/main/packages/emmett-crypto-shredding-kysely/database/migrations/1761627233034_crypto_shredding.ts) to your migrations directory and run it.
2020

21-
### 2. Create Key Management and Policy Resolver
21+
### 2. Create Encryption Policies
22+
23+
**⚠️ Important:** Policies must be created **before** events can be encrypted. Create them during:
24+
25+
- **Tenant onboarding** - When a new tenant/partition is created
26+
- **Application setup** - As a one-time bootstrap step
27+
- **Feature enablement** - When enabling crypto shredding for existing streams
2228

2329
```typescript
24-
import {
25-
createKeyManagement,
26-
createPolicyResolver
27-
} from '@wataruoguchi/emmett-crypto-shredding-kysely';
30+
import { createPolicies } from '@wataruoguchi/emmett-crypto-shredding-kysely';
2831

29-
const keyManagement = createKeyManagement(db);
30-
const policyResolver = createPolicyResolver(db, logger);
32+
// Example: During tenant onboarding
33+
async function onboardTenant(tenantId: string) {
34+
// Create tenant...
35+
36+
// Set up encryption policies for sensitive streams
37+
await createPolicies(db, [
38+
{
39+
policyId: `${tenantId}-generator`,
40+
partition: tenantId,
41+
streamTypeClass: 'generator', // Stream type to encrypt
42+
encryptionAlgorithm: 'AES-GCM',
43+
keyRotationIntervalDays: 180,
44+
keyScope: 'stream', // 'stream' or 'type'
45+
},
46+
]);
47+
}
3148
```
3249

33-
### 3. Set Up Encryption Policies
50+
**Alternative:** Use default policies for common use cases:
3451

3552
```typescript
36-
import { createDefaultPolicies, createPolicies } from '@wataruoguchi/emmett-crypto-shredding-kysely';
53+
import { createDefaultPolicies } from '@wataruoguchi/emmett-crypto-shredding-kysely';
3754

38-
// Option 1: Use default policies
39-
// Creates policies for common stream types:
40-
// - user-data: AES-GCM, 180 day rotation, stream scope
41-
// - audit-log: AES-GCM, 365 day rotation, stream scope
55+
// Creates policies for 'user-data' and 'audit-log' stream types
4256
await createDefaultPolicies(db, 'tenant-123');
57+
```
58+
59+
### 3. Create Encrypted Event Store in Your Module
60+
61+
Wire up the crypto event store in your module factory:
62+
63+
```typescript
64+
import {
65+
createCryptoEventStore,
66+
createWebCryptoProvider,
67+
type CryptoContext,
68+
} from '@wataruoguchi/emmett-crypto-shredding';
69+
import {
70+
createKeyManagement,
71+
createPolicyResolver,
72+
} from '@wataruoguchi/emmett-crypto-shredding-kysely';
73+
import { getKyselyEventStore } from '@wataruoguchi/emmett-event-store-kysely';
74+
75+
export function createGeneratorModule({ db, logger }) {
76+
const eventStore = createCryptoEventStore(
77+
getKyselyEventStore({ db, logger }),
78+
{
79+
policy: createPolicyResolver(db, logger),
80+
keys: createKeyManagement(db),
81+
crypto: createWebCryptoProvider(),
82+
buildAAD: ({ partition, streamId }: CryptoContext) =>
83+
new TextEncoder().encode(`${partition}:${streamId}`),
84+
logger,
85+
},
86+
);
87+
88+
// Use eventStore in your event handler...
89+
return createService({ eventStore });
90+
}
91+
```
92+
93+
**How it works:**
94+
95+
1. When events are appended, the policy resolver checks if the stream type has a policy
96+
2. If a policy exists, events are encrypted using the specified algorithm
97+
3. Keys are automatically generated and managed per the policy's `keyScope`
98+
4. If no policy exists for a stream type, events are stored unencrypted
99+
100+
## API Reference
101+
102+
### Policy Management
43103

44-
// Option 2: Create custom policies
104+
#### Creating Policies
105+
106+
```typescript
107+
import {
108+
createPolicies,
109+
createDefaultPolicies,
110+
type DatabasePolicyConfig
111+
} from '@wataruoguchi/emmett-crypto-shredding-kysely';
112+
113+
// Create custom policies
45114
await createPolicies(db, [
46115
{
47-
policyId: 'tenant-123-user-data',
116+
policyId: 'tenant-123-generator',
48117
partition: 'tenant-123',
49-
streamTypeClass: 'user-data',
118+
streamTypeClass: 'generator',
50119
encryptionAlgorithm: 'AES-GCM',
51120
keyRotationIntervalDays: 180,
52-
keyScope: 'stream',
121+
keyScope: 'stream', // 'stream' = one key per stream, 'type' = one key per stream type
53122
},
54123
]);
124+
125+
// Or use defaults (creates 'user-data' and 'audit-log' policies)
126+
await createDefaultPolicies(db, 'tenant-123');
55127
```
56128

57-
### 4. Create Encrypted Event Store
129+
#### Managing Policies
58130

59131
```typescript
60-
import { createCryptoEventStore } from '@wataruoguchi/emmett-crypto-shredding';
61-
import { getKyselyEventStore } from '@wataruoguchi/emmett-event-store-kysely';
132+
import {
133+
updatePolicy,
134+
listPolicies,
135+
deletePolicy
136+
} from '@wataruoguchi/emmett-crypto-shredding-kysely';
62137

63-
const baseEventStore = getKyselyEventStore({ db, logger });
64-
65-
const cryptoEventStore = createCryptoEventStore({
66-
baseEventStore,
67-
keyManagement,
68-
policyResolver,
69-
buildAAD: (ctx) => JSON.stringify({
70-
partition: ctx.partition,
71-
streamId: ctx.streamId,
72-
streamType: ctx.streamType,
73-
eventType: ctx.eventType,
74-
}),
75-
logger,
138+
// Update policy
139+
await updatePolicy(db, 'tenant-123-generator', 'tenant-123', {
140+
encryptionAlgorithm: 'AES-GCM',
141+
keyRotationIntervalDays: 365,
76142
});
77-
```
78143

79-
## API Reference
144+
// List all policies for a partition
145+
const policies = await listPolicies(db, 'tenant-123');
146+
147+
// Delete policy (stops encrypting new events for this stream type)
148+
await deletePolicy(db, 'tenant-123-generator', 'tenant-123');
149+
```
80150

81151
### Key Management
82152

153+
The `createKeyManagement` function returns a service that automatically manages encryption keys based on your policies:
154+
83155
```typescript
84-
// Get or create active key
156+
import { createKeyManagement } from '@wataruoguchi/emmett-crypto-shredding-kysely';
157+
158+
const keyManagement = createKeyManagement(db);
159+
160+
// Get or create active key (usually done automatically by crypto event store)
85161
const key = await keyManagement.getActiveKey({
86162
partition: 'tenant-123',
87-
keyRef: 'user-data',
163+
keyRef: 'generator-stream-123', // Varies based on keyScope
88164
});
89165

90-
// Rotate key
166+
// Manually rotate key for a specific reference
91167
await keyManagement.rotateKey({
92168
partition: 'tenant-123',
93-
keyRef: 'user-data',
169+
keyRef: 'generator-stream-123',
94170
});
95171

96-
// Destroy all keys for a partition (crypto shredding)
172+
// Crypto shredding: Destroy all keys for a partition
173+
// This makes all encrypted events for this partition unrecoverable
97174
await keyManagement.destroyPartitionKeys({
98175
partition: 'tenant-123',
99176
});
100177
```
101178

102-
### Policy Management
179+
## Common Patterns
180+
181+
### Tenant Onboarding with Encryption
103182

104183
```typescript
105-
// Create policies
106-
await createPolicies(db, policies);
184+
async function createTenantWithEncryption(tenantData: { name: string }) {
185+
// 1. Create tenant
186+
const tenant = await tenantRepository.create(tenantData);
187+
188+
// 2. Set up encryption policies for sensitive streams
189+
await createPolicies(db, [
190+
{
191+
policyId: `${tenant.id}-user-data`,
192+
partition: tenant.id,
193+
streamTypeClass: 'user-data',
194+
encryptionAlgorithm: 'AES-GCM',
195+
keyRotationIntervalDays: 180,
196+
keyScope: 'stream',
197+
},
198+
]);
199+
200+
return tenant;
201+
}
202+
```
107203

108-
// Update policy
109-
await updatePolicy(db, policyId, partition, {
110-
encryptionAlgorithm: 'AES-GCM',
111-
keyRotationIntervalDays: 365,
112-
});
204+
### Selective Encryption by Stream Type
205+
206+
Only encrypt sensitive stream types. Other streams remain unencrypted for performance:
207+
208+
```typescript
209+
// Encrypt sensitive streams
210+
await createPolicies(db, [
211+
{
212+
policyId: `${tenantId}-user-data`,
213+
partition: tenantId,
214+
streamTypeClass: 'user-data', // ✅ Encrypted
215+
encryptionAlgorithm: 'AES-GCM',
216+
keyRotationIntervalDays: 180,
217+
keyScope: 'stream',
218+
},
219+
]);
220+
221+
// No policy for 'cart' stream type → stored unencrypted ✓
222+
```
113223

114-
// List policies
115-
const policies = await listPolicies(db, partition);
224+
### Crypto Shredding (Right to be Forgotten)
116225

117-
// Delete policy
118-
await deletePolicy(db, policyId, partition);
226+
```typescript
227+
async function forgetTenant(tenantId: string) {
228+
// Destroy all encryption keys for the tenant
229+
// This makes all encrypted events permanently unrecoverable
230+
await keyManagement.destroyPartitionKeys({ partition: tenantId });
231+
232+
// Optionally: Delete unencrypted data or anonymize tenant records
233+
// ...
234+
}
119235
```
120236

237+
## Troubleshooting
238+
239+
### Events Not Being Encrypted
240+
241+
**Problem:** Events are stored in plaintext even though policies exist.
242+
243+
**Checklist:**
244+
245+
1. ✅ Verify policy exists: `await listPolicies(db, tenantId)`
246+
2. ✅ Check `streamTypeClass` matches your stream type exactly
247+
3. ✅ Ensure policy was created before appending events
248+
4. ✅ Verify `createCryptoEventStore` is being used (not just base event store)
249+
250+
### Cannot Read Encrypted Events
251+
252+
**Problem:** Events cannot be decrypted or appear as `[encrypted]`.
253+
254+
**Possible causes:**
255+
256+
- Keys have been destroyed (crypto shredding)
257+
- Wrong partition/tenant context
258+
- Database connectivity issues with `encryption_keys` table
259+
121260
## See Also
122261

123262
- [Crypto Shredding Core](./emmett-crypto-shredding) - Core encryption functionality
124263
- [Event Store Kysely](./emmett-event-store-kysely) - Base event store implementation
125-
- [Example Application](https://github.com/wataruoguchi/emmett-libs/tree/main/example) - Complete working example
264+
- [Example Application](https://github.com/wataruoguchi/emmett-libs/tree/main/example) - Complete working example with crypto shredding

0 commit comments

Comments
 (0)