@bananalink-sdk/protocol 1.2.7

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.
Files changed (158) hide show
  1. package/README.md +604 -0
  2. package/dist/chunk-32OWUOZ3.js +308 -0
  3. package/dist/chunk-32OWUOZ3.js.map +1 -0
  4. package/dist/chunk-65HNHRJK.cjs +123 -0
  5. package/dist/chunk-65HNHRJK.cjs.map +1 -0
  6. package/dist/chunk-7KYDLL3B.js +480 -0
  7. package/dist/chunk-7KYDLL3B.js.map +1 -0
  8. package/dist/chunk-A6FLEJ7R.cjs +62 -0
  9. package/dist/chunk-A6FLEJ7R.cjs.map +1 -0
  10. package/dist/chunk-CUJK7ZTS.js +217 -0
  11. package/dist/chunk-CUJK7ZTS.js.map +1 -0
  12. package/dist/chunk-GI3BUPIH.cjs +236 -0
  13. package/dist/chunk-GI3BUPIH.cjs.map +1 -0
  14. package/dist/chunk-JXHV66Q4.js +106 -0
  15. package/dist/chunk-JXHV66Q4.js.map +1 -0
  16. package/dist/chunk-KNGZKGRS.cjs +552 -0
  17. package/dist/chunk-KNGZKGRS.cjs.map +1 -0
  18. package/dist/chunk-LELPCIE7.js +840 -0
  19. package/dist/chunk-LELPCIE7.js.map +1 -0
  20. package/dist/chunk-MCZG7QEM.cjs +310 -0
  21. package/dist/chunk-MCZG7QEM.cjs.map +1 -0
  22. package/dist/chunk-TCVKC227.js +56 -0
  23. package/dist/chunk-TCVKC227.js.map +1 -0
  24. package/dist/chunk-VXLUSU5B.cjs +856 -0
  25. package/dist/chunk-VXLUSU5B.cjs.map +1 -0
  26. package/dist/chunk-WCQVDF3K.js +12 -0
  27. package/dist/chunk-WCQVDF3K.js.map +1 -0
  28. package/dist/chunk-WGEGR3DF.cjs +15 -0
  29. package/dist/chunk-WGEGR3DF.cjs.map +1 -0
  30. package/dist/client-session-claim-3QF3noOr.d.ts +197 -0
  31. package/dist/client-session-claim-C4lUik3b.d.cts +197 -0
  32. package/dist/core-DMhuNfoz.d.cts +62 -0
  33. package/dist/core-DMhuNfoz.d.ts +62 -0
  34. package/dist/crypto/providers/noble-provider.cjs +14 -0
  35. package/dist/crypto/providers/noble-provider.cjs.map +1 -0
  36. package/dist/crypto/providers/noble-provider.d.cts +30 -0
  37. package/dist/crypto/providers/noble-provider.d.ts +30 -0
  38. package/dist/crypto/providers/noble-provider.js +5 -0
  39. package/dist/crypto/providers/noble-provider.js.map +1 -0
  40. package/dist/crypto/providers/node-provider.cjs +308 -0
  41. package/dist/crypto/providers/node-provider.cjs.map +1 -0
  42. package/dist/crypto/providers/node-provider.d.cts +32 -0
  43. package/dist/crypto/providers/node-provider.d.ts +32 -0
  44. package/dist/crypto/providers/node-provider.js +306 -0
  45. package/dist/crypto/providers/node-provider.js.map +1 -0
  46. package/dist/crypto/providers/quickcrypto-provider.cjs +339 -0
  47. package/dist/crypto/providers/quickcrypto-provider.cjs.map +1 -0
  48. package/dist/crypto/providers/quickcrypto-provider.d.cts +34 -0
  49. package/dist/crypto/providers/quickcrypto-provider.d.ts +34 -0
  50. package/dist/crypto/providers/quickcrypto-provider.js +337 -0
  51. package/dist/crypto/providers/quickcrypto-provider.js.map +1 -0
  52. package/dist/crypto/providers/webcrypto-provider.cjs +310 -0
  53. package/dist/crypto/providers/webcrypto-provider.cjs.map +1 -0
  54. package/dist/crypto/providers/webcrypto-provider.d.cts +30 -0
  55. package/dist/crypto/providers/webcrypto-provider.d.ts +30 -0
  56. package/dist/crypto/providers/webcrypto-provider.js +308 -0
  57. package/dist/crypto/providers/webcrypto-provider.js.map +1 -0
  58. package/dist/crypto-BUS06Qz-.d.cts +40 -0
  59. package/dist/crypto-BUS06Qz-.d.ts +40 -0
  60. package/dist/crypto-export.cjs +790 -0
  61. package/dist/crypto-export.cjs.map +1 -0
  62. package/dist/crypto-export.d.cts +257 -0
  63. package/dist/crypto-export.d.ts +257 -0
  64. package/dist/crypto-export.js +709 -0
  65. package/dist/crypto-export.js.map +1 -0
  66. package/dist/crypto-provider-deYoVIxi.d.cts +36 -0
  67. package/dist/crypto-provider-deYoVIxi.d.ts +36 -0
  68. package/dist/index.cjs +615 -0
  69. package/dist/index.cjs.map +1 -0
  70. package/dist/index.d.cts +379 -0
  71. package/dist/index.d.ts +379 -0
  72. package/dist/index.js +504 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/schemas-export.cjs +294 -0
  75. package/dist/schemas-export.cjs.map +1 -0
  76. package/dist/schemas-export.d.cts +1598 -0
  77. package/dist/schemas-export.d.ts +1598 -0
  78. package/dist/schemas-export.js +5 -0
  79. package/dist/schemas-export.js.map +1 -0
  80. package/dist/siwe-export.cjs +237 -0
  81. package/dist/siwe-export.cjs.map +1 -0
  82. package/dist/siwe-export.d.cts +27 -0
  83. package/dist/siwe-export.d.ts +27 -0
  84. package/dist/siwe-export.js +228 -0
  85. package/dist/siwe-export.js.map +1 -0
  86. package/dist/testing.cjs +54 -0
  87. package/dist/testing.cjs.map +1 -0
  88. package/dist/testing.d.cts +20 -0
  89. package/dist/testing.d.ts +20 -0
  90. package/dist/testing.js +51 -0
  91. package/dist/testing.js.map +1 -0
  92. package/dist/validation-export.cjs +359 -0
  93. package/dist/validation-export.cjs.map +1 -0
  94. package/dist/validation-export.d.cts +3 -0
  95. package/dist/validation-export.d.ts +3 -0
  96. package/dist/validation-export.js +6 -0
  97. package/dist/validation-export.js.map +1 -0
  98. package/dist/validators-export.cjs +73 -0
  99. package/dist/validators-export.cjs.map +1 -0
  100. package/dist/validators-export.d.cts +37 -0
  101. package/dist/validators-export.d.ts +37 -0
  102. package/dist/validators-export.js +4 -0
  103. package/dist/validators-export.js.map +1 -0
  104. package/package.json +140 -0
  105. package/src/constants/index.ts +205 -0
  106. package/src/crypto/context.ts +228 -0
  107. package/src/crypto/diagnostics.ts +772 -0
  108. package/src/crypto/errors.ts +114 -0
  109. package/src/crypto/index.ts +89 -0
  110. package/src/crypto/payload-handler.ts +102 -0
  111. package/src/crypto/providers/compliance-provider.ts +579 -0
  112. package/src/crypto/providers/factory.ts +204 -0
  113. package/src/crypto/providers/index.ts +44 -0
  114. package/src/crypto/providers/noble-provider.ts +392 -0
  115. package/src/crypto/providers/node-provider.ts +433 -0
  116. package/src/crypto/providers/quickcrypto-provider.ts +483 -0
  117. package/src/crypto/providers/registry.ts +129 -0
  118. package/src/crypto/providers/webcrypto-provider.ts +364 -0
  119. package/src/crypto/session-security.ts +185 -0
  120. package/src/crypto/types.ts +93 -0
  121. package/src/crypto/utils.ts +190 -0
  122. package/src/crypto-export.ts +21 -0
  123. package/src/index.ts +38 -0
  124. package/src/schemas/auth.ts +60 -0
  125. package/src/schemas/client-messages.ts +57 -0
  126. package/src/schemas/core.ts +144 -0
  127. package/src/schemas/crypto.ts +65 -0
  128. package/src/schemas/discovery.ts +79 -0
  129. package/src/schemas/index.ts +239 -0
  130. package/src/schemas/relay-messages.ts +45 -0
  131. package/src/schemas/wallet-messages.ts +177 -0
  132. package/src/schemas-export.ts +23 -0
  133. package/src/siwe-export.ts +27 -0
  134. package/src/testing.ts +71 -0
  135. package/src/types/auth.ts +60 -0
  136. package/src/types/client-messages.ts +84 -0
  137. package/src/types/core.ts +131 -0
  138. package/src/types/crypto-provider.ts +264 -0
  139. package/src/types/crypto.ts +90 -0
  140. package/src/types/discovery.ts +50 -0
  141. package/src/types/errors.ts +87 -0
  142. package/src/types/index.ts +197 -0
  143. package/src/types/post-auth-operations.ts +363 -0
  144. package/src/types/providers.ts +72 -0
  145. package/src/types/relay-messages.ts +60 -0
  146. package/src/types/request-lifecycle.ts +161 -0
  147. package/src/types/signing-operations.ts +99 -0
  148. package/src/types/wallet-messages.ts +251 -0
  149. package/src/utils/client-session-claim.ts +188 -0
  150. package/src/utils/index.ts +54 -0
  151. package/src/utils/public-keys.ts +49 -0
  152. package/src/utils/siwe.ts +362 -0
  153. package/src/utils/url-decoding.ts +126 -0
  154. package/src/utils/url-encoding.ts +144 -0
  155. package/src/utils/wallet-session-claim.ts +188 -0
  156. package/src/validation-export.ts +32 -0
  157. package/src/validators/index.ts +222 -0
  158. package/src/validators-export.ts +8 -0
@@ -0,0 +1,364 @@
1
+ import type { Logger } from '@bananalink-sdk/logger';
2
+ import type { CryptoProvider, CryptoKeyLike, ProviderKeyPair } from '../../types/crypto-provider';
3
+ import { registerCryptoProvider } from './registry';
4
+
5
+ /**
6
+ * WebCrypto CryptoKey wrapper to implement CryptoKeyLike interface
7
+ */
8
+ class WebCryptoKeyWrapper implements CryptoKeyLike {
9
+ constructor(private readonly cryptoKey: CryptoKey) {}
10
+
11
+ get type(): 'public' | 'private' | 'secret' {
12
+ return this.cryptoKey.type as 'public' | 'private' | 'secret';
13
+ }
14
+
15
+ get algorithm(): string {
16
+ if (typeof this.cryptoKey.algorithm === 'string') {
17
+ return this.cryptoKey.algorithm;
18
+ }
19
+ return this.cryptoKey.algorithm.name;
20
+ }
21
+
22
+ get extractable(): boolean {
23
+ return this.cryptoKey.extractable;
24
+ }
25
+
26
+ get usages(): readonly string[] {
27
+ return this.cryptoKey.usages;
28
+ }
29
+
30
+ get nativeKey(): CryptoKey {
31
+ return this.cryptoKey;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Helper function to unwrap CryptoKeyLike to native CryptoKey
37
+ */
38
+ function unwrapCryptoKey(keyLike: CryptoKeyLike): CryptoKey {
39
+ if (keyLike instanceof WebCryptoKeyWrapper) {
40
+ return keyLike.nativeKey;
41
+ }
42
+ // For backward compatibility, assume it's already a CryptoKey
43
+ return keyLike as unknown as CryptoKey;
44
+ }
45
+
46
+ /**
47
+ * Web Crypto API implementation of CryptoProvider
48
+ * Works in browsers and Node.js environments with Web Crypto support
49
+ */
50
+ export class WebCryptoProvider implements CryptoProvider {
51
+ public readonly name = 'WebCrypto';
52
+ private readonly logger?: Logger;
53
+
54
+ public get isAvailable(): boolean {
55
+ return typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined';
56
+ }
57
+
58
+ constructor(logger?: Logger) {
59
+ if (!this.isAvailable) {
60
+ throw new Error('Web Crypto API not available in this environment');
61
+ }
62
+ this.logger = logger?.child({ component: 'WebCryptoProvider' });
63
+ }
64
+
65
+ /**
66
+ * Generate ECDH P-256 key pair
67
+ */
68
+ async generateKeyPair(): Promise<ProviderKeyPair> {
69
+ this.logger?.debug('Generating ECDH P-256 key pair');
70
+
71
+ const keyPair = await crypto.subtle.generateKey(
72
+ {
73
+ name: 'ECDH',
74
+ namedCurve: 'P-256',
75
+ },
76
+ true, // extractable
77
+ ['deriveKey']
78
+ );
79
+
80
+ this.logger?.debug('Key pair generation completed');
81
+ return {
82
+ publicKey: new WebCryptoKeyWrapper(keyPair.publicKey),
83
+ privateKey: new WebCryptoKeyWrapper(keyPair.privateKey),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Export public key to ArrayBuffer (raw format)
89
+ */
90
+ async exportPublicKey(publicKey: CryptoKeyLike): Promise<ArrayBuffer> {
91
+ return crypto.subtle.exportKey('raw', unwrapCryptoKey(publicKey));
92
+ }
93
+
94
+ /**
95
+ * Export private key to ArrayBuffer (raw format)
96
+ */
97
+ async exportPrivateKey(privateKey: CryptoKeyLike): Promise<ArrayBuffer> {
98
+ // WebCrypto doesn't support 'raw' export for ECDH private keys
99
+ // Use JWK export and extract the d-value (32 bytes)
100
+ const jwk = await crypto.subtle.exportKey('jwk', unwrapCryptoKey(privateKey));
101
+ const dValue = new Uint8Array(Buffer.from(jwk.d as string, 'base64url'));
102
+ return dValue.buffer.slice(dValue.byteOffset, dValue.byteOffset + dValue.byteLength);
103
+ }
104
+
105
+ /**
106
+ * Import public key from ArrayBuffer (raw format)
107
+ */
108
+ async importPublicKey(keyData: ArrayBuffer): Promise<CryptoKeyLike> {
109
+ const keyBytes = new Uint8Array(keyData);
110
+
111
+ this.logger?.debug('importPublicKey called', {
112
+ keyLength: keyBytes.length,
113
+ keyBytesFirst20: Array.from(keyBytes.slice(0, 20)).map(b => b.toString(16).padStart(2, '0')).join(' ')
114
+ });
115
+
116
+ try {
117
+ const cryptoKey = await crypto.subtle.importKey(
118
+ 'raw',
119
+ keyData,
120
+ {
121
+ name: 'ECDH',
122
+ namedCurve: 'P-256',
123
+ },
124
+ true,
125
+ []
126
+ );
127
+
128
+ this.logger?.debug('Public key import successful');
129
+ return new WebCryptoKeyWrapper(cryptoKey);
130
+ } catch (error) {
131
+ this.logger?.error('Public key import failed', {
132
+ error: {
133
+ message: error instanceof Error ? error.message : String(error),
134
+ stack: error instanceof Error ? error.stack : undefined
135
+ }
136
+ });
137
+ throw new Error(`Invalid P-256 public key: ${String(error)}`);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Import private key from ArrayBuffer (raw format)
143
+ */
144
+ async importPrivateKey(keyData: ArrayBuffer): Promise<CryptoKeyLike> {
145
+ this.logger?.debug('Importing private key', {
146
+ keyLength: keyData.byteLength
147
+ });
148
+
149
+ try {
150
+ const cryptoKey = await crypto.subtle.importKey(
151
+ 'raw',
152
+ keyData,
153
+ {
154
+ name: 'ECDH',
155
+ namedCurve: 'P-256',
156
+ },
157
+ true,
158
+ ['deriveKey']
159
+ );
160
+
161
+ this.logger?.debug('Private key import successful');
162
+ return new WebCryptoKeyWrapper(cryptoKey);
163
+ } catch (error) {
164
+ this.logger?.error('Private key import failed', {
165
+ error: {
166
+ message: error instanceof Error ? error.message : String(error),
167
+ stack: error instanceof Error ? error.stack : undefined
168
+ }
169
+ });
170
+ throw new Error(`Invalid P-256 private key: ${String(error)}`);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Derive shared secret from ECDH key agreement
176
+ */
177
+ async deriveSharedSecret(privateKey: CryptoKeyLike, publicKey: CryptoKeyLike): Promise<CryptoKeyLike> {
178
+ this.logger?.debug('Deriving shared secret using ECDH');
179
+
180
+ try {
181
+ const derivedKey = await crypto.subtle.deriveKey(
182
+ {
183
+ name: 'ECDH',
184
+ public: unwrapCryptoKey(publicKey),
185
+ },
186
+ unwrapCryptoKey(privateKey),
187
+ {
188
+ name: 'AES-GCM',
189
+ length: 256,
190
+ },
191
+ true,
192
+ ['encrypt', 'decrypt']
193
+ );
194
+
195
+ this.logger?.debug('Shared secret derivation completed');
196
+ return new WebCryptoKeyWrapper(derivedKey);
197
+ } catch (error) {
198
+ this.logger?.error('Shared secret derivation failed', {
199
+ error: {
200
+ message: error instanceof Error ? error.message : String(error),
201
+ stack: error instanceof Error ? error.stack : undefined
202
+ }
203
+ });
204
+ throw error;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Derive AES-GCM key from shared secret using HKDF
210
+ */
211
+ async deriveEncryptionKey(sharedSecret: CryptoKeyLike, salt: ArrayBuffer, info: ArrayBuffer): Promise<CryptoKeyLike> {
212
+ // Import shared secret as HKDF base key
213
+ const baseKey = await crypto.subtle.importKey(
214
+ 'raw',
215
+ await crypto.subtle.exportKey('raw', unwrapCryptoKey(sharedSecret)),
216
+ { name: 'HKDF' },
217
+ false,
218
+ ['deriveKey']
219
+ );
220
+
221
+ // Derive encryption key using HKDF
222
+ const derivedKey = await crypto.subtle.deriveKey(
223
+ {
224
+ name: 'HKDF',
225
+ salt: salt,
226
+ info: info,
227
+ hash: 'SHA-256',
228
+ },
229
+ baseKey,
230
+ { name: 'AES-GCM', length: 256 },
231
+ true,
232
+ ['encrypt', 'decrypt']
233
+ );
234
+ return new WebCryptoKeyWrapper(derivedKey);
235
+ }
236
+
237
+ /**
238
+ * Generate random bytes
239
+ */
240
+ randomBytes(length: number): ArrayBuffer {
241
+ const buffer = new ArrayBuffer(length);
242
+ const view = new Uint8Array(buffer);
243
+ crypto.getRandomValues(view);
244
+ return buffer;
245
+ }
246
+
247
+ /**
248
+ * Encrypt data using AES-GCM
249
+ */
250
+ async encrypt(key: CryptoKeyLike, data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
251
+ this.logger?.debug('Encrypting data with AES-GCM', {
252
+ dataSize: data.byteLength,
253
+ ivSize: iv.byteLength
254
+ });
255
+
256
+ try {
257
+ const ciphertext = await crypto.subtle.encrypt(
258
+ {
259
+ name: 'AES-GCM',
260
+ iv: iv,
261
+ tagLength: 128, // 128-bit tag
262
+ },
263
+ unwrapCryptoKey(key),
264
+ data
265
+ );
266
+
267
+ this.logger?.debug('Encryption completed', {
268
+ ciphertextSize: ciphertext.byteLength
269
+ });
270
+ return ciphertext;
271
+ } catch (error) {
272
+ this.logger?.error('Encryption failed', {
273
+ error: {
274
+ message: error instanceof Error ? error.message : String(error),
275
+ stack: error instanceof Error ? error.stack : undefined
276
+ }
277
+ });
278
+ throw error;
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Decrypt data using AES-GCM
284
+ */
285
+ async decrypt(key: CryptoKeyLike, data: ArrayBuffer, iv: ArrayBuffer): Promise<ArrayBuffer> {
286
+ this.logger?.debug('Decrypting data with AES-GCM', {
287
+ dataSize: data.byteLength,
288
+ ivSize: iv.byteLength
289
+ });
290
+
291
+ try {
292
+ const plaintext = await crypto.subtle.decrypt(
293
+ {
294
+ name: 'AES-GCM',
295
+ iv: iv,
296
+ tagLength: 128, // 128-bit tag
297
+ },
298
+ unwrapCryptoKey(key),
299
+ data
300
+ );
301
+
302
+ this.logger?.debug('Decryption completed', {
303
+ plaintextSize: plaintext.byteLength
304
+ });
305
+ return plaintext;
306
+ } catch (error) {
307
+ this.logger?.error('Decryption failed', {
308
+ error: {
309
+ message: error instanceof Error ? error.message : String(error),
310
+ stack: error instanceof Error ? error.stack : undefined
311
+ }
312
+ });
313
+ throw error;
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Generate HMAC-SHA256
319
+ */
320
+ async generateHMAC(key: CryptoKeyLike, data: ArrayBuffer): Promise<ArrayBuffer> {
321
+ // Convert AES key to HMAC key
322
+ const hmacKey = await crypto.subtle.importKey(
323
+ 'raw',
324
+ await crypto.subtle.exportKey('raw', unwrapCryptoKey(key)),
325
+ { name: 'HMAC', hash: 'SHA-256' },
326
+ false,
327
+ ['sign']
328
+ );
329
+
330
+ return crypto.subtle.sign('HMAC', hmacKey, data);
331
+ }
332
+
333
+ /**
334
+ * Verify HMAC-SHA256
335
+ */
336
+ async verifyHMAC(key: CryptoKeyLike, data: ArrayBuffer, mac: ArrayBuffer): Promise<boolean> {
337
+ // Convert AES key to HMAC key
338
+ const hmacKey = await crypto.subtle.importKey(
339
+ 'raw',
340
+ await crypto.subtle.exportKey('raw', unwrapCryptoKey(key)),
341
+ { name: 'HMAC', hash: 'SHA-256' },
342
+ false,
343
+ ['verify']
344
+ );
345
+
346
+ return crypto.subtle.verify('HMAC', hmacKey, mac, data);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Self-register WebCrypto provider on import
352
+ * This allows the provider to be available when explicitly imported
353
+ */
354
+ registerCryptoProvider('webcrypto', () => new WebCryptoProvider());
355
+
356
+ // TypeScript module augmentation to track this provider is available
357
+ declare global {
358
+ // eslint-disable-next-line @typescript-eslint/no-namespace
359
+ namespace BananaLink {
360
+ interface RegisteredCryptoProviders {
361
+ webcrypto: true;
362
+ }
363
+ }
364
+ }
@@ -0,0 +1,185 @@
1
+ import type { Logger } from '@bananalink-sdk/logger';
2
+ import type { SessionKeys, EncryptedMessage, CryptoConfig } from './types';
3
+ import { arrayBufferToBase64, base64ToArrayBuffer, stringToArrayBuffer, arrayBufferToString } from './utils';
4
+ import type { CryptoProvider, CryptoKeyLike, ProviderKeyPair } from './providers';
5
+ import { createCryptoProvider } from './providers';
6
+
7
+ const CRYPTO_CONFIG: CryptoConfig = {
8
+ algorithm: 'AES-GCM',
9
+ keyLength: 256,
10
+ ivLength: 12,
11
+ tagLength: 128, // tagLength is in bits for Web Crypto API (128 bits = 16 bytes)
12
+ curve: 'P-256',
13
+ };
14
+
15
+ export class SessionSecurity {
16
+ private keyPair?: ProviderKeyPair;
17
+ private sharedSecret?: CryptoKeyLike;
18
+ private sessionId: string;
19
+ private encryptionKey?: CryptoKeyLike; // For AES-GCM
20
+ private provider: CryptoProvider;
21
+
22
+ constructor(sessionId?: string, provider?: CryptoProvider, logger?: Logger) {
23
+ this.provider = provider || createCryptoProvider(undefined, logger);
24
+ this.sessionId = sessionId || this.generateSessionId();
25
+ }
26
+
27
+ /**
28
+ * Get the crypto provider used by this session
29
+ */
30
+ getCryptoProvider(): CryptoProvider {
31
+ return this.provider;
32
+ }
33
+
34
+ /**
35
+ * Establish a new session with ephemeral key generation
36
+ */
37
+ static async establishSession(sessionId?: string, provider?: CryptoProvider, logger?: Logger): Promise<SessionSecurity> {
38
+ const session = new SessionSecurity(sessionId, provider, logger);
39
+ await session.generateKeyPair();
40
+ return session;
41
+ }
42
+
43
+ /**
44
+ * Generate ECDH key pair for this session
45
+ */
46
+ private async generateKeyPair(): Promise<void> {
47
+ this.keyPair = await this.provider.generateKeyPair();
48
+ }
49
+
50
+ /**
51
+ * Get the public key for sharing (base64 encoded)
52
+ */
53
+ async getPublicKey(): Promise<string> {
54
+ if (!this.keyPair?.publicKey) {
55
+ throw new Error('Key pair not generated. Call generateKeyPair() first.');
56
+ }
57
+
58
+ const exported = await this.provider.exportPublicKey(this.keyPair.publicKey);
59
+ return arrayBufferToBase64(exported);
60
+ }
61
+
62
+ /**
63
+ * Derive shared secret from peer's public key
64
+ */
65
+ async deriveSharedSecret(peerPublicKeyBase64: string): Promise<void> {
66
+ if (!this.keyPair?.privateKey) {
67
+ throw new Error('Key pair not generated');
68
+ }
69
+
70
+ const peerPublicKeyBuffer = base64ToArrayBuffer(peerPublicKeyBase64);
71
+ const importedPeerKey = await this.provider.importPublicKey(peerPublicKeyBuffer);
72
+
73
+ this.sharedSecret = await this.provider.deriveSharedSecret(this.keyPair.privateKey, importedPeerKey);
74
+ }
75
+
76
+ /**
77
+ * Derives an encryption key from the shared secret using HKDF.
78
+ * @param salt - A non-secret random value.
79
+ * @param info - Context-specific information.
80
+ */
81
+ async deriveEncryptionKey(salt: ArrayBuffer, info: ArrayBuffer): Promise<void> {
82
+ if (!this.sharedSecret) {
83
+ throw new Error('Shared secret not derived yet.');
84
+ }
85
+
86
+ this.encryptionKey = await this.provider.deriveEncryptionKey(this.sharedSecret, salt, info);
87
+ }
88
+
89
+ /**
90
+ * Encrypt a message using the derived session key
91
+ */
92
+ async encrypt(message: Record<string, unknown>): Promise<EncryptedMessage> {
93
+ if (!this.encryptionKey) {
94
+ throw new Error('Encryption key not derived');
95
+ }
96
+
97
+ const iv = this.provider.randomBytes(CRYPTO_CONFIG.ivLength);
98
+ const plaintext = stringToArrayBuffer(JSON.stringify(message));
99
+
100
+ const ciphertext = await this.provider.encrypt(this.encryptionKey, plaintext, iv);
101
+
102
+ // Generate HMAC for authentication
103
+ const dataToSign = new Uint8Array([
104
+ ...new Uint8Array(iv),
105
+ ...new Uint8Array(ciphertext)
106
+ ]);
107
+
108
+ const macBuffer = await this.provider.generateHMAC(this.encryptionKey, dataToSign.buffer);
109
+
110
+ return {
111
+ iv: arrayBufferToBase64(iv),
112
+ ciphertext: arrayBufferToBase64(ciphertext),
113
+ mac: arrayBufferToBase64(macBuffer),
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Decrypt a message using the derived session key
119
+ */
120
+ async decrypt(encryptedMessage: EncryptedMessage): Promise<Record<string, unknown>> {
121
+ if (!this.encryptionKey) {
122
+ throw new Error('Encryption key not derived');
123
+ }
124
+
125
+ const iv = base64ToArrayBuffer(encryptedMessage.iv);
126
+ const ciphertext = base64ToArrayBuffer(encryptedMessage.ciphertext);
127
+ const receivedMac = base64ToArrayBuffer(encryptedMessage.mac);
128
+
129
+ // Verify HMAC first
130
+ const dataToVerify = new Uint8Array([
131
+ ...new Uint8Array(iv),
132
+ ...new Uint8Array(ciphertext)
133
+ ]);
134
+
135
+ const isValidMac = await this.provider.verifyHMAC(this.encryptionKey, dataToVerify.buffer, receivedMac);
136
+ if (!isValidMac) {
137
+ throw new Error('Message authentication failed');
138
+ }
139
+
140
+ // Decrypt the message
141
+ const plaintextBuffer = await this.provider.decrypt(this.encryptionKey, ciphertext, iv);
142
+ const plaintextString = arrayBufferToString(plaintextBuffer);
143
+ return JSON.parse(plaintextString) as Record<string, unknown>;
144
+ }
145
+
146
+ /**
147
+ * Get session information
148
+ */
149
+ async getSessionKeys(): Promise<SessionKeys> {
150
+ if (!this.keyPair || !this.sharedSecret) {
151
+ throw new Error('Session not properly initialized');
152
+ }
153
+
154
+ return {
155
+ sessionId: this.sessionId,
156
+ publicKey: await this.getPublicKey(),
157
+ sharedSecret: this.sharedSecret, // We know it exists due to the check above
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Generate a random session ID
163
+ */
164
+ private generateSessionId(): string {
165
+ const sessionBytes = this.provider.randomBytes(16); // 128 bits
166
+ return arrayBufferToBase64(sessionBytes);
167
+ }
168
+
169
+ /**
170
+ * Get provider information
171
+ */
172
+ getProviderInfo(): { name: string; isAvailable: boolean } {
173
+ return {
174
+ name: this.provider.name,
175
+ isAvailable: this.provider.isAvailable,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Check if session is ready for encryption/decryption
181
+ */
182
+ get isReady(): boolean {
183
+ return !!(this.keyPair && this.sharedSecret && this.encryptionKey);
184
+ }
185
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Crypto Runtime Types
3
+ *
4
+ * Type definitions for crypto operations and configuration used by implementations.
5
+ * These types are separate from the provider interface and describe data structures
6
+ * used during encryption, decryption, and key management.
7
+ *
8
+ * IMPORTANT: This file contains ONLY type definitions with zero runtime code.
9
+ */
10
+
11
+ import type { CryptoKeyLike } from '../types/crypto-provider';
12
+
13
+ /**
14
+ * Supported encryption algorithms
15
+ * - AES-GCM: AES-256 in Galois/Counter Mode with authentication
16
+ * - plaintext: No encryption (development/testing only)
17
+ */
18
+ export type EncryptionAlgorithm = 'AES-GCM' | 'plaintext';
19
+
20
+ /**
21
+ * Encrypted message structure
22
+ * Contains all components needed to decrypt and verify a message
23
+ */
24
+ export interface EncryptedMessage {
25
+ /** Base64-encoded initialization vector (12 bytes for GCM) */
26
+ iv: string;
27
+ /** Base64-encoded ciphertext */
28
+ ciphertext: string;
29
+ /** Base64-encoded HMAC-SHA256 authentication code */
30
+ mac: string;
31
+ }
32
+
33
+ /**
34
+ * Asymmetric key pair structure
35
+ * Using CryptoKey for Web Crypto API compatibility
36
+ *
37
+ * Note: This is kept for backward compatibility with existing code.
38
+ * New code should use ProviderKeyPair from crypto-provider types.
39
+ */
40
+ export interface KeyPair {
41
+ publicKey: CryptoKey;
42
+ privateKey: CryptoKey;
43
+ }
44
+
45
+ /**
46
+ * Session-specific encryption keys
47
+ * Manages shared secrets and public keys for a session
48
+ */
49
+ export interface SessionKeys {
50
+ /** Shared secret derived from ECDH key exchange */
51
+ sharedSecret: CryptoKey | CryptoKeyLike;
52
+ /** Unique session identifier */
53
+ sessionId: string;
54
+ /** Base64-encoded public key for sharing with remote party */
55
+ publicKey: string;
56
+ }
57
+
58
+ /**
59
+ * Cryptographic algorithm configuration
60
+ * Defines the parameters for BananaLink's crypto implementation
61
+ */
62
+ export interface CryptoConfig {
63
+ /** Encryption algorithm (always AES-GCM for production) */
64
+ algorithm: 'AES-GCM';
65
+ /** AES key length in bits */
66
+ keyLength: 256;
67
+ /** Initialization vector length in bytes (12 bytes for GCM) */
68
+ ivLength: 12;
69
+ /** Authentication tag length in bits (128 bits = 16 bytes) */
70
+ tagLength: 128;
71
+ /** Elliptic curve for ECDH key exchange */
72
+ curve: 'P-256';
73
+ }
74
+
75
+ /**
76
+ * Options for creating a crypto payload
77
+ * Supports both encrypted and plaintext modes
78
+ */
79
+ export type CreatePayloadOptions =
80
+ | {
81
+ /** Plaintext mode (no encryption) */
82
+ encAlgo: 'plaintext';
83
+ /** Session identifier */
84
+ sessionId: string;
85
+ }
86
+ | {
87
+ /** Encrypted mode with AES-GCM */
88
+ encAlgo: 'AES-GCM';
89
+ /** Session identifier */
90
+ sessionId: string;
91
+ /** Base64-encoded remote public key for ECDH */
92
+ publicKeyB64: string;
93
+ };