@tuturuuu/utils 0.0.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +122 -3
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { decryptField, encryptField } from '../encryption-service';
|
|
3
|
+
import { generateWorkspaceKey } from './test-helpers';
|
|
4
|
+
|
|
5
|
+
describe('encryption-service', () => {
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Field Encryption/Decryption Tests
|
|
8
|
+
// ============================================================================
|
|
9
|
+
describe('field encryption/decryption', () => {
|
|
10
|
+
let workspaceKey: Buffer;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
workspaceKey = generateWorkspaceKey();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should encrypt and decrypt string field correctly', () => {
|
|
17
|
+
const plaintext = 'Team Meeting';
|
|
18
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
19
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
20
|
+
|
|
21
|
+
expect(decrypted).toBe(plaintext);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should handle empty strings', () => {
|
|
25
|
+
const ciphertext = encryptField('', workspaceKey);
|
|
26
|
+
expect(ciphertext).toBe('');
|
|
27
|
+
|
|
28
|
+
const decrypted = decryptField('', workspaceKey);
|
|
29
|
+
expect(decrypted).toBe('');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should handle Unicode characters (Vietnamese)', () => {
|
|
33
|
+
const plaintext = 'Cuộc họp nhóm buổi sáng';
|
|
34
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
35
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
36
|
+
|
|
37
|
+
expect(decrypted).toBe(plaintext);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should handle emoji', () => {
|
|
41
|
+
const plaintext = '🎉 Birthday Party 🎂';
|
|
42
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
43
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
44
|
+
|
|
45
|
+
expect(decrypted).toBe(plaintext);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle long text', () => {
|
|
49
|
+
const plaintext =
|
|
50
|
+
'This is a very long description that contains multiple sentences. '.repeat(
|
|
51
|
+
100
|
|
52
|
+
);
|
|
53
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
54
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
55
|
+
|
|
56
|
+
expect(decrypted).toBe(plaintext);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should produce different ciphertext for same plaintext (random IV)', () => {
|
|
60
|
+
const plaintext = 'Same text';
|
|
61
|
+
const ciphertext1 = encryptField(plaintext, workspaceKey);
|
|
62
|
+
const ciphertext2 = encryptField(plaintext, workspaceKey);
|
|
63
|
+
|
|
64
|
+
expect(ciphertext1).not.toBe(ciphertext2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should fail decryption with wrong key and return original (backward compat)', () => {
|
|
68
|
+
const wrongKey = generateWorkspaceKey();
|
|
69
|
+
const ciphertext = encryptField('Secret', workspaceKey);
|
|
70
|
+
|
|
71
|
+
// With the new try-catch, it returns original ciphertext for backward compatibility
|
|
72
|
+
const result = decryptField(ciphertext, wrongKey);
|
|
73
|
+
expect(result).toBe(ciphertext);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should fail decryption when ciphertext is tampered and return original', () => {
|
|
77
|
+
const plaintext = 'Sensitive Meeting Notes';
|
|
78
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
79
|
+
|
|
80
|
+
// Tamper with a byte in the middle of the ciphertext
|
|
81
|
+
const midIndex = Math.floor(ciphertext.length / 2);
|
|
82
|
+
const originalChar = ciphertext[midIndex];
|
|
83
|
+
// Guarantee the new char is different from the original
|
|
84
|
+
const tamperedChar = originalChar === 'A' ? 'B' : 'A';
|
|
85
|
+
const tamperedCiphertext =
|
|
86
|
+
ciphertext.slice(0, midIndex) +
|
|
87
|
+
tamperedChar +
|
|
88
|
+
ciphertext.slice(midIndex + 1);
|
|
89
|
+
|
|
90
|
+
// Ensure we actually changed something
|
|
91
|
+
expect(tamperedCiphertext).not.toBe(ciphertext);
|
|
92
|
+
expect(tamperedCiphertext.length).toBe(ciphertext.length);
|
|
93
|
+
|
|
94
|
+
// With try-catch wrapper, should return original ciphertext
|
|
95
|
+
const result = decryptField(tamperedCiphertext, workspaceKey);
|
|
96
|
+
expect(result).toBe(tamperedCiphertext);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle special characters', () => {
|
|
100
|
+
const plaintext = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\`~';
|
|
101
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
102
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
103
|
+
|
|
104
|
+
expect(decrypted).toBe(plaintext);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle newlines and tabs', () => {
|
|
108
|
+
const plaintext = 'Line 1\nLine 2\tTabbed\rCarriage return';
|
|
109
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
110
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
111
|
+
|
|
112
|
+
expect(decrypted).toBe(plaintext);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should handle mixed languages', () => {
|
|
116
|
+
const plaintext = 'English 日本語 العربية עברית';
|
|
117
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
118
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
119
|
+
|
|
120
|
+
expect(decrypted).toBe(plaintext);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should handle null bytes', () => {
|
|
124
|
+
const plaintext = 'Before\x00After';
|
|
125
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
126
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
127
|
+
|
|
128
|
+
expect(decrypted).toBe(plaintext);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ============================================================================
|
|
133
|
+
// Key Validation Tests
|
|
134
|
+
// ============================================================================
|
|
135
|
+
describe('key validation', () => {
|
|
136
|
+
it('should throw error for non-Buffer workspace key', () => {
|
|
137
|
+
expect(() => {
|
|
138
|
+
encryptField('test', 'not-a-buffer' as unknown as Buffer);
|
|
139
|
+
}).toThrow('Invalid workspaceKey: expected Buffer, got string');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should throw error for wrong key length', () => {
|
|
143
|
+
const shortKey = Buffer.alloc(16); // 128 bits instead of 256
|
|
144
|
+
|
|
145
|
+
expect(() => {
|
|
146
|
+
encryptField('test', shortKey);
|
|
147
|
+
}).toThrow(
|
|
148
|
+
'Invalid workspaceKey: expected 32 bytes for AES-256, got 16 bytes'
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should throw error for too long key', () => {
|
|
153
|
+
const longKey = Buffer.alloc(64); // 512 bits
|
|
154
|
+
|
|
155
|
+
expect(() => {
|
|
156
|
+
encryptField('test', longKey);
|
|
157
|
+
}).toThrow(
|
|
158
|
+
'Invalid workspaceKey: expected 32 bytes for AES-256, got 64 bytes'
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should accept valid 32-byte key', () => {
|
|
163
|
+
const validKey = generateWorkspaceKey();
|
|
164
|
+
expect(() => {
|
|
165
|
+
encryptField('test', validKey);
|
|
166
|
+
}).not.toThrow();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should throw for null workspace key', () => {
|
|
170
|
+
expect(() => {
|
|
171
|
+
encryptField('test', null as unknown as Buffer);
|
|
172
|
+
}).toThrow('Invalid workspaceKey: expected Buffer, got object');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should throw for undefined workspace key', () => {
|
|
176
|
+
expect(() => {
|
|
177
|
+
encryptField('test', undefined as unknown as Buffer);
|
|
178
|
+
}).toThrow('Invalid workspaceKey: expected Buffer, got undefined');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should throw for array workspace key', () => {
|
|
182
|
+
expect(() => {
|
|
183
|
+
encryptField('test', [1, 2, 3] as unknown as Buffer);
|
|
184
|
+
}).toThrow('Invalid workspaceKey: expected Buffer, got object');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Decryption Error Handling Tests
|
|
190
|
+
// ============================================================================
|
|
191
|
+
describe('decryption error handling', () => {
|
|
192
|
+
let workspaceKey: Buffer;
|
|
193
|
+
|
|
194
|
+
beforeEach(() => {
|
|
195
|
+
workspaceKey = generateWorkspaceKey();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should return original for too-short ciphertext', () => {
|
|
199
|
+
const shortCiphertext = 'abc'; // Too short to be valid encrypted data
|
|
200
|
+
|
|
201
|
+
const result = decryptField(shortCiphertext, workspaceKey);
|
|
202
|
+
expect(result).toBe(shortCiphertext);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should return original for invalid base64', () => {
|
|
206
|
+
// Invalid base64 will decode to something short
|
|
207
|
+
const invalidBase64 = '!!!invalid!!!';
|
|
208
|
+
|
|
209
|
+
const result = decryptField(invalidBase64, workspaceKey);
|
|
210
|
+
expect(result).toBe(invalidBase64);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should return original for corrupted auth tag', () => {
|
|
214
|
+
const plaintext = 'Secret data';
|
|
215
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
216
|
+
|
|
217
|
+
// Corrupt the last few characters (auth tag area)
|
|
218
|
+
const corrupted = `${ciphertext.slice(0, -4)}XXXX`;
|
|
219
|
+
|
|
220
|
+
const result = decryptField(corrupted, workspaceKey);
|
|
221
|
+
expect(result).toBe(corrupted);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should handle extremely long ciphertext gracefully', () => {
|
|
225
|
+
const plaintext = 'x'.repeat(100000);
|
|
226
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
227
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
228
|
+
|
|
229
|
+
expect(decrypted).toBe(plaintext);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateWorkspaceKey } from '../encryption-service';
|
|
3
|
+
|
|
4
|
+
describe('encryption-service', () => {
|
|
5
|
+
describe('generateWorkspaceKey', () => {
|
|
6
|
+
it('should generate a 256-bit key', () => {
|
|
7
|
+
const key = generateWorkspaceKey();
|
|
8
|
+
expect(key).toBeInstanceOf(Buffer);
|
|
9
|
+
expect(key.length).toBe(32); // 256 bits = 32 bytes
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should generate unique keys each time', () => {
|
|
13
|
+
const key1 = generateWorkspaceKey();
|
|
14
|
+
const key2 = generateWorkspaceKey();
|
|
15
|
+
expect(key1.equals(key2)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should generate cryptographically random keys', () => {
|
|
19
|
+
// Generate multiple keys and ensure they have reasonable entropy
|
|
20
|
+
const keys: Buffer[] = [];
|
|
21
|
+
for (let i = 0; i < 10; i++) {
|
|
22
|
+
keys.push(generateWorkspaceKey());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// All keys should be unique
|
|
26
|
+
const uniqueKeys = new Set(keys.map((k) => k.toString('hex')));
|
|
27
|
+
expect(uniqueKeys.size).toBe(10);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decryptCalendarEventFields,
|
|
4
|
+
decryptField,
|
|
5
|
+
decryptWorkspaceKey,
|
|
6
|
+
encryptCalendarEventFields,
|
|
7
|
+
encryptField,
|
|
8
|
+
encryptWorkspaceKey,
|
|
9
|
+
} from '../encryption-service';
|
|
10
|
+
import {
|
|
11
|
+
generateWorkspaceKey,
|
|
12
|
+
setupEncryptionEnv,
|
|
13
|
+
TEST_MASTER_KEY,
|
|
14
|
+
} from './test-helpers';
|
|
15
|
+
|
|
16
|
+
const SCRYPT_COVERAGE_TIMEOUT_MS = 30_000;
|
|
17
|
+
|
|
18
|
+
describe('encryption-service', () => {
|
|
19
|
+
setupEncryptionEnv();
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Concurrency and Performance Tests
|
|
23
|
+
// ============================================================================
|
|
24
|
+
describe('concurrency and performance', () => {
|
|
25
|
+
it('should handle concurrent encryptions', async () => {
|
|
26
|
+
const workspaceKey = generateWorkspaceKey();
|
|
27
|
+
const plaintexts = Array.from({ length: 100 }, (_, i) => `Message ${i}`);
|
|
28
|
+
|
|
29
|
+
const encryptions = plaintexts.map((pt) =>
|
|
30
|
+
Promise.resolve(encryptField(pt, workspaceKey))
|
|
31
|
+
);
|
|
32
|
+
const ciphertexts = await Promise.all(encryptions);
|
|
33
|
+
|
|
34
|
+
// All ciphertexts should be unique
|
|
35
|
+
const uniqueCiphertexts = new Set(ciphertexts);
|
|
36
|
+
expect(uniqueCiphertexts.size).toBe(100);
|
|
37
|
+
|
|
38
|
+
// All should decrypt correctly
|
|
39
|
+
ciphertexts.forEach((ct, i) => {
|
|
40
|
+
const decrypted = decryptField(ct, workspaceKey);
|
|
41
|
+
expect(decrypted).toBe(`Message ${i}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should handle concurrent key generations', async () => {
|
|
46
|
+
const generations = Array.from({ length: 100 }, () =>
|
|
47
|
+
Promise.resolve(generateWorkspaceKey())
|
|
48
|
+
);
|
|
49
|
+
const keys = await Promise.all(generations);
|
|
50
|
+
|
|
51
|
+
// All keys should be unique
|
|
52
|
+
const uniqueKeys = new Set(keys.map((k) => k.toString('hex')));
|
|
53
|
+
expect(uniqueKeys.size).toBe(100);
|
|
54
|
+
|
|
55
|
+
// All should be valid 32-byte keys
|
|
56
|
+
keys.forEach((key) => {
|
|
57
|
+
expect(key.length).toBe(32);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it(
|
|
62
|
+
'should benefit from key derivation cache',
|
|
63
|
+
async () => {
|
|
64
|
+
const workspaceKey = generateWorkspaceKey();
|
|
65
|
+
|
|
66
|
+
// Verify cache is working: multiple calls complete without error
|
|
67
|
+
// and produce valid encrypted output (deterministic check instead of timing)
|
|
68
|
+
const encrypted1 = await encryptWorkspaceKey(
|
|
69
|
+
workspaceKey,
|
|
70
|
+
TEST_MASTER_KEY
|
|
71
|
+
);
|
|
72
|
+
const encrypted2 = await encryptWorkspaceKey(
|
|
73
|
+
workspaceKey,
|
|
74
|
+
TEST_MASTER_KEY
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Both should be valid base64 strings
|
|
78
|
+
expect(encrypted1).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
|
79
|
+
expect(encrypted2).toMatch(/^[A-Za-z0-9+/]+=*$/);
|
|
80
|
+
|
|
81
|
+
// Each encryption produces different ciphertext (random IV)
|
|
82
|
+
expect(encrypted1).not.toBe(encrypted2);
|
|
83
|
+
|
|
84
|
+
// Both should decrypt to the same key
|
|
85
|
+
const decrypted1 = await decryptWorkspaceKey(
|
|
86
|
+
encrypted1,
|
|
87
|
+
TEST_MASTER_KEY
|
|
88
|
+
);
|
|
89
|
+
const decrypted2 = await decryptWorkspaceKey(
|
|
90
|
+
encrypted2,
|
|
91
|
+
TEST_MASTER_KEY
|
|
92
|
+
);
|
|
93
|
+
expect(decrypted1.equals(workspaceKey)).toBe(true);
|
|
94
|
+
expect(decrypted2.equals(workspaceKey)).toBe(true);
|
|
95
|
+
},
|
|
96
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Edge Cases and Boundary Tests
|
|
102
|
+
// ============================================================================
|
|
103
|
+
describe('edge cases', () => {
|
|
104
|
+
let workspaceKey: Buffer;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
workspaceKey = generateWorkspaceKey();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should handle maximum safe string length', () => {
|
|
111
|
+
// Test with a reasonably large string (1MB)
|
|
112
|
+
const largePlaintext = 'a'.repeat(1024 * 1024);
|
|
113
|
+
const ciphertext = encryptField(largePlaintext, workspaceKey);
|
|
114
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
115
|
+
|
|
116
|
+
expect(decrypted).toBe(largePlaintext);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should handle single character', () => {
|
|
120
|
+
const plaintext = 'x';
|
|
121
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
122
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
123
|
+
|
|
124
|
+
expect(decrypted).toBe(plaintext);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle only whitespace', () => {
|
|
128
|
+
const plaintext = ' \t\n ';
|
|
129
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
130
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
131
|
+
|
|
132
|
+
expect(decrypted).toBe(plaintext);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should handle extremely long location', () => {
|
|
136
|
+
const event = {
|
|
137
|
+
title: 'Meeting',
|
|
138
|
+
description: 'Desc',
|
|
139
|
+
location: 'Room '.repeat(10000),
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const encrypted = encryptCalendarEventFields(event, workspaceKey);
|
|
143
|
+
const decrypted = decryptCalendarEventFields(encrypted, workspaceKey);
|
|
144
|
+
|
|
145
|
+
expect(decrypted.location).toBe(event.location);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle HTML content', () => {
|
|
149
|
+
const plaintext = '<script>alert("xss")</script><b>Bold</b>';
|
|
150
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
151
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
152
|
+
|
|
153
|
+
expect(decrypted).toBe(plaintext);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle JSON content', () => {
|
|
157
|
+
const plaintext = JSON.stringify({
|
|
158
|
+
key: 'value',
|
|
159
|
+
nested: { array: [1, 2, 3] },
|
|
160
|
+
});
|
|
161
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
162
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
163
|
+
|
|
164
|
+
expect(decrypted).toBe(plaintext);
|
|
165
|
+
expect(JSON.parse(decrypted)).toEqual({
|
|
166
|
+
key: 'value',
|
|
167
|
+
nested: { array: [1, 2, 3] },
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('should handle URL content', () => {
|
|
172
|
+
const plaintext = 'https://example.com/path?query=value&other=123#hash';
|
|
173
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
174
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
175
|
+
|
|
176
|
+
expect(decrypted).toBe(plaintext);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('should handle markdown content', () => {
|
|
180
|
+
const plaintext = '# Heading\n\n**bold** _italic_ `code`\n\n- list item';
|
|
181
|
+
const ciphertext = encryptField(plaintext, workspaceKey);
|
|
182
|
+
const decrypted = decryptField(ciphertext, workspaceKey);
|
|
183
|
+
|
|
184
|
+
expect(decrypted).toBe(plaintext);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { afterEach, beforeEach } from 'vitest';
|
|
2
|
+
import { generateWorkspaceKey } from '../encryption-service';
|
|
3
|
+
|
|
4
|
+
export const TEST_MASTER_KEY = 'test-master-key-for-unit-testing-only';
|
|
5
|
+
|
|
6
|
+
export function setupEncryptionEnv() {
|
|
7
|
+
let originalEnv: string | undefined;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
originalEnv = process.env.ENCRYPTION_MASTER_KEY;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
if (originalEnv !== undefined) {
|
|
15
|
+
process.env.ENCRYPTION_MASTER_KEY = originalEnv;
|
|
16
|
+
} else {
|
|
17
|
+
delete process.env.ENCRYPTION_MASTER_KEY;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { generateWorkspaceKey };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decryptWorkspaceKey,
|
|
4
|
+
encryptWorkspaceKey,
|
|
5
|
+
} from '../encryption-service';
|
|
6
|
+
import {
|
|
7
|
+
generateWorkspaceKey,
|
|
8
|
+
setupEncryptionEnv,
|
|
9
|
+
TEST_MASTER_KEY,
|
|
10
|
+
} from './test-helpers';
|
|
11
|
+
|
|
12
|
+
const SCRYPT_COVERAGE_TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
describe('encryption-service', () => {
|
|
15
|
+
setupEncryptionEnv();
|
|
16
|
+
|
|
17
|
+
describe('workspace key encryption/decryption', () => {
|
|
18
|
+
it(
|
|
19
|
+
'should encrypt and decrypt workspace key correctly',
|
|
20
|
+
async () => {
|
|
21
|
+
const originalKey = generateWorkspaceKey();
|
|
22
|
+
const encryptedKey = await encryptWorkspaceKey(
|
|
23
|
+
originalKey,
|
|
24
|
+
TEST_MASTER_KEY
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(encryptedKey).toBeTruthy();
|
|
28
|
+
expect(typeof encryptedKey).toBe('string');
|
|
29
|
+
|
|
30
|
+
const decryptedKey = await decryptWorkspaceKey(
|
|
31
|
+
encryptedKey,
|
|
32
|
+
TEST_MASTER_KEY
|
|
33
|
+
);
|
|
34
|
+
expect(decryptedKey.equals(originalKey)).toBe(true);
|
|
35
|
+
},
|
|
36
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
it(
|
|
40
|
+
'should produce different ciphertext for same key (random IV)',
|
|
41
|
+
async () => {
|
|
42
|
+
const key = generateWorkspaceKey();
|
|
43
|
+
const encrypted1 = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
|
|
44
|
+
const encrypted2 = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
|
|
45
|
+
|
|
46
|
+
expect(encrypted1).not.toBe(encrypted2);
|
|
47
|
+
},
|
|
48
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
it(
|
|
52
|
+
'should fail decryption with wrong master key',
|
|
53
|
+
async () => {
|
|
54
|
+
const key = generateWorkspaceKey();
|
|
55
|
+
const encryptedKey = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
|
|
56
|
+
|
|
57
|
+
await expect(
|
|
58
|
+
decryptWorkspaceKey(encryptedKey, 'wrong-master-key')
|
|
59
|
+
).rejects.toThrow();
|
|
60
|
+
},
|
|
61
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
it(
|
|
65
|
+
'should handle different master key lengths',
|
|
66
|
+
async () => {
|
|
67
|
+
const key = generateWorkspaceKey();
|
|
68
|
+
|
|
69
|
+
// Short master key
|
|
70
|
+
const shortMasterKey = 'short';
|
|
71
|
+
const encrypted1 = await encryptWorkspaceKey(key, shortMasterKey);
|
|
72
|
+
const decrypted1 = await decryptWorkspaceKey(
|
|
73
|
+
encrypted1,
|
|
74
|
+
shortMasterKey
|
|
75
|
+
);
|
|
76
|
+
expect(decrypted1.equals(key)).toBe(true);
|
|
77
|
+
|
|
78
|
+
// Long master key
|
|
79
|
+
const longMasterKey = 'a'.repeat(256);
|
|
80
|
+
const encrypted2 = await encryptWorkspaceKey(key, longMasterKey);
|
|
81
|
+
const decrypted2 = await decryptWorkspaceKey(encrypted2, longMasterKey);
|
|
82
|
+
expect(decrypted2.equals(key)).toBe(true);
|
|
83
|
+
},
|
|
84
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
it(
|
|
88
|
+
'should produce valid base64 encoded output',
|
|
89
|
+
async () => {
|
|
90
|
+
const key = generateWorkspaceKey();
|
|
91
|
+
const encrypted = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
|
|
92
|
+
|
|
93
|
+
// Should be valid base64
|
|
94
|
+
expect(() => Buffer.from(encrypted, 'base64')).not.toThrow();
|
|
95
|
+
|
|
96
|
+
// Decoded buffer should have minimum length (IV + ciphertext + authTag)
|
|
97
|
+
const decoded = Buffer.from(encrypted, 'base64');
|
|
98
|
+
expect(decoded.length).toBeGreaterThanOrEqual(12 + 32 + 16); // IV + key + tag
|
|
99
|
+
},
|
|
100
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
it(
|
|
104
|
+
'should fail on corrupted encrypted key',
|
|
105
|
+
async () => {
|
|
106
|
+
const key = generateWorkspaceKey();
|
|
107
|
+
const encrypted = await encryptWorkspaceKey(key, TEST_MASTER_KEY);
|
|
108
|
+
|
|
109
|
+
// Corrupt the encrypted data by modifying the auth tag (last 16 bytes)
|
|
110
|
+
// This guarantees decryption will fail because AES-GCM uses auth tag for integrity
|
|
111
|
+
const decoded = Buffer.from(encrypted, 'base64');
|
|
112
|
+
// Flip bits in the auth tag (last 16 bytes) - decoded is guaranteed to have min size
|
|
113
|
+
const lastIdx = decoded.length - 1;
|
|
114
|
+
const secondLastIdx = decoded.length - 2;
|
|
115
|
+
decoded.writeUInt8(decoded.readUInt8(lastIdx) ^ 0xff, lastIdx);
|
|
116
|
+
decoded.writeUInt8(
|
|
117
|
+
decoded.readUInt8(secondLastIdx) ^ 0xff,
|
|
118
|
+
secondLastIdx
|
|
119
|
+
);
|
|
120
|
+
const corrupted = decoded.toString('base64');
|
|
121
|
+
|
|
122
|
+
await expect(
|
|
123
|
+
decryptWorkspaceKey(corrupted, TEST_MASTER_KEY)
|
|
124
|
+
).rejects.toThrow();
|
|
125
|
+
},
|
|
126
|
+
SCRYPT_COVERAGE_TIMEOUT_MS
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|