@kojinx/collab-crypto 0.0.2
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/esm2022/kojinx-collab-crypto.mjs +5 -0
- package/esm2022/lib/models/crypto.models.mjs +2 -0
- package/esm2022/lib/services/e2e-chat-crypto.service.mjs +148 -0
- package/esm2022/lib/services/session-key-manager.service.mjs +267 -0
- package/esm2022/lib/tokens.mjs +36 -0
- package/esm2022/lib/utils/safety-number.utils.mjs +72 -0
- package/esm2022/public-api.mjs +9 -0
- package/fesm2022/kojinx-collab-crypto.mjs +528 -0
- package/fesm2022/kojinx-collab-crypto.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/models/crypto.models.d.ts +99 -0
- package/lib/services/e2e-chat-crypto.service.d.ts +58 -0
- package/lib/services/session-key-manager.service.d.ts +71 -0
- package/lib/tokens.d.ts +24 -0
- package/lib/utils/safety-number.utils.d.ts +30 -0
- package/package.json +26 -0
- package/public-api.d.ts +5 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injectable } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Injected Asymmetric Cryptography Provider (RSA-2048 OAEP).
|
|
6
|
+
* On desktop: Implemented via Tauri Rust Keyring IPC.
|
|
7
|
+
* On mobile: Implemented via SubtleCrypto + Capacitor SecureStorage.
|
|
8
|
+
*/
|
|
9
|
+
const KOJINX_CRYPTO_PROVIDER = new InjectionToken('KOJINX_CRYPTO_PROVIDER');
|
|
10
|
+
/**
|
|
11
|
+
* Injected Client-Agnostic HTTP Fetcher (bypass CORS on native).
|
|
12
|
+
*/
|
|
13
|
+
const KOJINX_HTTP_FETCH = new InjectionToken('KOJINX_HTTP_FETCH');
|
|
14
|
+
/**
|
|
15
|
+
* Injected Storage Provider for local session key caching.
|
|
16
|
+
*/
|
|
17
|
+
const KOJINX_CRYPTO_STORAGE = new InjectionToken('KOJINX_CRYPTO_STORAGE', {
|
|
18
|
+
factory: () => ({
|
|
19
|
+
getItem: (key) => Promise.resolve(localStorage.getItem(key)),
|
|
20
|
+
setItem: (key, value) => {
|
|
21
|
+
localStorage.setItem(key, value);
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
},
|
|
24
|
+
removeItem: (key) => {
|
|
25
|
+
localStorage.removeItem(key);
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Injected API Base URL accessor function.
|
|
32
|
+
*/
|
|
33
|
+
const KOJINX_API_BASE_URL = new InjectionToken('KOJINX_API_BASE_URL');
|
|
34
|
+
/**
|
|
35
|
+
* Injected Auth Bearer Token accessor function.
|
|
36
|
+
*/
|
|
37
|
+
const KOJINX_AUTH_TOKEN = new InjectionToken('KOJINX_AUTH_TOKEN');
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Computes deterministic SHA-256 safety verification numbers (Signal-style).
|
|
41
|
+
* Users can compare these 30-digit numbers out-of-band to verify end-to-end identity.
|
|
42
|
+
*/
|
|
43
|
+
class SafetyNumberUtils {
|
|
44
|
+
/**
|
|
45
|
+
* Generates a Safety Number between two identity public keys.
|
|
46
|
+
*
|
|
47
|
+
* @param localPublicKey Base64-encoded local RSA public key
|
|
48
|
+
* @param remotePublicKey Base64-encoded remote RSA public key
|
|
49
|
+
*/
|
|
50
|
+
static async generate(localPublicKey, remotePublicKey) {
|
|
51
|
+
if (!localPublicKey || !remotePublicKey) {
|
|
52
|
+
throw new Error('Both public keys are required to generate safety numbers.');
|
|
53
|
+
}
|
|
54
|
+
const localFingerprint = await this.hashPublicKey(localPublicKey);
|
|
55
|
+
const remoteFingerprint = await this.hashPublicKey(remotePublicKey);
|
|
56
|
+
// Sort lexicographically to ensure order-independent verification
|
|
57
|
+
const sorted = [localFingerprint, remoteFingerprint].sort();
|
|
58
|
+
const combinedDigest = await this.sha256(sorted.join(''));
|
|
59
|
+
// Convert hex digest to a 30-digit decimal representation
|
|
60
|
+
const digits = this.hexTo30Digits(combinedDigest);
|
|
61
|
+
const formatted = this.formatBlocks(digits);
|
|
62
|
+
return {
|
|
63
|
+
raw: combinedDigest,
|
|
64
|
+
formatted,
|
|
65
|
+
localFingerprint: localFingerprint.substring(0, 16),
|
|
66
|
+
remoteFingerprint: remoteFingerprint.substring(0, 16)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Hashes a single public key using SHA-256.
|
|
71
|
+
*/
|
|
72
|
+
static async hashPublicKey(publicKeyBase64) {
|
|
73
|
+
const cleanKey = publicKeyBase64.replace(/\s+/g, '');
|
|
74
|
+
return this.sha256(cleanKey);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Computes SHA-256 hex string using W3C Web Crypto API.
|
|
78
|
+
*/
|
|
79
|
+
static async sha256(input) {
|
|
80
|
+
const encoder = new TextEncoder();
|
|
81
|
+
const data = encoder.encode(input);
|
|
82
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
83
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
84
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Converts a hexadecimal string into a deterministic 30-digit decimal string.
|
|
88
|
+
*/
|
|
89
|
+
static hexTo30Digits(hex) {
|
|
90
|
+
let result = '';
|
|
91
|
+
// Take chunks of 8 hex characters (32-bit integers) to produce 5-digit decimal blocks
|
|
92
|
+
for (let i = 0; i < 6; i++) {
|
|
93
|
+
const chunk = hex.substring(i * 8, (i + 1) * 8);
|
|
94
|
+
const num = parseInt(chunk, 16) % 100000;
|
|
95
|
+
result += num.toString().padStart(5, '0');
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Formats a 30-digit string into 6 blocks of 5 digits separated by spaces.
|
|
101
|
+
*/
|
|
102
|
+
static formatBlocks(digits) {
|
|
103
|
+
const blocks = [];
|
|
104
|
+
for (let i = 0; i < digits.length; i += 5) {
|
|
105
|
+
blocks.push(digits.substring(i, i + 5));
|
|
106
|
+
}
|
|
107
|
+
return blocks.join(' ');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Pure End-to-End Encryption cryptographic service.
|
|
113
|
+
* Handles AES-256-GCM message encryption/decryption and RSA key wrapping/unwrapping.
|
|
114
|
+
*/
|
|
115
|
+
class E2EChatCryptoService {
|
|
116
|
+
constructor() {
|
|
117
|
+
this.cryptoProvider = inject(KOJINX_CRYPTO_PROVIDER, {
|
|
118
|
+
optional: true
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Generates a new cryptographically secure 256-bit AES-GCM session key.
|
|
123
|
+
*/
|
|
124
|
+
async generateAesKey() {
|
|
125
|
+
return crypto.subtle.generateKey({
|
|
126
|
+
name: 'AES-GCM',
|
|
127
|
+
length: 256
|
|
128
|
+
}, true, // extractable for RSA wrapping & secure export
|
|
129
|
+
['encrypt', 'decrypt']);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Encrypts plaintext string using AES-256-GCM with a fresh 12-byte (96-bit) nonce.
|
|
133
|
+
*
|
|
134
|
+
* @param sessionKey In-memory active session key
|
|
135
|
+
* @param plaintext Plaintext message content
|
|
136
|
+
*/
|
|
137
|
+
async encryptMessage(sessionKey, plaintext) {
|
|
138
|
+
if (!sessionKey || !sessionKey.rawKey) {
|
|
139
|
+
throw new Error('Invalid session key provided for encryption.');
|
|
140
|
+
}
|
|
141
|
+
// Standard 12-byte initialization vector for AES-GCM
|
|
142
|
+
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
|
143
|
+
const encoder = new TextEncoder();
|
|
144
|
+
const encodedPlaintext = encoder.encode(plaintext);
|
|
145
|
+
const ciphertextBuffer = await crypto.subtle.encrypt({
|
|
146
|
+
name: 'AES-GCM',
|
|
147
|
+
iv: nonce
|
|
148
|
+
}, sessionKey.rawKey, encodedPlaintext);
|
|
149
|
+
return {
|
|
150
|
+
ciphertext: this.arrayBufferToBase64(ciphertextBuffer),
|
|
151
|
+
nonce: this.arrayBufferToBase64(nonce.buffer),
|
|
152
|
+
sessionKeyId: sessionKey.id,
|
|
153
|
+
generation: sessionKey.generation
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Decrypts an EncryptedMessagePayload using AES-256-GCM.
|
|
158
|
+
*
|
|
159
|
+
* @param sessionKey In-memory session key corresponding to payload.sessionKeyId
|
|
160
|
+
* @param payload Encrypted payload containing ciphertext and nonce
|
|
161
|
+
*/
|
|
162
|
+
async decryptMessage(sessionKey, payload) {
|
|
163
|
+
if (!sessionKey || !sessionKey.rawKey) {
|
|
164
|
+
throw new Error('Invalid session key provided for decryption.');
|
|
165
|
+
}
|
|
166
|
+
const ciphertext = this.base64ToArrayBuffer(payload.ciphertext);
|
|
167
|
+
const nonce = this.base64ToArrayBuffer(payload.nonce);
|
|
168
|
+
const decryptedBuffer = await crypto.subtle.decrypt({
|
|
169
|
+
name: 'AES-GCM',
|
|
170
|
+
iv: new Uint8Array(nonce)
|
|
171
|
+
}, sessionKey.rawKey, ciphertext);
|
|
172
|
+
const decoder = new TextDecoder();
|
|
173
|
+
return decoder.decode(decryptedBuffer);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Exports an AES-GCM key and wraps (encrypts) it with a recipient's RSA public key.
|
|
177
|
+
*
|
|
178
|
+
* @param aesKey AES CryptoKey to wrap
|
|
179
|
+
* @param recipientPublicKey Base64-encoded recipient RSA public key
|
|
180
|
+
*/
|
|
181
|
+
async wrapSessionKey(aesKey, recipientPublicKey) {
|
|
182
|
+
if (!this.cryptoProvider) {
|
|
183
|
+
throw new Error('KOJINX_CRYPTO_PROVIDER is not provided. Cannot wrap session keys.');
|
|
184
|
+
}
|
|
185
|
+
const rawKeyBytes = await crypto.subtle.exportKey('raw', aesKey);
|
|
186
|
+
const rawKeyBase64 = this.arrayBufferToBase64(rawKeyBytes);
|
|
187
|
+
// Asymmetrically encrypt using host RSA provider
|
|
188
|
+
return this.cryptoProvider.encrypt(recipientPublicKey, rawKeyBase64);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Unwraps (decrypts) an RSA-encrypted AES raw key and imports it back into a W3C CryptoKey.
|
|
192
|
+
*
|
|
193
|
+
* @param encryptedAesKeyBase64 RSA-encrypted AES raw key in base64 format
|
|
194
|
+
*/
|
|
195
|
+
async unwrapSessionKey(encryptedAesKeyBase64) {
|
|
196
|
+
if (!this.cryptoProvider) {
|
|
197
|
+
throw new Error('KOJINX_CRYPTO_PROVIDER is not provided. Cannot unwrap session keys.');
|
|
198
|
+
}
|
|
199
|
+
// Asymmetrically decrypt using local private key
|
|
200
|
+
const rawKeyBase64 = await this.cryptoProvider.decrypt(encryptedAesKeyBase64);
|
|
201
|
+
const rawKeyBuffer = this.base64ToArrayBuffer(rawKeyBase64);
|
|
202
|
+
return crypto.subtle.importKey('raw', rawKeyBuffer, {
|
|
203
|
+
name: 'AES-GCM',
|
|
204
|
+
length: 256
|
|
205
|
+
}, true, ['encrypt', 'decrypt']);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Exports a CryptoKey to base64 raw bytes.
|
|
209
|
+
*/
|
|
210
|
+
async exportRawKeyBase64(key) {
|
|
211
|
+
const raw = await crypto.subtle.exportKey('raw', key);
|
|
212
|
+
return this.arrayBufferToBase64(raw);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Imports a base64 raw key string into a CryptoKey.
|
|
216
|
+
*/
|
|
217
|
+
async importRawKeyBase64(base64) {
|
|
218
|
+
const buffer = this.base64ToArrayBuffer(base64);
|
|
219
|
+
return crypto.subtle.importKey('raw', buffer, {
|
|
220
|
+
name: 'AES-GCM',
|
|
221
|
+
length: 256
|
|
222
|
+
}, true, ['encrypt', 'decrypt']);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Encodes ArrayBuffer to Base64 string.
|
|
226
|
+
*/
|
|
227
|
+
arrayBufferToBase64(buffer) {
|
|
228
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
229
|
+
let binary = '';
|
|
230
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
231
|
+
binary += String.fromCharCode(bytes[i]);
|
|
232
|
+
}
|
|
233
|
+
return btoa(binary);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Decodes Base64 string to ArrayBuffer.
|
|
237
|
+
*/
|
|
238
|
+
base64ToArrayBuffer(base64) {
|
|
239
|
+
const binary = atob(base64.trim());
|
|
240
|
+
const bytes = new Uint8Array(binary.length);
|
|
241
|
+
for (let i = 0; i < binary.length; i++) {
|
|
242
|
+
bytes[i] = binary.charCodeAt(i);
|
|
243
|
+
}
|
|
244
|
+
return bytes.buffer;
|
|
245
|
+
}
|
|
246
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: E2EChatCryptoService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
247
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: E2EChatCryptoService, providedIn: 'root' }); }
|
|
248
|
+
}
|
|
249
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: E2EChatCryptoService, decorators: [{
|
|
250
|
+
type: Injectable,
|
|
251
|
+
args: [{
|
|
252
|
+
providedIn: 'root'
|
|
253
|
+
}]
|
|
254
|
+
}] });
|
|
255
|
+
|
|
256
|
+
/** 30 days in milliseconds for maximum session key lifetime */
|
|
257
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
258
|
+
/**
|
|
259
|
+
* Manages the lifecycle, caching, negotiation, and rotation of conversation session keys.
|
|
260
|
+
*/
|
|
261
|
+
class SessionKeyManagerService {
|
|
262
|
+
constructor() {
|
|
263
|
+
this.cryptoService = inject(E2EChatCryptoService);
|
|
264
|
+
this.httpFetch = inject(KOJINX_HTTP_FETCH, {
|
|
265
|
+
optional: true
|
|
266
|
+
});
|
|
267
|
+
this.getApiUrl = inject(KOJINX_API_BASE_URL, {
|
|
268
|
+
optional: true
|
|
269
|
+
});
|
|
270
|
+
this.getAuthToken = inject(KOJINX_AUTH_TOKEN, { optional: true });
|
|
271
|
+
this.storage = inject(KOJINX_CRYPTO_STORAGE, {
|
|
272
|
+
optional: true
|
|
273
|
+
});
|
|
274
|
+
/** Fast in-memory cache mapped by conversationId */
|
|
275
|
+
this.activeKeysByConversation = new Map();
|
|
276
|
+
/** Fast in-memory cache mapped by sessionKeyId (for historic messages) */
|
|
277
|
+
this.keysById = new Map();
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Retrieves or creates an active session key for a 1:1 Direct Message conversation.
|
|
281
|
+
*
|
|
282
|
+
* @param localUserId Current user's ID
|
|
283
|
+
* @param remoteUserId Remote contact's ID
|
|
284
|
+
* @param remotePublicKey Base64 RSA public key of remote contact
|
|
285
|
+
* @param localPublicKey Base64 RSA public key of local user
|
|
286
|
+
*/
|
|
287
|
+
async getOrCreateDmSessionKey(localUserId, remoteUserId, remotePublicKey, localPublicKey) {
|
|
288
|
+
const conversationId = this.buildDmConversationId(localUserId, remoteUserId);
|
|
289
|
+
// 1. Check in-memory cache
|
|
290
|
+
const cached = this.activeKeysByConversation.get(conversationId);
|
|
291
|
+
if (cached && !this.isKeyExpired(cached)) {
|
|
292
|
+
return cached;
|
|
293
|
+
}
|
|
294
|
+
// 2. Attempt fetching from server if HTTP fetch is provided
|
|
295
|
+
if (this.httpFetch && this.getApiUrl && this.getAuthToken) {
|
|
296
|
+
const serverKey = await this.fetchSessionKeyFromServer(conversationId, 'dm');
|
|
297
|
+
if (serverKey) {
|
|
298
|
+
this.cacheKey(serverKey);
|
|
299
|
+
return serverKey;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// 3. Create fresh session key and distribute
|
|
303
|
+
return this.createAndDistributeSessionKey({
|
|
304
|
+
type: 'dm',
|
|
305
|
+
conversationId,
|
|
306
|
+
generation: cached ? cached.generation + 1 : 1,
|
|
307
|
+
members: [
|
|
308
|
+
{ userId: localUserId, publicKey: localPublicKey },
|
|
309
|
+
{ userId: remoteUserId, publicKey: remotePublicKey }
|
|
310
|
+
]
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Retrieves or creates an active session key for a Native Channel or Group.
|
|
315
|
+
*
|
|
316
|
+
* @param channelId Unique channel ID
|
|
317
|
+
* @param members List of all workspace/channel members with their public keys
|
|
318
|
+
*/
|
|
319
|
+
async getOrCreateChannelSessionKey(channelId, members) {
|
|
320
|
+
const cached = this.activeKeysByConversation.get(channelId);
|
|
321
|
+
if (cached && !this.isKeyExpired(cached)) {
|
|
322
|
+
return cached;
|
|
323
|
+
}
|
|
324
|
+
if (this.httpFetch && this.getApiUrl && this.getAuthToken) {
|
|
325
|
+
const serverKey = await this.fetchSessionKeyFromServer(channelId, 'channel');
|
|
326
|
+
if (serverKey) {
|
|
327
|
+
this.cacheKey(serverKey);
|
|
328
|
+
return serverKey;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return this.createAndDistributeSessionKey({
|
|
332
|
+
type: 'channel',
|
|
333
|
+
conversationId: channelId,
|
|
334
|
+
generation: cached ? cached.generation + 1 : 1,
|
|
335
|
+
members
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Retrieves a session key by its specific sessionKeyId (used for decrypting historic messages).
|
|
340
|
+
*/
|
|
341
|
+
async getSessionKeyById(sessionKeyId) {
|
|
342
|
+
const cached = this.keysById.get(sessionKeyId);
|
|
343
|
+
if (cached)
|
|
344
|
+
return cached;
|
|
345
|
+
// Check local storage
|
|
346
|
+
if (this.storage) {
|
|
347
|
+
const stored = await this.storage.getItem(`kojinx_sk_${sessionKeyId}`);
|
|
348
|
+
if (stored) {
|
|
349
|
+
try {
|
|
350
|
+
const parsed = JSON.parse(stored);
|
|
351
|
+
const rawKey = await this.cryptoService.importRawKeyBase64(parsed.rawKeyBase64);
|
|
352
|
+
const key = {
|
|
353
|
+
id: parsed.id,
|
|
354
|
+
type: parsed.type,
|
|
355
|
+
conversationId: parsed.conversationId,
|
|
356
|
+
rawKey,
|
|
357
|
+
generation: parsed.generation,
|
|
358
|
+
createdAt: parsed.createdAt
|
|
359
|
+
};
|
|
360
|
+
this.cacheKey(key);
|
|
361
|
+
return key;
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
// ignore corrupted local key
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Generates a new AES session key, wraps it for all members, and uploads to the server.
|
|
372
|
+
*/
|
|
373
|
+
async createAndDistributeSessionKey(params) {
|
|
374
|
+
const rawAesKey = await this.cryptoService.generateAesKey();
|
|
375
|
+
const sessionKeyId = this.generateUuid();
|
|
376
|
+
const memberPayloads = {};
|
|
377
|
+
for (const member of params.members) {
|
|
378
|
+
if (member.publicKey) {
|
|
379
|
+
memberPayloads[member.userId] =
|
|
380
|
+
await this.cryptoService.wrapSessionKey(rawAesKey, member.publicKey);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const sessionKey = {
|
|
384
|
+
id: sessionKeyId,
|
|
385
|
+
type: params.type,
|
|
386
|
+
conversationId: params.conversationId,
|
|
387
|
+
rawKey: rawAesKey,
|
|
388
|
+
generation: params.generation,
|
|
389
|
+
createdAt: Date.now()
|
|
390
|
+
};
|
|
391
|
+
// Store in memory
|
|
392
|
+
this.cacheKey(sessionKey);
|
|
393
|
+
await this.persistKeyLocally(sessionKey);
|
|
394
|
+
// Upload to server if online
|
|
395
|
+
if (this.httpFetch && this.getApiUrl && this.getAuthToken) {
|
|
396
|
+
const token = await this.getAuthToken();
|
|
397
|
+
const apiUrl = this.getApiUrl();
|
|
398
|
+
const payload = {
|
|
399
|
+
sessionKeyId,
|
|
400
|
+
conversationType: params.type,
|
|
401
|
+
conversationId: params.conversationId,
|
|
402
|
+
generation: params.generation,
|
|
403
|
+
memberPayloads
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
await this.httpFetch(`${apiUrl}/api/e2e/session-keys`, {
|
|
407
|
+
method: 'POST',
|
|
408
|
+
headers: {
|
|
409
|
+
Authorization: `Bearer ${token}`,
|
|
410
|
+
'Content-Type': 'application/json'
|
|
411
|
+
},
|
|
412
|
+
body: JSON.stringify(payload)
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
console.warn('[SessionKeyManager] Failed to sync session key to server:', err);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return sessionKey;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Helper to check if a key has exceeded the 30-day rotation threshold.
|
|
423
|
+
*/
|
|
424
|
+
isKeyExpired(key) {
|
|
425
|
+
return Date.now() - key.createdAt > THIRTY_DAYS_MS;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Builds a deterministic conversation identifier for 1:1 DMs (lexicographical sort).
|
|
429
|
+
*/
|
|
430
|
+
buildDmConversationId(userIdA, userIdB) {
|
|
431
|
+
return [userIdA, userIdB].sort().join('_');
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Caches a session key in memory.
|
|
435
|
+
*/
|
|
436
|
+
cacheKey(key) {
|
|
437
|
+
this.activeKeysByConversation.set(key.conversationId, key);
|
|
438
|
+
this.keysById.set(key.id, key);
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Persists an unwrapped session key to local storage for fast startup.
|
|
442
|
+
*/
|
|
443
|
+
async persistKeyLocally(key) {
|
|
444
|
+
if (!this.storage)
|
|
445
|
+
return;
|
|
446
|
+
try {
|
|
447
|
+
const rawKeyBase64 = await this.cryptoService.exportRawKeyBase64(key.rawKey);
|
|
448
|
+
const data = JSON.stringify({
|
|
449
|
+
id: key.id,
|
|
450
|
+
type: key.type,
|
|
451
|
+
conversationId: key.conversationId,
|
|
452
|
+
rawKeyBase64,
|
|
453
|
+
generation: key.generation,
|
|
454
|
+
createdAt: key.createdAt
|
|
455
|
+
});
|
|
456
|
+
await this.storage.setItem(`kojinx_sk_${key.id}`, data);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
// ignore persistence error
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Fetches latest session key from server and unwraps it locally.
|
|
464
|
+
*/
|
|
465
|
+
async fetchSessionKeyFromServer(conversationId, type) {
|
|
466
|
+
if (!this.httpFetch || !this.getApiUrl || !this.getAuthToken)
|
|
467
|
+
return null;
|
|
468
|
+
try {
|
|
469
|
+
const token = await this.getAuthToken();
|
|
470
|
+
const apiUrl = this.getApiUrl();
|
|
471
|
+
const res = await this.httpFetch(`${apiUrl}/api/e2e/session-keys/${conversationId}`, {
|
|
472
|
+
method: 'GET',
|
|
473
|
+
headers: {
|
|
474
|
+
Authorization: `Bearer ${token}`
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
if (res.status === 200 && res.data?.encryptedKey) {
|
|
478
|
+
const rawKey = await this.cryptoService.unwrapSessionKey(res.data.encryptedKey);
|
|
479
|
+
return {
|
|
480
|
+
id: res.data.sessionKeyId,
|
|
481
|
+
type,
|
|
482
|
+
conversationId,
|
|
483
|
+
rawKey,
|
|
484
|
+
generation: res.data.generation || 1,
|
|
485
|
+
createdAt: res.data.createdAt
|
|
486
|
+
? new Date(res.data.createdAt).getTime()
|
|
487
|
+
: Date.now()
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
// Not found or network error
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Generates a random UUID (W3C standard or fallback).
|
|
498
|
+
*/
|
|
499
|
+
generateUuid() {
|
|
500
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
501
|
+
return crypto.randomUUID();
|
|
502
|
+
}
|
|
503
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
504
|
+
const r = (Math.random() * 16) | 0;
|
|
505
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
506
|
+
return v.toString(16);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionKeyManagerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
510
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionKeyManagerService, providedIn: 'root' }); }
|
|
511
|
+
}
|
|
512
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SessionKeyManagerService, decorators: [{
|
|
513
|
+
type: Injectable,
|
|
514
|
+
args: [{
|
|
515
|
+
providedIn: 'root'
|
|
516
|
+
}]
|
|
517
|
+
}] });
|
|
518
|
+
|
|
519
|
+
/*
|
|
520
|
+
* Public API Surface of @kojinx/collab-crypto
|
|
521
|
+
*/
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Generated bundle index. Do not edit.
|
|
525
|
+
*/
|
|
526
|
+
|
|
527
|
+
export { E2EChatCryptoService, KOJINX_API_BASE_URL, KOJINX_AUTH_TOKEN, KOJINX_CRYPTO_PROVIDER, KOJINX_CRYPTO_STORAGE, KOJINX_HTTP_FETCH, SafetyNumberUtils, SessionKeyManagerService };
|
|
528
|
+
//# sourceMappingURL=kojinx-collab-crypto.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kojinx-collab-crypto.mjs","sources":["../../../../projects/kojinx/collab-crypto/src/lib/tokens.ts","../../../../projects/kojinx/collab-crypto/src/lib/utils/safety-number.utils.ts","../../../../projects/kojinx/collab-crypto/src/lib/services/e2e-chat-crypto.service.ts","../../../../projects/kojinx/collab-crypto/src/lib/services/session-key-manager.service.ts","../../../../projects/kojinx/collab-crypto/src/public-api.ts","../../../../projects/kojinx/collab-crypto/src/kojinx-collab-crypto.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport {\n KojinxCryptoProvider,\n KojinxFetchFunction,\n KojinxStorageProvider\n} from './models/crypto.models';\n\n/**\n * Injected Asymmetric Cryptography Provider (RSA-2048 OAEP).\n * On desktop: Implemented via Tauri Rust Keyring IPC.\n * On mobile: Implemented via SubtleCrypto + Capacitor SecureStorage.\n */\nexport const KOJINX_CRYPTO_PROVIDER = new InjectionToken<KojinxCryptoProvider>(\n 'KOJINX_CRYPTO_PROVIDER'\n);\n\n/**\n * Injected Client-Agnostic HTTP Fetcher (bypass CORS on native).\n */\nexport const KOJINX_HTTP_FETCH = new InjectionToken<KojinxFetchFunction>(\n 'KOJINX_HTTP_FETCH'\n);\n\n/**\n * Injected Storage Provider for local session key caching.\n */\nexport const KOJINX_CRYPTO_STORAGE = new InjectionToken<KojinxStorageProvider>(\n 'KOJINX_CRYPTO_STORAGE',\n {\n factory: () => ({\n getItem: (key: string) => Promise.resolve(localStorage.getItem(key)),\n setItem: (key: string, value: string) => {\n localStorage.setItem(key, value);\n return Promise.resolve();\n },\n removeItem: (key: string) => {\n localStorage.removeItem(key);\n return Promise.resolve();\n }\n })\n }\n);\n\n/**\n * Injected API Base URL accessor function.\n */\nexport const KOJINX_API_BASE_URL = new InjectionToken<() => string>(\n 'KOJINX_API_BASE_URL'\n);\n\n/**\n * Injected Auth Bearer Token accessor function.\n */\nexport const KOJINX_AUTH_TOKEN = new InjectionToken<() => Promise<string | null>>(\n 'KOJINX_AUTH_TOKEN'\n);\n","import { SafetyNumber } from '../models/crypto.models';\n\n/**\n * Computes deterministic SHA-256 safety verification numbers (Signal-style).\n * Users can compare these 30-digit numbers out-of-band to verify end-to-end identity.\n */\nexport class SafetyNumberUtils {\n /**\n * Generates a Safety Number between two identity public keys.\n *\n * @param localPublicKey Base64-encoded local RSA public key\n * @param remotePublicKey Base64-encoded remote RSA public key\n */\n static async generate(\n localPublicKey: string,\n remotePublicKey: string\n ): Promise<SafetyNumber> {\n if (!localPublicKey || !remotePublicKey) {\n throw new Error('Both public keys are required to generate safety numbers.');\n }\n\n const localFingerprint = await this.hashPublicKey(localPublicKey);\n const remoteFingerprint = await this.hashPublicKey(remotePublicKey);\n\n // Sort lexicographically to ensure order-independent verification\n const sorted = [localFingerprint, remoteFingerprint].sort();\n const combinedDigest = await this.sha256(sorted.join(''));\n\n // Convert hex digest to a 30-digit decimal representation\n const digits = this.hexTo30Digits(combinedDigest);\n const formatted = this.formatBlocks(digits);\n\n return {\n raw: combinedDigest,\n formatted,\n localFingerprint: localFingerprint.substring(0, 16),\n remoteFingerprint: remoteFingerprint.substring(0, 16)\n };\n }\n\n /**\n * Hashes a single public key using SHA-256.\n */\n static async hashPublicKey(publicKeyBase64: string): Promise<string> {\n const cleanKey = publicKeyBase64.replace(/\\s+/g, '');\n return this.sha256(cleanKey);\n }\n\n /**\n * Computes SHA-256 hex string using W3C Web Crypto API.\n */\n private static async sha256(input: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(input);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n }\n\n /**\n * Converts a hexadecimal string into a deterministic 30-digit decimal string.\n */\n private static hexTo30Digits(hex: string): string {\n let result = '';\n // Take chunks of 8 hex characters (32-bit integers) to produce 5-digit decimal blocks\n for (let i = 0; i < 6; i++) {\n const chunk = hex.substring(i * 8, (i + 1) * 8);\n const num = parseInt(chunk, 16) % 100000;\n result += num.toString().padStart(5, '0');\n }\n return result;\n }\n\n /**\n * Formats a 30-digit string into 6 blocks of 5 digits separated by spaces.\n */\n private static formatBlocks(digits: string): string {\n const blocks: string[] = [];\n for (let i = 0; i < digits.length; i += 5) {\n blocks.push(digits.substring(i, i + 5));\n }\n return blocks.join(' ');\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport {\n ConversationSessionKey,\n EncryptedMessagePayload,\n KojinxCryptoProvider\n} from '../models/crypto.models';\nimport { KOJINX_CRYPTO_PROVIDER } from '../tokens';\n\n/**\n * Pure End-to-End Encryption cryptographic service.\n * Handles AES-256-GCM message encryption/decryption and RSA key wrapping/unwrapping.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class E2EChatCryptoService {\n private cryptoProvider = inject<KojinxCryptoProvider>(KOJINX_CRYPTO_PROVIDER, {\n optional: true\n });\n\n /**\n * Generates a new cryptographically secure 256-bit AES-GCM session key.\n */\n async generateAesKey(): Promise<CryptoKey> {\n return crypto.subtle.generateKey(\n {\n name: 'AES-GCM',\n length: 256\n },\n true, // extractable for RSA wrapping & secure export\n ['encrypt', 'decrypt']\n );\n }\n\n /**\n * Encrypts plaintext string using AES-256-GCM with a fresh 12-byte (96-bit) nonce.\n *\n * @param sessionKey In-memory active session key\n * @param plaintext Plaintext message content\n */\n async encryptMessage(\n sessionKey: ConversationSessionKey,\n plaintext: string\n ): Promise<EncryptedMessagePayload> {\n if (!sessionKey || !sessionKey.rawKey) {\n throw new Error('Invalid session key provided for encryption.');\n }\n\n // Standard 12-byte initialization vector for AES-GCM\n const nonce = crypto.getRandomValues(new Uint8Array(12));\n const encoder = new TextEncoder();\n const encodedPlaintext = encoder.encode(plaintext);\n\n const ciphertextBuffer = await crypto.subtle.encrypt(\n {\n name: 'AES-GCM',\n iv: nonce\n },\n sessionKey.rawKey,\n encodedPlaintext\n );\n\n return {\n ciphertext: this.arrayBufferToBase64(ciphertextBuffer),\n nonce: this.arrayBufferToBase64(nonce.buffer),\n sessionKeyId: sessionKey.id,\n generation: sessionKey.generation\n };\n }\n\n /**\n * Decrypts an EncryptedMessagePayload using AES-256-GCM.\n *\n * @param sessionKey In-memory session key corresponding to payload.sessionKeyId\n * @param payload Encrypted payload containing ciphertext and nonce\n */\n async decryptMessage(\n sessionKey: ConversationSessionKey,\n payload: EncryptedMessagePayload\n ): Promise<string> {\n if (!sessionKey || !sessionKey.rawKey) {\n throw new Error('Invalid session key provided for decryption.');\n }\n\n const ciphertext = this.base64ToArrayBuffer(payload.ciphertext);\n const nonce = this.base64ToArrayBuffer(payload.nonce);\n\n const decryptedBuffer = await crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: new Uint8Array(nonce)\n },\n sessionKey.rawKey,\n ciphertext\n );\n\n const decoder = new TextDecoder();\n return decoder.decode(decryptedBuffer);\n }\n\n /**\n * Exports an AES-GCM key and wraps (encrypts) it with a recipient's RSA public key.\n *\n * @param aesKey AES CryptoKey to wrap\n * @param recipientPublicKey Base64-encoded recipient RSA public key\n */\n async wrapSessionKey(\n aesKey: CryptoKey,\n recipientPublicKey: string\n ): Promise<string> {\n if (!this.cryptoProvider) {\n throw new Error(\n 'KOJINX_CRYPTO_PROVIDER is not provided. Cannot wrap session keys.'\n );\n }\n\n const rawKeyBytes = await crypto.subtle.exportKey('raw', aesKey);\n const rawKeyBase64 = this.arrayBufferToBase64(rawKeyBytes);\n\n // Asymmetrically encrypt using host RSA provider\n return this.cryptoProvider.encrypt(recipientPublicKey, rawKeyBase64);\n }\n\n /**\n * Unwraps (decrypts) an RSA-encrypted AES raw key and imports it back into a W3C CryptoKey.\n *\n * @param encryptedAesKeyBase64 RSA-encrypted AES raw key in base64 format\n */\n async unwrapSessionKey(encryptedAesKeyBase64: string): Promise<CryptoKey> {\n if (!this.cryptoProvider) {\n throw new Error(\n 'KOJINX_CRYPTO_PROVIDER is not provided. Cannot unwrap session keys.'\n );\n }\n\n // Asymmetrically decrypt using local private key\n const rawKeyBase64 = await this.cryptoProvider.decrypt(\n encryptedAesKeyBase64\n );\n const rawKeyBuffer = this.base64ToArrayBuffer(rawKeyBase64);\n\n return crypto.subtle.importKey(\n 'raw',\n rawKeyBuffer,\n {\n name: 'AES-GCM',\n length: 256\n },\n true,\n ['encrypt', 'decrypt']\n );\n }\n\n /**\n * Exports a CryptoKey to base64 raw bytes.\n */\n async exportRawKeyBase64(key: CryptoKey): Promise<string> {\n const raw = await crypto.subtle.exportKey('raw', key);\n return this.arrayBufferToBase64(raw);\n }\n\n /**\n * Imports a base64 raw key string into a CryptoKey.\n */\n async importRawKeyBase64(base64: string): Promise<CryptoKey> {\n const buffer = this.base64ToArrayBuffer(base64);\n return crypto.subtle.importKey(\n 'raw',\n buffer,\n {\n name: 'AES-GCM',\n length: 256\n },\n true,\n ['encrypt', 'decrypt']\n );\n }\n\n /**\n * Encodes ArrayBuffer to Base64 string.\n */\n arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {\n const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.byteLength; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary);\n }\n\n /**\n * Decodes Base64 string to ArrayBuffer.\n */\n base64ToArrayBuffer(base64: string): ArrayBuffer {\n const binary = atob(base64.trim());\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n return bytes.buffer;\n }\n}\n","import { Injectable, inject } from '@angular/core';\nimport {\n ConversationSessionKey,\n KojinxFetchFunction,\n KojinxStorageProvider,\n MemberPublicKey,\n SessionKeyExchangePayload\n} from '../models/crypto.models';\nimport {\n KOJINX_API_BASE_URL,\n KOJINX_AUTH_TOKEN,\n KOJINX_CRYPTO_STORAGE,\n KOJINX_HTTP_FETCH\n} from '../tokens';\nimport { E2EChatCryptoService } from './e2e-chat-crypto.service';\n\n/** 30 days in milliseconds for maximum session key lifetime */\nconst THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;\n\n/**\n * Manages the lifecycle, caching, negotiation, and rotation of conversation session keys.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class SessionKeyManagerService {\n private cryptoService = inject(E2EChatCryptoService);\n private httpFetch = inject<KojinxFetchFunction>(KOJINX_HTTP_FETCH, {\n optional: true\n });\n private getApiUrl = inject<() => string>(KOJINX_API_BASE_URL, {\n optional: true\n });\n private getAuthToken = inject<() => Promise<string | null>>(\n KOJINX_AUTH_TOKEN,\n { optional: true }\n );\n private storage = inject<KojinxStorageProvider>(KOJINX_CRYPTO_STORAGE, {\n optional: true\n });\n\n /** Fast in-memory cache mapped by conversationId */\n private activeKeysByConversation = new Map<string, ConversationSessionKey>();\n /** Fast in-memory cache mapped by sessionKeyId (for historic messages) */\n private keysById = new Map<string, ConversationSessionKey>();\n\n /**\n * Retrieves or creates an active session key for a 1:1 Direct Message conversation.\n *\n * @param localUserId Current user's ID\n * @param remoteUserId Remote contact's ID\n * @param remotePublicKey Base64 RSA public key of remote contact\n * @param localPublicKey Base64 RSA public key of local user\n */\n async getOrCreateDmSessionKey(\n localUserId: string,\n remoteUserId: string,\n remotePublicKey: string,\n localPublicKey: string\n ): Promise<ConversationSessionKey> {\n const conversationId = this.buildDmConversationId(\n localUserId,\n remoteUserId\n );\n\n // 1. Check in-memory cache\n const cached = this.activeKeysByConversation.get(conversationId);\n if (cached && !this.isKeyExpired(cached)) {\n return cached;\n }\n\n // 2. Attempt fetching from server if HTTP fetch is provided\n if (this.httpFetch && this.getApiUrl && this.getAuthToken) {\n const serverKey = await this.fetchSessionKeyFromServer(\n conversationId,\n 'dm'\n );\n if (serverKey) {\n this.cacheKey(serverKey);\n return serverKey;\n }\n }\n\n // 3. Create fresh session key and distribute\n return this.createAndDistributeSessionKey({\n type: 'dm',\n conversationId,\n generation: cached ? cached.generation + 1 : 1,\n members: [\n { userId: localUserId, publicKey: localPublicKey },\n { userId: remoteUserId, publicKey: remotePublicKey }\n ]\n });\n }\n\n /**\n * Retrieves or creates an active session key for a Native Channel or Group.\n *\n * @param channelId Unique channel ID\n * @param members List of all workspace/channel members with their public keys\n */\n async getOrCreateChannelSessionKey(\n channelId: string,\n members: MemberPublicKey[]\n ): Promise<ConversationSessionKey> {\n const cached = this.activeKeysByConversation.get(channelId);\n if (cached && !this.isKeyExpired(cached)) {\n return cached;\n }\n\n if (this.httpFetch && this.getApiUrl && this.getAuthToken) {\n const serverKey = await this.fetchSessionKeyFromServer(\n channelId,\n 'channel'\n );\n if (serverKey) {\n this.cacheKey(serverKey);\n return serverKey;\n }\n }\n\n return this.createAndDistributeSessionKey({\n type: 'channel',\n conversationId: channelId,\n generation: cached ? cached.generation + 1 : 1,\n members\n });\n }\n\n /**\n * Retrieves a session key by its specific sessionKeyId (used for decrypting historic messages).\n */\n async getSessionKeyById(\n sessionKeyId: string\n ): Promise<ConversationSessionKey | null> {\n const cached = this.keysById.get(sessionKeyId);\n if (cached) return cached;\n\n // Check local storage\n if (this.storage) {\n const stored = await this.storage.getItem(\n `kojinx_sk_${sessionKeyId}`\n );\n if (stored) {\n try {\n const parsed = JSON.parse(stored);\n const rawKey = await this.cryptoService.importRawKeyBase64(\n parsed.rawKeyBase64\n );\n const key: ConversationSessionKey = {\n id: parsed.id,\n type: parsed.type,\n conversationId: parsed.conversationId,\n rawKey,\n generation: parsed.generation,\n createdAt: parsed.createdAt\n };\n this.cacheKey(key);\n return key;\n } catch {\n // ignore corrupted local key\n }\n }\n }\n\n return null;\n }\n\n /**\n * Generates a new AES session key, wraps it for all members, and uploads to the server.\n */\n async createAndDistributeSessionKey(params: {\n type: 'dm' | 'channel' | 'group';\n conversationId: string;\n generation: number;\n members: MemberPublicKey[];\n }): Promise<ConversationSessionKey> {\n const rawAesKey = await this.cryptoService.generateAesKey();\n const sessionKeyId = this.generateUuid();\n\n const memberPayloads: Record<string, string> = {};\n for (const member of params.members) {\n if (member.publicKey) {\n memberPayloads[member.userId] =\n await this.cryptoService.wrapSessionKey(\n rawAesKey,\n member.publicKey\n );\n }\n }\n\n const sessionKey: ConversationSessionKey = {\n id: sessionKeyId,\n type: params.type,\n conversationId: params.conversationId,\n rawKey: rawAesKey,\n generation: params.generation,\n createdAt: Date.now()\n };\n\n // Store in memory\n this.cacheKey(sessionKey);\n await this.persistKeyLocally(sessionKey);\n\n // Upload to server if online\n if (this.httpFetch && this.getApiUrl && this.getAuthToken) {\n const token = await this.getAuthToken();\n const apiUrl = this.getApiUrl();\n const payload: SessionKeyExchangePayload = {\n sessionKeyId,\n conversationType: params.type,\n conversationId: params.conversationId,\n generation: params.generation,\n memberPayloads\n };\n\n try {\n await this.httpFetch(`${apiUrl}/api/e2e/session-keys`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(payload)\n });\n } catch (err) {\n console.warn(\n '[SessionKeyManager] Failed to sync session key to server:',\n err\n );\n }\n }\n\n return sessionKey;\n }\n\n /**\n * Helper to check if a key has exceeded the 30-day rotation threshold.\n */\n isKeyExpired(key: ConversationSessionKey): boolean {\n return Date.now() - key.createdAt > THIRTY_DAYS_MS;\n }\n\n /**\n * Builds a deterministic conversation identifier for 1:1 DMs (lexicographical sort).\n */\n buildDmConversationId(userIdA: string, userIdB: string): string {\n return [userIdA, userIdB].sort().join('_');\n }\n\n /**\n * Caches a session key in memory.\n */\n private cacheKey(key: ConversationSessionKey): void {\n this.activeKeysByConversation.set(key.conversationId, key);\n this.keysById.set(key.id, key);\n }\n\n /**\n * Persists an unwrapped session key to local storage for fast startup.\n */\n private async persistKeyLocally(\n key: ConversationSessionKey\n ): Promise<void> {\n if (!this.storage) return;\n try {\n const rawKeyBase64 = await this.cryptoService.exportRawKeyBase64(\n key.rawKey\n );\n const data = JSON.stringify({\n id: key.id,\n type: key.type,\n conversationId: key.conversationId,\n rawKeyBase64,\n generation: key.generation,\n createdAt: key.createdAt\n });\n await this.storage.setItem(`kojinx_sk_${key.id}`, data);\n } catch {\n // ignore persistence error\n }\n }\n\n /**\n * Fetches latest session key from server and unwraps it locally.\n */\n private async fetchSessionKeyFromServer(\n conversationId: string,\n type: 'dm' | 'channel' | 'group'\n ): Promise<ConversationSessionKey | null> {\n if (!this.httpFetch || !this.getApiUrl || !this.getAuthToken) return null;\n\n try {\n const token = await this.getAuthToken();\n const apiUrl = this.getApiUrl();\n const res = await this.httpFetch<{\n sessionKeyId: string;\n generation: number;\n encryptedKey: string;\n createdAt?: string;\n }>(`${apiUrl}/api/e2e/session-keys/${conversationId}`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${token}`\n }\n });\n\n if (res.status === 200 && res.data?.encryptedKey) {\n const rawKey = await this.cryptoService.unwrapSessionKey(\n res.data.encryptedKey\n );\n return {\n id: res.data.sessionKeyId,\n type,\n conversationId,\n rawKey,\n generation: res.data.generation || 1,\n createdAt: res.data.createdAt\n ? new Date(res.data.createdAt).getTime()\n : Date.now()\n };\n }\n } catch {\n // Not found or network error\n }\n\n return null;\n }\n\n /**\n * Generates a random UUID (W3C standard or fallback).\n */\n private generateUuid(): string {\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n const r = (Math.random() * 16) | 0;\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n }\n}\n","/*\n * Public API Surface of @kojinx/collab-crypto\n */\n\nexport * from './lib/tokens';\nexport * from './lib/models/crypto.models';\nexport * from './lib/utils/safety-number.utils';\nexport * from './lib/services/e2e-chat-crypto.service';\nexport * from './lib/services/session-key-manager.service';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|