holosphere 2.0.0-alpha13 → 2.0.0-alpha15
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/dist/2019-ATLjawsU.cjs +8 -0
- package/dist/{2019-Cp3uYhyY.cjs.map → 2019-ATLjawsU.cjs.map} +1 -1
- package/dist/{2019-CLMqIAfQ.js → 2019-BfjzDRje.js} +1667 -1721
- package/dist/{2019-CLMqIAfQ.js.map → 2019-BfjzDRje.js.map} +1 -1
- package/dist/{browser-nUQt1cnB.js → browser-CKTczilW.js} +2 -2
- package/dist/{browser-nUQt1cnB.js.map → browser-CKTczilW.js.map} +1 -1
- package/dist/{browser-D6cNVl0v.cjs → browser-GOg6KKOV.cjs} +2 -2
- package/dist/{browser-D6cNVl0v.cjs.map → browser-GOg6KKOV.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +25 -21
- package/dist/{index-CoAjtqsD.js → index-BdnrGafX.js} +2 -2
- package/dist/{index-CoAjtqsD.js.map → index-BdnrGafX.js.map} +1 -1
- package/dist/{index-BN_uoxQK.js → index-C3Cag0SV.js} +2558 -495
- package/dist/index-C3Cag0SV.js.map +1 -0
- package/dist/index-ChpSfdYS.cjs +29 -0
- package/dist/index-ChpSfdYS.cjs.map +1 -0
- package/dist/{index-DJjGSwXG.cjs → index-D_QecZNu.cjs} +2 -2
- package/dist/{index-DJjGSwXG.cjs.map → index-D_QecZNu.cjs.map} +1 -1
- package/dist/{index-Z5TstN1e.js → index-nMC3dWZ5.js} +2 -2
- package/dist/{index-Z5TstN1e.js.map → index-nMC3dWZ5.js.map} +1 -1
- package/dist/{index-Cp3tI53z.cjs → index-z5HWfWMu.cjs} +2 -2
- package/dist/{index-Cp3tI53z.cjs.map → index-z5HWfWMu.cjs.map} +1 -1
- package/dist/{indexeddb-storage-CZK5A7XH.cjs → indexeddb-storage-DWSeL-YF.cjs} +2 -2
- package/dist/{indexeddb-storage-CZK5A7XH.cjs.map → indexeddb-storage-DWSeL-YF.cjs.map} +1 -1
- package/dist/{indexeddb-storage-bpA01pAU.js → indexeddb-storage-dx01N0ET.js} +2 -2
- package/dist/{indexeddb-storage-bpA01pAU.js.map → indexeddb-storage-dx01N0ET.js.map} +1 -1
- package/dist/{memory-storage-BqhmytP_.js → memory-storage-BPIfkpcf.js} +2 -2
- package/dist/{memory-storage-BqhmytP_.js.map → memory-storage-BPIfkpcf.js.map} +1 -1
- package/dist/{memory-storage-B1k8Jszd.cjs → memory-storage-CKUGDq2d.cjs} +2 -2
- package/dist/{memory-storage-B1k8Jszd.cjs.map → memory-storage-CKUGDq2d.cjs.map} +1 -1
- package/package.json +3 -1
- package/scripts/test-ndk-direct.js +104 -0
- package/src/crypto/key-store.js +356 -0
- package/src/crypto/lens-keys.js +205 -0
- package/src/crypto/secp256k1.js +181 -18
- package/src/federation/handshake.js +317 -23
- package/src/federation/hologram.js +25 -17
- package/src/federation/registry.js +779 -59
- package/src/index.js +416 -27
- package/src/lib/federation-methods.js +308 -4
- package/src/storage/nostr-async.js +144 -86
- package/src/storage/nostr-client.js +77 -18
- package/src/storage/nostr-wrapper.js +4 -1
- package/src/storage/unified-storage.js +5 -4
- package/src/subscriptions/manager.js +1 -1
- package/vitest.config.js +6 -1
- package/dist/2019-Cp3uYhyY.cjs +0 -8
- package/dist/index-BN_uoxQK.js.map +0 -1
- package/dist/index-V8EHMYEY.cjs +0 -29
- package/dist/index-V8EHMYEY.cjs.map +0 -1
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Lens Key Store for Encrypted Federation
|
|
3
|
+
*
|
|
4
|
+
* Manages symmetric keys for encrypted lenses. Each HoloSphere instance
|
|
5
|
+
* has its own key store that tracks:
|
|
6
|
+
* - Keys we own (generated for our own lenses)
|
|
7
|
+
* - Keys we received (shared by federation partners)
|
|
8
|
+
*
|
|
9
|
+
* Keys are stored in memory and can be persisted to Nostr for recovery.
|
|
10
|
+
*
|
|
11
|
+
* @module crypto/key-store
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
generateLensKey,
|
|
16
|
+
wrapKeyForRecipient,
|
|
17
|
+
unwrapKey,
|
|
18
|
+
serializeLensKey,
|
|
19
|
+
deserializeLensKey
|
|
20
|
+
} from './lens-keys.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* In-memory key store for lens symmetric keys.
|
|
24
|
+
* Each HoloSphere instance should have its own LensKeyStore.
|
|
25
|
+
*
|
|
26
|
+
* @class LensKeyStore
|
|
27
|
+
*/
|
|
28
|
+
export class LensKeyStore {
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new LensKeyStore instance.
|
|
31
|
+
*/
|
|
32
|
+
constructor() {
|
|
33
|
+
/**
|
|
34
|
+
* Keys we own (generated for our lenses).
|
|
35
|
+
* Map: lensPath -> { key: Uint8Array, wrappedForSelf: string, createdAt: number }
|
|
36
|
+
* @type {Map<string, Object>}
|
|
37
|
+
*/
|
|
38
|
+
this.ownKeys = new Map();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Keys we received from federation partners.
|
|
42
|
+
* Map: lensPath -> { key: Uint8Array, fromPubKey: string, receivedAt: number }
|
|
43
|
+
* @type {Map<string, Object>}
|
|
44
|
+
*/
|
|
45
|
+
this.receivedKeys = new Map();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Wrapped keys for partners (for sharing during federation).
|
|
49
|
+
* Map: lensPath -> Map<partnerPubKey, wrappedKey>
|
|
50
|
+
* @type {Map<string, Map<string, string>>}
|
|
51
|
+
*/
|
|
52
|
+
this.partnerWrappedKeys = new Map();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Store a lens key we own (we generated it).
|
|
57
|
+
*
|
|
58
|
+
* @param {string} lensPath - Path identifying the lens (appName/holonId/lensName)
|
|
59
|
+
* @param {Uint8Array} key - 32-byte symmetric key
|
|
60
|
+
* @param {string} wrappedForSelf - NIP-44 wrapped key for self-recovery
|
|
61
|
+
*/
|
|
62
|
+
storeOwnKey(lensPath, key, wrappedForSelf) {
|
|
63
|
+
this.ownKeys.set(lensPath, {
|
|
64
|
+
key,
|
|
65
|
+
wrappedForSelf,
|
|
66
|
+
createdAt: Date.now()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Store a lens key we received from a federation partner.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} lensPath - Path identifying the lens
|
|
74
|
+
* @param {Uint8Array} key - 32-byte symmetric key
|
|
75
|
+
* @param {string} fromPubKey - Public key of the partner who shared the key
|
|
76
|
+
*/
|
|
77
|
+
storeReceivedKey(lensPath, key, fromPubKey) {
|
|
78
|
+
this.receivedKeys.set(lensPath, {
|
|
79
|
+
key,
|
|
80
|
+
fromPubKey,
|
|
81
|
+
receivedAt: Date.now()
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the key for a lens (own or received).
|
|
87
|
+
* Own keys take precedence over received keys.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} lensPath - Path identifying the lens
|
|
90
|
+
* @returns {Uint8Array|null} 32-byte symmetric key or null if not found
|
|
91
|
+
*/
|
|
92
|
+
getKey(lensPath) {
|
|
93
|
+
const own = this.ownKeys.get(lensPath);
|
|
94
|
+
if (own) return own.key;
|
|
95
|
+
|
|
96
|
+
const received = this.receivedKeys.get(lensPath);
|
|
97
|
+
if (received) return received.key;
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Check if we have a key for a lens.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} lensPath - Path identifying the lens
|
|
106
|
+
* @returns {boolean} True if key exists
|
|
107
|
+
*/
|
|
108
|
+
hasKey(lensPath) {
|
|
109
|
+
return this.ownKeys.has(lensPath) || this.receivedKeys.has(lensPath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if we own the key for a lens (vs received it).
|
|
114
|
+
*
|
|
115
|
+
* @param {string} lensPath - Path identifying the lens
|
|
116
|
+
* @returns {boolean} True if we own the key
|
|
117
|
+
*/
|
|
118
|
+
isOwnKey(lensPath) {
|
|
119
|
+
return this.ownKeys.has(lensPath);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get or create a key for a lens.
|
|
124
|
+
* If key doesn't exist, generates a new one.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} lensPath - Path identifying the lens
|
|
127
|
+
* @param {string} ownerPrivKey - Owner's private key for wrapping
|
|
128
|
+
* @param {string} ownerPubKey - Owner's public key
|
|
129
|
+
* @returns {Object} { key: Uint8Array, isNew: boolean, wrappedForSelf: string }
|
|
130
|
+
*/
|
|
131
|
+
getOrCreateKey(lensPath, ownerPrivKey, ownerPubKey) {
|
|
132
|
+
// Check if key already exists
|
|
133
|
+
const existing = this.ownKeys.get(lensPath);
|
|
134
|
+
if (existing) {
|
|
135
|
+
return {
|
|
136
|
+
key: existing.key,
|
|
137
|
+
isNew: false,
|
|
138
|
+
wrappedForSelf: existing.wrappedForSelf
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Also check received keys (shouldn't generate if we received one)
|
|
143
|
+
if (this.receivedKeys.has(lensPath)) {
|
|
144
|
+
const received = this.receivedKeys.get(lensPath);
|
|
145
|
+
return {
|
|
146
|
+
key: received.key,
|
|
147
|
+
isNew: false,
|
|
148
|
+
wrappedForSelf: null // We don't have wrapped version for received keys
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Generate new key
|
|
153
|
+
const key = generateLensKey();
|
|
154
|
+
const wrappedForSelf = wrapKeyForRecipient(key, ownerPrivKey, ownerPubKey);
|
|
155
|
+
|
|
156
|
+
// Store it
|
|
157
|
+
this.storeOwnKey(lensPath, key, wrappedForSelf);
|
|
158
|
+
|
|
159
|
+
return { key, isNew: true, wrappedForSelf };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Wrap a lens key for a partner (for federation sharing).
|
|
164
|
+
*
|
|
165
|
+
* @param {string} lensPath - Path identifying the lens
|
|
166
|
+
* @param {string} ownerPrivKey - Owner's private key
|
|
167
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
168
|
+
* @returns {string|null} Wrapped key for partner or null if no key exists
|
|
169
|
+
*/
|
|
170
|
+
wrapKeyForPartner(lensPath, ownerPrivKey, partnerPubKey) {
|
|
171
|
+
const key = this.getKey(lensPath);
|
|
172
|
+
if (!key) return null;
|
|
173
|
+
|
|
174
|
+
const wrappedKey = wrapKeyForRecipient(key, ownerPrivKey, partnerPubKey);
|
|
175
|
+
|
|
176
|
+
// Cache the wrapped key for this partner
|
|
177
|
+
if (!this.partnerWrappedKeys.has(lensPath)) {
|
|
178
|
+
this.partnerWrappedKeys.set(lensPath, new Map());
|
|
179
|
+
}
|
|
180
|
+
this.partnerWrappedKeys.get(lensPath).set(partnerPubKey, wrappedKey);
|
|
181
|
+
|
|
182
|
+
return wrappedKey;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get all partners who have been given wrapped keys for a lens.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} lensPath - Path identifying the lens
|
|
189
|
+
* @returns {string[]} Array of partner public keys
|
|
190
|
+
*/
|
|
191
|
+
getPartnersWithAccess(lensPath) {
|
|
192
|
+
const partners = this.partnerWrappedKeys.get(lensPath);
|
|
193
|
+
return partners ? Array.from(partners.keys()) : [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Revoke a partner's access by removing their wrapped key.
|
|
198
|
+
* Note: This doesn't prevent them from using their cached key.
|
|
199
|
+
* For true revocation, you need to re-key the lens.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} lensPath - Path identifying the lens
|
|
202
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
203
|
+
* @returns {boolean} True if partner was found and removed
|
|
204
|
+
*/
|
|
205
|
+
revokePartnerAccess(lensPath, partnerPubKey) {
|
|
206
|
+
const partners = this.partnerWrappedKeys.get(lensPath);
|
|
207
|
+
if (partners) {
|
|
208
|
+
return partners.delete(partnerPubKey);
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Re-key a lens (generate new key and re-wrap for remaining partners).
|
|
215
|
+
* Used for key rotation or access revocation.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} lensPath - Path identifying the lens
|
|
218
|
+
* @param {string} ownerPrivKey - Owner's private key
|
|
219
|
+
* @param {string} ownerPubKey - Owner's public key
|
|
220
|
+
* @param {string[]} [excludePartners=[]] - Partners to exclude from re-wrapping
|
|
221
|
+
* @returns {Object} { newKey, wrappedForSelf, partnerKeys: Map<pubKey, wrappedKey> }
|
|
222
|
+
*/
|
|
223
|
+
rekeyLens(lensPath, ownerPrivKey, ownerPubKey, excludePartners = []) {
|
|
224
|
+
// Get current partners
|
|
225
|
+
const currentPartners = this.getPartnersWithAccess(lensPath);
|
|
226
|
+
const remainingPartners = currentPartners.filter(p => !excludePartners.includes(p));
|
|
227
|
+
|
|
228
|
+
// Generate new key
|
|
229
|
+
const newKey = generateLensKey();
|
|
230
|
+
const wrappedForSelf = wrapKeyForRecipient(newKey, ownerPrivKey, ownerPubKey);
|
|
231
|
+
|
|
232
|
+
// Store new key
|
|
233
|
+
this.storeOwnKey(lensPath, newKey, wrappedForSelf);
|
|
234
|
+
|
|
235
|
+
// Re-wrap for remaining partners
|
|
236
|
+
const partnerKeys = new Map();
|
|
237
|
+
this.partnerWrappedKeys.set(lensPath, new Map());
|
|
238
|
+
|
|
239
|
+
for (const partnerPubKey of remainingPartners) {
|
|
240
|
+
const wrappedKey = wrapKeyForRecipient(newKey, ownerPrivKey, partnerPubKey);
|
|
241
|
+
this.partnerWrappedKeys.get(lensPath).set(partnerPubKey, wrappedKey);
|
|
242
|
+
partnerKeys.set(partnerPubKey, wrappedKey);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { newKey, wrappedForSelf, partnerKeys };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Receive and unwrap a key from a partner.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} lensPath - Path identifying the lens
|
|
252
|
+
* @param {string} wrappedKey - NIP-44 wrapped key from partner
|
|
253
|
+
* @param {string} recipientPrivKey - Our private key
|
|
254
|
+
* @param {string} senderPubKey - Partner's public key
|
|
255
|
+
* @returns {Uint8Array} Unwrapped 32-byte symmetric key
|
|
256
|
+
*/
|
|
257
|
+
receiveKey(lensPath, wrappedKey, recipientPrivKey, senderPubKey) {
|
|
258
|
+
const key = unwrapKey(wrappedKey, recipientPrivKey, senderPubKey);
|
|
259
|
+
this.storeReceivedKey(lensPath, key, senderPubKey);
|
|
260
|
+
return key;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Export own keys for persistent storage.
|
|
265
|
+
* Returns serializable format that can be stored in Nostr.
|
|
266
|
+
*
|
|
267
|
+
* @returns {Object[]} Array of { lensPath, wrappedForSelf, createdAt }
|
|
268
|
+
*/
|
|
269
|
+
exportOwnKeys() {
|
|
270
|
+
const exported = [];
|
|
271
|
+
for (const [lensPath, data] of this.ownKeys.entries()) {
|
|
272
|
+
exported.push({
|
|
273
|
+
lensPath,
|
|
274
|
+
wrappedForSelf: data.wrappedForSelf,
|
|
275
|
+
createdAt: data.createdAt
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return exported;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Import own keys from persistent storage.
|
|
283
|
+
*
|
|
284
|
+
* @param {Object[]} keyData - Array of exported key data
|
|
285
|
+
* @param {string} ownerPrivKey - Owner's private key for unwrapping
|
|
286
|
+
* @param {string} ownerPubKey - Owner's public key
|
|
287
|
+
*/
|
|
288
|
+
importOwnKeys(keyData, ownerPrivKey, ownerPubKey) {
|
|
289
|
+
for (const data of keyData) {
|
|
290
|
+
const key = unwrapKey(data.wrappedForSelf, ownerPrivKey, ownerPubKey);
|
|
291
|
+
this.ownKeys.set(data.lensPath, {
|
|
292
|
+
key,
|
|
293
|
+
wrappedForSelf: data.wrappedForSelf,
|
|
294
|
+
createdAt: data.createdAt
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Clear all keys from the store.
|
|
301
|
+
* Use with caution - this will remove all access to encrypted data.
|
|
302
|
+
*/
|
|
303
|
+
clear() {
|
|
304
|
+
this.ownKeys.clear();
|
|
305
|
+
this.receivedKeys.clear();
|
|
306
|
+
this.partnerWrappedKeys.clear();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Get statistics about the key store.
|
|
311
|
+
*
|
|
312
|
+
* @returns {Object} { ownKeyCount, receivedKeyCount, totalPartnerGrants }
|
|
313
|
+
*/
|
|
314
|
+
getStats() {
|
|
315
|
+
let totalPartnerGrants = 0;
|
|
316
|
+
for (const partners of this.partnerWrappedKeys.values()) {
|
|
317
|
+
totalPartnerGrants += partners.size;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
ownKeyCount: this.ownKeys.size,
|
|
322
|
+
receivedKeyCount: this.receivedKeys.size,
|
|
323
|
+
totalPartnerGrants
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Build a lens path from components.
|
|
330
|
+
*
|
|
331
|
+
* @param {string} appName - Application namespace
|
|
332
|
+
* @param {string} holonId - Holon identifier
|
|
333
|
+
* @param {string} lensName - Lens name
|
|
334
|
+
* @returns {string} Lens path (appName/holonId/lensName)
|
|
335
|
+
*/
|
|
336
|
+
export function buildLensPath(appName, holonId, lensName) {
|
|
337
|
+
return `${appName}/${holonId}/${lensName}`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Parse a lens path into components.
|
|
342
|
+
*
|
|
343
|
+
* @param {string} lensPath - Lens path to parse
|
|
344
|
+
* @returns {Object} { appName, holonId, lensName }
|
|
345
|
+
*/
|
|
346
|
+
export function parseLensPath(lensPath) {
|
|
347
|
+
const parts = lensPath.split('/');
|
|
348
|
+
if (parts.length < 3) {
|
|
349
|
+
throw new Error(`Invalid lens path: ${lensPath}`);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
appName: parts[0],
|
|
353
|
+
holonId: parts[1],
|
|
354
|
+
lensName: parts[2]
|
|
355
|
+
};
|
|
356
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Symmetric Key Management for Encrypted Federation
|
|
3
|
+
*
|
|
4
|
+
* Provides symmetric key generation, data encryption/decryption using AES-256-GCM,
|
|
5
|
+
* and key wrapping/unwrapping using NIP-44 for secure key exchange.
|
|
6
|
+
*
|
|
7
|
+
* Key Principles:
|
|
8
|
+
* - Each lens has its own symmetric key (256-bit AES key)
|
|
9
|
+
* - Data is encrypted once with the symmetric key (single source of truth)
|
|
10
|
+
* - Key = Access: Having the symmetric key means you can read the data
|
|
11
|
+
* - Key exchange happens during federation approval via NIP-44 encrypted DMs
|
|
12
|
+
*
|
|
13
|
+
* Browser-compatible: Uses @noble/ciphers for AES-GCM encryption.
|
|
14
|
+
*
|
|
15
|
+
* @module crypto/lens-keys
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { gcm } from '@noble/ciphers/aes';
|
|
19
|
+
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
20
|
+
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
21
|
+
import { encryptNIP44, decryptNIP44 } from './nostr-utils.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a 256-bit symmetric key for a lens.
|
|
25
|
+
* Uses cryptographically secure random bytes.
|
|
26
|
+
*
|
|
27
|
+
* @returns {Uint8Array} 32-byte (256-bit) symmetric key
|
|
28
|
+
*/
|
|
29
|
+
export function generateLensKey() {
|
|
30
|
+
return randomBytes(32);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Convert Uint8Array to base64 string.
|
|
35
|
+
* @param {Uint8Array} bytes
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
function uint8ToBase64(bytes) {
|
|
39
|
+
// Browser-compatible base64 encoding
|
|
40
|
+
if (typeof btoa === 'function') {
|
|
41
|
+
return btoa(String.fromCharCode(...bytes));
|
|
42
|
+
}
|
|
43
|
+
// Node.js fallback
|
|
44
|
+
return Buffer.from(bytes).toString('base64');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert base64 string to Uint8Array.
|
|
49
|
+
* @param {string} base64
|
|
50
|
+
* @returns {Uint8Array}
|
|
51
|
+
*/
|
|
52
|
+
function base64ToUint8(base64) {
|
|
53
|
+
// Browser-compatible base64 decoding
|
|
54
|
+
if (typeof atob === 'function') {
|
|
55
|
+
const binary = atob(base64);
|
|
56
|
+
const bytes = new Uint8Array(binary.length);
|
|
57
|
+
for (let i = 0; i < binary.length; i++) {
|
|
58
|
+
bytes[i] = binary.charCodeAt(i);
|
|
59
|
+
}
|
|
60
|
+
return bytes;
|
|
61
|
+
}
|
|
62
|
+
// Node.js fallback
|
|
63
|
+
return new Uint8Array(Buffer.from(base64, 'base64'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Encrypt data with symmetric key using AES-256-GCM.
|
|
68
|
+
* GCM provides both confidentiality and authenticity.
|
|
69
|
+
*
|
|
70
|
+
* Format: base64(iv[12] + ciphertext_with_tag)
|
|
71
|
+
*
|
|
72
|
+
* @param {Uint8Array} key - 32-byte symmetric key
|
|
73
|
+
* @param {string} plaintext - Data to encrypt (will be UTF-8 encoded)
|
|
74
|
+
* @returns {string} Base64-encoded encrypted data (iv + ciphertext + authTag)
|
|
75
|
+
*/
|
|
76
|
+
export function encryptWithKey(key, plaintext) {
|
|
77
|
+
// Generate random 12-byte IV (recommended size for GCM)
|
|
78
|
+
const iv = randomBytes(12);
|
|
79
|
+
|
|
80
|
+
// Create cipher and encrypt (GCM tag is appended automatically)
|
|
81
|
+
const aes = gcm(key, iv);
|
|
82
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
83
|
+
const ciphertext = aes.encrypt(plaintextBytes);
|
|
84
|
+
|
|
85
|
+
// Concatenate: iv (12) + ciphertext (includes 16-byte auth tag at end)
|
|
86
|
+
const combined = new Uint8Array(iv.length + ciphertext.length);
|
|
87
|
+
combined.set(iv, 0);
|
|
88
|
+
combined.set(ciphertext, iv.length);
|
|
89
|
+
|
|
90
|
+
return uint8ToBase64(combined);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Decrypt data with symmetric key using AES-256-GCM.
|
|
95
|
+
*
|
|
96
|
+
* @param {Uint8Array} key - 32-byte symmetric key
|
|
97
|
+
* @param {string} ciphertext - Base64-encoded encrypted data
|
|
98
|
+
* @returns {string} Decrypted plaintext (UTF-8)
|
|
99
|
+
* @throws {Error} If decryption fails (wrong key or tampered data)
|
|
100
|
+
*/
|
|
101
|
+
export function decryptWithKey(key, ciphertext) {
|
|
102
|
+
// Decode base64
|
|
103
|
+
const data = base64ToUint8(ciphertext);
|
|
104
|
+
|
|
105
|
+
// Extract components
|
|
106
|
+
const iv = data.slice(0, 12);
|
|
107
|
+
const encrypted = data.slice(12); // ciphertext + authTag
|
|
108
|
+
|
|
109
|
+
// Create cipher and decrypt
|
|
110
|
+
const aes = gcm(key, iv);
|
|
111
|
+
const decrypted = aes.decrypt(encrypted);
|
|
112
|
+
|
|
113
|
+
return new TextDecoder().decode(decrypted);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Wrap a symmetric key for a recipient using NIP-44 encryption.
|
|
118
|
+
* This allows secure key exchange via Nostr DMs.
|
|
119
|
+
*
|
|
120
|
+
* @param {Uint8Array} lensKey - 32-byte symmetric key to wrap
|
|
121
|
+
* @param {string} senderPrivKey - Sender's hex private key
|
|
122
|
+
* @param {string} recipientPubKey - Recipient's hex public key
|
|
123
|
+
* @returns {string} NIP-44 encrypted wrapped key
|
|
124
|
+
*/
|
|
125
|
+
export function wrapKeyForRecipient(lensKey, senderPrivKey, recipientPubKey) {
|
|
126
|
+
// Convert key to base64 for NIP-44 encryption
|
|
127
|
+
const keyBase64 = uint8ToBase64(lensKey);
|
|
128
|
+
return encryptNIP44(senderPrivKey, recipientPubKey, keyBase64);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Unwrap a symmetric key received from a sender using NIP-44 decryption.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} wrappedKey - NIP-44 encrypted wrapped key
|
|
135
|
+
* @param {string} recipientPrivKey - Recipient's hex private key
|
|
136
|
+
* @param {string} senderPubKey - Sender's hex public key
|
|
137
|
+
* @returns {Uint8Array} 32-byte symmetric key
|
|
138
|
+
*/
|
|
139
|
+
export function unwrapKey(wrappedKey, recipientPrivKey, senderPubKey) {
|
|
140
|
+
const keyBase64 = decryptNIP44(recipientPrivKey, senderPubKey, wrappedKey);
|
|
141
|
+
return base64ToUint8(keyBase64);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Serialize a lens key for persistent storage.
|
|
146
|
+
* The key is wrapped for the owner using NIP-44.
|
|
147
|
+
*
|
|
148
|
+
* @param {Uint8Array} lensKey - 32-byte symmetric key
|
|
149
|
+
* @param {string} ownerPrivKey - Owner's hex private key
|
|
150
|
+
* @param {string} ownerPubKey - Owner's hex public key
|
|
151
|
+
* @returns {Object} Serializable key data { wrappedKey, algorithm, version }
|
|
152
|
+
*/
|
|
153
|
+
export function serializeLensKey(lensKey, ownerPrivKey, ownerPubKey) {
|
|
154
|
+
return {
|
|
155
|
+
wrappedKey: wrapKeyForRecipient(lensKey, ownerPrivKey, ownerPubKey),
|
|
156
|
+
algorithm: 'aes-256-gcm',
|
|
157
|
+
keyWrap: 'nip44',
|
|
158
|
+
version: '1.0'
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Deserialize a lens key from persistent storage.
|
|
164
|
+
*
|
|
165
|
+
* @param {Object} keyData - Serialized key data
|
|
166
|
+
* @param {string} ownerPrivKey - Owner's hex private key
|
|
167
|
+
* @param {string} ownerPubKey - Owner's hex public key
|
|
168
|
+
* @returns {Uint8Array} 32-byte symmetric key
|
|
169
|
+
*/
|
|
170
|
+
export function deserializeLensKey(keyData, ownerPrivKey, ownerPubKey) {
|
|
171
|
+
if (keyData.version !== '1.0') {
|
|
172
|
+
throw new Error(`Unsupported lens key version: ${keyData.version}`);
|
|
173
|
+
}
|
|
174
|
+
return unwrapKey(keyData.wrappedKey, ownerPrivKey, ownerPubKey);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if content is encrypted (base64-encoded AES-GCM format).
|
|
179
|
+
* Encrypted content starts with a valid base64 string of at least 28 bytes
|
|
180
|
+
* (12 IV + 16 authTag) and is not valid JSON.
|
|
181
|
+
*
|
|
182
|
+
* @param {string} content - Content string to check
|
|
183
|
+
* @returns {boolean} True if content appears to be encrypted
|
|
184
|
+
*/
|
|
185
|
+
export function isEncrypted(content) {
|
|
186
|
+
if (!content || typeof content !== 'string') return false;
|
|
187
|
+
|
|
188
|
+
// Quick check: if it starts with { or [, it's likely JSON
|
|
189
|
+
const trimmed = content.trim();
|
|
190
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Check if it's valid base64 with minimum length for encrypted data
|
|
195
|
+
// 12 (IV) + 16 (authTag) = 28 bytes minimum = ~38 base64 chars
|
|
196
|
+
if (content.length < 38) return false;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const decoded = base64ToUint8(content);
|
|
200
|
+
// Minimum size check: IV (12) + authTag (16) + at least 1 byte of data
|
|
201
|
+
return decoded.length >= 29;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|