@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.
- package/README.md +604 -0
- package/dist/chunk-32OWUOZ3.js +308 -0
- package/dist/chunk-32OWUOZ3.js.map +1 -0
- package/dist/chunk-65HNHRJK.cjs +123 -0
- package/dist/chunk-65HNHRJK.cjs.map +1 -0
- package/dist/chunk-7KYDLL3B.js +480 -0
- package/dist/chunk-7KYDLL3B.js.map +1 -0
- package/dist/chunk-A6FLEJ7R.cjs +62 -0
- package/dist/chunk-A6FLEJ7R.cjs.map +1 -0
- package/dist/chunk-CUJK7ZTS.js +217 -0
- package/dist/chunk-CUJK7ZTS.js.map +1 -0
- package/dist/chunk-GI3BUPIH.cjs +236 -0
- package/dist/chunk-GI3BUPIH.cjs.map +1 -0
- package/dist/chunk-JXHV66Q4.js +106 -0
- package/dist/chunk-JXHV66Q4.js.map +1 -0
- package/dist/chunk-KNGZKGRS.cjs +552 -0
- package/dist/chunk-KNGZKGRS.cjs.map +1 -0
- package/dist/chunk-LELPCIE7.js +840 -0
- package/dist/chunk-LELPCIE7.js.map +1 -0
- package/dist/chunk-MCZG7QEM.cjs +310 -0
- package/dist/chunk-MCZG7QEM.cjs.map +1 -0
- package/dist/chunk-TCVKC227.js +56 -0
- package/dist/chunk-TCVKC227.js.map +1 -0
- package/dist/chunk-VXLUSU5B.cjs +856 -0
- package/dist/chunk-VXLUSU5B.cjs.map +1 -0
- package/dist/chunk-WCQVDF3K.js +12 -0
- package/dist/chunk-WCQVDF3K.js.map +1 -0
- package/dist/chunk-WGEGR3DF.cjs +15 -0
- package/dist/chunk-WGEGR3DF.cjs.map +1 -0
- package/dist/client-session-claim-3QF3noOr.d.ts +197 -0
- package/dist/client-session-claim-C4lUik3b.d.cts +197 -0
- package/dist/core-DMhuNfoz.d.cts +62 -0
- package/dist/core-DMhuNfoz.d.ts +62 -0
- package/dist/crypto/providers/noble-provider.cjs +14 -0
- package/dist/crypto/providers/noble-provider.cjs.map +1 -0
- package/dist/crypto/providers/noble-provider.d.cts +30 -0
- package/dist/crypto/providers/noble-provider.d.ts +30 -0
- package/dist/crypto/providers/noble-provider.js +5 -0
- package/dist/crypto/providers/noble-provider.js.map +1 -0
- package/dist/crypto/providers/node-provider.cjs +308 -0
- package/dist/crypto/providers/node-provider.cjs.map +1 -0
- package/dist/crypto/providers/node-provider.d.cts +32 -0
- package/dist/crypto/providers/node-provider.d.ts +32 -0
- package/dist/crypto/providers/node-provider.js +306 -0
- package/dist/crypto/providers/node-provider.js.map +1 -0
- package/dist/crypto/providers/quickcrypto-provider.cjs +339 -0
- package/dist/crypto/providers/quickcrypto-provider.cjs.map +1 -0
- package/dist/crypto/providers/quickcrypto-provider.d.cts +34 -0
- package/dist/crypto/providers/quickcrypto-provider.d.ts +34 -0
- package/dist/crypto/providers/quickcrypto-provider.js +337 -0
- package/dist/crypto/providers/quickcrypto-provider.js.map +1 -0
- package/dist/crypto/providers/webcrypto-provider.cjs +310 -0
- package/dist/crypto/providers/webcrypto-provider.cjs.map +1 -0
- package/dist/crypto/providers/webcrypto-provider.d.cts +30 -0
- package/dist/crypto/providers/webcrypto-provider.d.ts +30 -0
- package/dist/crypto/providers/webcrypto-provider.js +308 -0
- package/dist/crypto/providers/webcrypto-provider.js.map +1 -0
- package/dist/crypto-BUS06Qz-.d.cts +40 -0
- package/dist/crypto-BUS06Qz-.d.ts +40 -0
- package/dist/crypto-export.cjs +790 -0
- package/dist/crypto-export.cjs.map +1 -0
- package/dist/crypto-export.d.cts +257 -0
- package/dist/crypto-export.d.ts +257 -0
- package/dist/crypto-export.js +709 -0
- package/dist/crypto-export.js.map +1 -0
- package/dist/crypto-provider-deYoVIxi.d.cts +36 -0
- package/dist/crypto-provider-deYoVIxi.d.ts +36 -0
- package/dist/index.cjs +615 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +379 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +504 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas-export.cjs +294 -0
- package/dist/schemas-export.cjs.map +1 -0
- package/dist/schemas-export.d.cts +1598 -0
- package/dist/schemas-export.d.ts +1598 -0
- package/dist/schemas-export.js +5 -0
- package/dist/schemas-export.js.map +1 -0
- package/dist/siwe-export.cjs +237 -0
- package/dist/siwe-export.cjs.map +1 -0
- package/dist/siwe-export.d.cts +27 -0
- package/dist/siwe-export.d.ts +27 -0
- package/dist/siwe-export.js +228 -0
- package/dist/siwe-export.js.map +1 -0
- package/dist/testing.cjs +54 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +20 -0
- package/dist/testing.d.ts +20 -0
- package/dist/testing.js +51 -0
- package/dist/testing.js.map +1 -0
- package/dist/validation-export.cjs +359 -0
- package/dist/validation-export.cjs.map +1 -0
- package/dist/validation-export.d.cts +3 -0
- package/dist/validation-export.d.ts +3 -0
- package/dist/validation-export.js +6 -0
- package/dist/validation-export.js.map +1 -0
- package/dist/validators-export.cjs +73 -0
- package/dist/validators-export.cjs.map +1 -0
- package/dist/validators-export.d.cts +37 -0
- package/dist/validators-export.d.ts +37 -0
- package/dist/validators-export.js +4 -0
- package/dist/validators-export.js.map +1 -0
- package/package.json +140 -0
- package/src/constants/index.ts +205 -0
- package/src/crypto/context.ts +228 -0
- package/src/crypto/diagnostics.ts +772 -0
- package/src/crypto/errors.ts +114 -0
- package/src/crypto/index.ts +89 -0
- package/src/crypto/payload-handler.ts +102 -0
- package/src/crypto/providers/compliance-provider.ts +579 -0
- package/src/crypto/providers/factory.ts +204 -0
- package/src/crypto/providers/index.ts +44 -0
- package/src/crypto/providers/noble-provider.ts +392 -0
- package/src/crypto/providers/node-provider.ts +433 -0
- package/src/crypto/providers/quickcrypto-provider.ts +483 -0
- package/src/crypto/providers/registry.ts +129 -0
- package/src/crypto/providers/webcrypto-provider.ts +364 -0
- package/src/crypto/session-security.ts +185 -0
- package/src/crypto/types.ts +93 -0
- package/src/crypto/utils.ts +190 -0
- package/src/crypto-export.ts +21 -0
- package/src/index.ts +38 -0
- package/src/schemas/auth.ts +60 -0
- package/src/schemas/client-messages.ts +57 -0
- package/src/schemas/core.ts +144 -0
- package/src/schemas/crypto.ts +65 -0
- package/src/schemas/discovery.ts +79 -0
- package/src/schemas/index.ts +239 -0
- package/src/schemas/relay-messages.ts +45 -0
- package/src/schemas/wallet-messages.ts +177 -0
- package/src/schemas-export.ts +23 -0
- package/src/siwe-export.ts +27 -0
- package/src/testing.ts +71 -0
- package/src/types/auth.ts +60 -0
- package/src/types/client-messages.ts +84 -0
- package/src/types/core.ts +131 -0
- package/src/types/crypto-provider.ts +264 -0
- package/src/types/crypto.ts +90 -0
- package/src/types/discovery.ts +50 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +197 -0
- package/src/types/post-auth-operations.ts +363 -0
- package/src/types/providers.ts +72 -0
- package/src/types/relay-messages.ts +60 -0
- package/src/types/request-lifecycle.ts +161 -0
- package/src/types/signing-operations.ts +99 -0
- package/src/types/wallet-messages.ts +251 -0
- package/src/utils/client-session-claim.ts +188 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/public-keys.ts +49 -0
- package/src/utils/siwe.ts +362 -0
- package/src/utils/url-decoding.ts +126 -0
- package/src/utils/url-encoding.ts +144 -0
- package/src/utils/wallet-session-claim.ts +188 -0
- package/src/validation-export.ts +32 -0
- package/src/validators/index.ts +222 -0
- 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
|
+
};
|