@profullstack/qryptchat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1348 @@
1
+ import {
2
+ sessionPath,
3
+ baseUrl,
4
+ loadSession,
5
+ saveSession,
6
+ clearSession
7
+ } from "./chunk-fb6zrz6m.js";
8
+
9
+ // src/cli/crypto.js
10
+ import { MlKem1024 as MlKem10242 } from "mlkem";
11
+
12
+ // src/lib/crypto/post-quantum-encryption.js
13
+ import { MlKem1024, MlKem768 } from "mlkem";
14
+
15
+ // src/lib/crypto/index.js
16
+ import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
17
+ var CRYPTO_CONFIG = {
18
+ KYBER: {
19
+ PUBLIC_KEY_SIZE: 1568,
20
+ PRIVATE_KEY_SIZE: 3168,
21
+ CIPHERTEXT_SIZE: 1568,
22
+ SHARED_SECRET_SIZE: 32
23
+ },
24
+ DILITHIUM: {
25
+ PUBLIC_KEY_SIZE: 2592,
26
+ PRIVATE_KEY_SIZE: 4864,
27
+ SIGNATURE_SIZE: 4595
28
+ },
29
+ CHACHA20_POLY1305: {
30
+ KEY_SIZE: 32,
31
+ NONCE_SIZE: 12,
32
+ TAG_SIZE: 16
33
+ },
34
+ HKDF: {
35
+ HASH: "SHA-256",
36
+ SALT_SIZE: 32,
37
+ INFO_PREFIX: "QryptChat-v1"
38
+ }
39
+ };
40
+ var Base64 = {
41
+ encode(bytes) {
42
+ let binary = "";
43
+ for (let i = 0;i < bytes.length; i++) {
44
+ binary += String.fromCharCode(bytes[i]);
45
+ }
46
+ return btoa(binary);
47
+ },
48
+ decode(base64) {
49
+ const binary = atob(base64);
50
+ const bytes = new Uint8Array(binary.length);
51
+ for (let i = 0;i < binary.length; i++) {
52
+ bytes[i] = binary.charCodeAt(i);
53
+ }
54
+ return bytes;
55
+ }
56
+ };
57
+
58
+ class SecureRandom {
59
+ static getRandomBytes(length) {
60
+ const bytes = new Uint8Array(length);
61
+ crypto.getRandomValues(bytes);
62
+ return bytes;
63
+ }
64
+ static generateSalt() {
65
+ return this.getRandomBytes(CRYPTO_CONFIG.HKDF.SALT_SIZE);
66
+ }
67
+ static generateNonce() {
68
+ return this.getRandomBytes(CRYPTO_CONFIG.CHACHA20_POLY1305.NONCE_SIZE);
69
+ }
70
+ }
71
+
72
+ class HKDF {
73
+ static async derive(inputKeyMaterial, salt, info, length) {
74
+ const key = await crypto.subtle.importKey("raw", inputKeyMaterial, { name: "HKDF" }, false, ["deriveKey", "deriveBits"]);
75
+ const derivedBits = await crypto.subtle.deriveBits({
76
+ name: "HKDF",
77
+ hash: CRYPTO_CONFIG.HKDF.HASH,
78
+ salt,
79
+ info: new TextEncoder().encode(`${CRYPTO_CONFIG.HKDF.INFO_PREFIX}-${info}`)
80
+ }, key, length * 8);
81
+ return new Uint8Array(derivedBits);
82
+ }
83
+ static async deriveRootAndChainKeys(sharedSecret, salt, context) {
84
+ const derivedKey = await this.derive(sharedSecret, salt, context, 64);
85
+ return {
86
+ rootKey: derivedKey.slice(0, 32),
87
+ chainKey: derivedKey.slice(32, 64)
88
+ };
89
+ }
90
+ static async deriveMessageKey(chainKey, messageNumber) {
91
+ const info = `MessageKey-${messageNumber}`;
92
+ return await this.derive(chainKey, new Uint8Array(0), info, CRYPTO_CONFIG.CHACHA20_POLY1305.KEY_SIZE);
93
+ }
94
+ }
95
+
96
+ class ChaCha20Poly1305 {
97
+ static async encrypt(key, nonce, plaintext, additionalData = new Uint8Array(0)) {
98
+ try {
99
+ const cipher = chacha20poly1305(key, nonce, additionalData);
100
+ const ciphertext = cipher.encrypt(plaintext);
101
+ return ciphertext;
102
+ } catch (error) {
103
+ throw new EncryptionError(`ChaCha20-Poly1305 encryption failed: ${error.message}`);
104
+ }
105
+ }
106
+ static async decrypt(key, nonce, ciphertext, additionalData = new Uint8Array(0)) {
107
+ try {
108
+ const cipher = chacha20poly1305(key, nonce, additionalData);
109
+ const plaintext = cipher.decrypt(ciphertext);
110
+ return plaintext;
111
+ } catch (error) {
112
+ throw new DecryptionError(`ChaCha20-Poly1305 decryption failed: ${error.message}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ class CryptoError extends Error {
118
+ constructor(message, code) {
119
+ super(message);
120
+ this.name = "CryptoError";
121
+ this.code = code;
122
+ }
123
+ }
124
+ class EncryptionError extends CryptoError {
125
+ constructor(message) {
126
+ super(message, "ENCRYPTION_ERROR");
127
+ this.name = "EncryptionError";
128
+ }
129
+ }
130
+
131
+ class DecryptionError extends CryptoError {
132
+ constructor(message) {
133
+ super(message, "DECRYPTION_ERROR");
134
+ this.name = "DecryptionError";
135
+ }
136
+ }
137
+ class CryptoUtils {
138
+ static constantTimeEqual(a, b) {
139
+ if (a.length !== b.length) {
140
+ return false;
141
+ }
142
+ let result = 0;
143
+ for (let i = 0;i < a.length; i++) {
144
+ result |= a[i] ^ b[i];
145
+ }
146
+ return result === 0;
147
+ }
148
+ static secureClear(data) {
149
+ if (data && data.fill) {
150
+ data.fill(0);
151
+ }
152
+ }
153
+ static concatenate(...arrays) {
154
+ const totalLength = arrays.reduce((sum, arr) => sum + arr.length, 0);
155
+ const result = new Uint8Array(totalLength);
156
+ let offset = 0;
157
+ for (const array of arrays) {
158
+ result.set(array, offset);
159
+ offset += array.length;
160
+ }
161
+ return result;
162
+ }
163
+ static generateKeyId() {
164
+ const randomBytes = SecureRandom.getRandomBytes(16);
165
+ return Base64.encode(randomBytes).replace(/[+/=]/g, "").substring(0, 22);
166
+ }
167
+ static async hash(data) {
168
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
169
+ return new Uint8Array(hashBuffer);
170
+ }
171
+ static validateKeySize(key, algorithm, keyType) {
172
+ const config = CRYPTO_CONFIG[algorithm];
173
+ if (!config) {
174
+ throw new CryptoError(`Unknown algorithm: ${algorithm}`);
175
+ }
176
+ const expectedSize = config[`${keyType.toUpperCase()}_KEY_SIZE`];
177
+ if (!expectedSize) {
178
+ throw new CryptoError(`Unknown key type: ${keyType} for algorithm: ${algorithm}`);
179
+ }
180
+ if (key.length !== expectedSize) {
181
+ throw new CryptoError(`Invalid ${keyType} key size for ${algorithm}. Expected ${expectedSize}, got ${key.length}`);
182
+ }
183
+ }
184
+ }
185
+
186
+ // src/lib/crypto/indexed-db-manager.js
187
+ var DB_NAME = "QryptChatKeys";
188
+ var DB_VERSION = 1;
189
+ var STORE_NAME = "keypairs";
190
+
191
+ class IndexedDBManager {
192
+ db = null;
193
+ async openDB() {
194
+ if (this.db) {
195
+ return this.db;
196
+ }
197
+ return new Promise((resolve, reject) => {
198
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
199
+ request.onupgradeneeded = (event) => {
200
+ const db = event.target.result;
201
+ db.createObjectStore(STORE_NAME, { keyPath: "id" });
202
+ };
203
+ request.onsuccess = (event) => {
204
+ this.db = event.target.result;
205
+ resolve(this.db);
206
+ };
207
+ request.onerror = (event) => {
208
+ reject("Error opening IndexedDB: " + event.target.error);
209
+ };
210
+ });
211
+ }
212
+ async set(id, value) {
213
+ const db = await this.openDB();
214
+ return new Promise((resolve, reject) => {
215
+ const transaction = db.transaction([STORE_NAME], "readwrite");
216
+ const store = transaction.objectStore(STORE_NAME);
217
+ const request = store.put({ id, value });
218
+ request.onsuccess = () => {
219
+ resolve(undefined);
220
+ };
221
+ request.onerror = (event) => {
222
+ reject("Error saving to IndexedDB: " + event.target.error);
223
+ };
224
+ });
225
+ }
226
+ async get(id) {
227
+ const db = await this.openDB();
228
+ return new Promise((resolve, reject) => {
229
+ const transaction = db.transaction([STORE_NAME], "readonly");
230
+ const store = transaction.objectStore(STORE_NAME);
231
+ const request = store.get(id);
232
+ request.onsuccess = (event) => {
233
+ const target = event.target;
234
+ resolve(target.result ? target.result.value : null);
235
+ };
236
+ request.onerror = (event) => {
237
+ reject("Error reading from IndexedDB: " + event.target.error);
238
+ };
239
+ });
240
+ }
241
+ async delete(id) {
242
+ const db = await this.openDB();
243
+ return new Promise((resolve, reject) => {
244
+ const transaction = db.transaction([STORE_NAME], "readwrite");
245
+ const store = transaction.objectStore(STORE_NAME);
246
+ const request = store.delete(id);
247
+ request.onsuccess = () => {
248
+ resolve(undefined);
249
+ };
250
+ request.onerror = (event) => {
251
+ reject("Error deleting from IndexedDB: " + event.target.error);
252
+ };
253
+ });
254
+ }
255
+ }
256
+ var indexedDBManager = new IndexedDBManager;
257
+
258
+ // src/lib/crypto/post-quantum-encryption.js
259
+ class PostQuantumEncryptionService {
260
+ constructor() {
261
+ this.userKeys = null;
262
+ this.userKeys768 = null;
263
+ this.publicKeyCache = new Map;
264
+ this.isInitialized = false;
265
+ this.storageKey = "qryptchat_pq_keypair";
266
+ this.storageKey768 = "qryptchat_pq_keypair_768";
267
+ this.kemAlgorithm = new MlKem1024;
268
+ this.kemName = "ML-KEM-1024";
269
+ this.kemAlgorithm768 = new MlKem768;
270
+ this.kemName768 = "ML-KEM-768";
271
+ this.ML_KEM_768_PUBLIC_KEY_SIZE = 1184;
272
+ this.ML_KEM_1024_PUBLIC_KEY_SIZE = 1568;
273
+ }
274
+ async initialize() {
275
+ if (typeof window !== "undefined") {
276
+ await Promise.all([
277
+ this.loadUserKeys(),
278
+ this.loadUserKeys768()
279
+ ]);
280
+ if (!this.userKeys) {
281
+ await this.generateUserKeys();
282
+ }
283
+ if (!this.userKeys768) {
284
+ await this.generateUserKeys768();
285
+ }
286
+ }
287
+ this.isInitialized = true;
288
+ console.log(`\uD83D\uDD10 Post-quantum encryption service initialized (${this.kemName} with ${this.kemName768} backward compatibility)`);
289
+ }
290
+ async getUserKeys() {
291
+ if (this.userKeys) {
292
+ return this.userKeys;
293
+ }
294
+ await this.loadUserKeys();
295
+ if (this.userKeys) {
296
+ return this.userKeys;
297
+ }
298
+ return await this.generateUserKeys();
299
+ }
300
+ async generateUserKeys() {
301
+ try {
302
+ console.log(`\uD83D\uDD10 Generating ${this.kemName} key pair...`);
303
+ const keyPair = await this.kemAlgorithm.generateKeyPair();
304
+ const publicKey = Base64.encode(keyPair[0]);
305
+ const privateKey = Base64.encode(keyPair[1]);
306
+ this.userKeys = { publicKey, privateKey };
307
+ if (typeof window !== "undefined") {
308
+ const keyData = {
309
+ publicKey,
310
+ privateKey,
311
+ algorithm: this.kemName,
312
+ timestamp: Date.now(),
313
+ version: "3.0"
314
+ };
315
+ await indexedDBManager.set(this.storageKey, keyData);
316
+ }
317
+ console.log(`\uD83D\uDD10 Generated new ${this.kemName} key pair`);
318
+ if (typeof window !== "undefined") {
319
+ window.dispatchEvent(new CustomEvent("qryptchat:keys-generated"));
320
+ }
321
+ return this.userKeys;
322
+ } catch (error) {
323
+ console.error("\uD83D\uDD10 Failed to generate post-quantum keys:", error);
324
+ throw error;
325
+ }
326
+ }
327
+ async loadUserKeys() {
328
+ try {
329
+ if (typeof window === "undefined")
330
+ return;
331
+ const keyData = await indexedDBManager.get(this.storageKey);
332
+ if (!keyData)
333
+ return;
334
+ if (keyData.publicKey && keyData.privateKey && keyData.algorithm === this.kemName) {
335
+ this.userKeys = {
336
+ publicKey: keyData.publicKey,
337
+ privateKey: keyData.privateKey
338
+ };
339
+ console.log(`\uD83D\uDD10 Loaded ${this.kemName} keys from storage`);
340
+ } else if (keyData.algorithm && keyData.algorithm !== this.kemName) {
341
+ console.warn(`\uD83D\uDD10 Stored keys use ${keyData.algorithm}, but current algorithm is ${this.kemName}. Keys need regeneration.`);
342
+ }
343
+ } catch (error) {
344
+ console.error("\uD83D\uDD10 Failed to load post-quantum keys:", error);
345
+ }
346
+ }
347
+ async loadUserKeys768() {
348
+ try {
349
+ if (typeof window === "undefined")
350
+ return;
351
+ const keyData = await indexedDBManager.get(this.storageKey768);
352
+ if (!keyData)
353
+ return;
354
+ if (keyData.publicKey && keyData.privateKey && keyData.algorithm === this.kemName768) {
355
+ this.userKeys768 = {
356
+ publicKey: keyData.publicKey,
357
+ privateKey: keyData.privateKey
358
+ };
359
+ console.log(`\uD83D\uDD10 Loaded ${this.kemName768} keys from storage (for backward compatibility)`);
360
+ }
361
+ } catch (error) {
362
+ console.error("\uD83D\uDD10 Failed to load ML-KEM-768 keys:", error);
363
+ }
364
+ }
365
+ async getPublicKey() {
366
+ const keys = await this.getUserKeys();
367
+ return keys.publicKey;
368
+ }
369
+ storePublicKey(userId, publicKey) {
370
+ this.publicKeyCache.set(userId, publicKey);
371
+ console.log(`\uD83D\uDD10 Stored ${this.kemName} public key for user: ${userId}`);
372
+ }
373
+ getStoredPublicKey(userId) {
374
+ return this.publicKeyCache.get(userId) || null;
375
+ }
376
+ async encryptForRecipient(message, recipientPublicKey) {
377
+ try {
378
+ if (!this.isInitialized) {
379
+ throw new Error("Post-quantum encryption service not initialized");
380
+ }
381
+ console.log(`\uD83D\uDD10 Encrypting message using ML-KEM-1024...`);
382
+ if (!recipientPublicKey || typeof recipientPublicKey !== "string") {
383
+ throw new Error("Invalid recipient public key format");
384
+ }
385
+ if (!/^[A-Za-z0-9+/=]+$/.test(recipientPublicKey)) {
386
+ throw new Error("Public key contains invalid Base64 characters");
387
+ }
388
+ let recipientPubKeyBytes;
389
+ try {
390
+ recipientPubKeyBytes = Base64.decode(recipientPublicKey);
391
+ } catch (decodeError) {
392
+ console.error("\uD83D\uDD10 [ERROR] Failed to decode public key from Base64:", decodeError);
393
+ throw new Error("Failed to decode public key");
394
+ }
395
+ recipientPubKeyBytes = this.stripKeyHeaderIfPresent(recipientPubKeyBytes);
396
+ if (recipientPubKeyBytes.length !== this.ML_KEM_1024_PUBLIC_KEY_SIZE && recipientPubKeyBytes.length !== this.ML_KEM_768_PUBLIC_KEY_SIZE) {
397
+ throw new Error(`Invalid recipient public key size: ${recipientPubKeyBytes.length} bytes. ` + `Expected ${this.ML_KEM_1024_PUBLIC_KEY_SIZE} (ML-KEM-1024) or ${this.ML_KEM_768_PUBLIC_KEY_SIZE} (ML-KEM-768). ` + `Recipient may need to use Nuclear Key Reset in Settings.`);
398
+ }
399
+ let kemAlgorithm = this.kemAlgorithm;
400
+ let kemName = this.kemName;
401
+ console.log(`\uD83D\uDD10 [DEBUG] Recipient public key length: ${recipientPubKeyBytes.length} bytes`);
402
+ if (!this.isValidPublicKey(recipientPubKeyBytes)) {
403
+ console.error(`\uD83D\uDD10 [ERROR] Invalid public key format detected (length: ${recipientPubKeyBytes.length} bytes)`);
404
+ throw new Error("Invalid public key format");
405
+ }
406
+ if (recipientPubKeyBytes.length === this.ML_KEM_768_PUBLIC_KEY_SIZE) {
407
+ console.log(`\uD83D\uDD10 [COMPATIBILITY] Detected ML-KEM-768 public key, using ML-KEM-768 for encryption`);
408
+ try {
409
+ const [kem768Ciphertext, kem768SharedSecret] = await this.kemAlgorithm768.encap(recipientPubKeyBytes);
410
+ const salt = SecureRandom.generateSalt();
411
+ const chachaKey = await HKDF.derive(kem768SharedSecret, salt, "ChaCha20-Poly1305", 32);
412
+ const nonce = SecureRandom.generateNonce();
413
+ const plaintext = new TextEncoder().encode(message);
414
+ const messageCiphertext = await ChaCha20Poly1305.encrypt(chachaKey, nonce, plaintext);
415
+ const encryptedMessage = {
416
+ v: 3,
417
+ alg: this.kemName768,
418
+ kem: Base64.encode(kem768Ciphertext),
419
+ s: Base64.encode(salt),
420
+ n: Base64.encode(nonce),
421
+ c: Base64.encode(messageCiphertext),
422
+ t: Date.now()
423
+ };
424
+ CryptoUtils.secureClear(chachaKey);
425
+ CryptoUtils.secureClear(kem768SharedSecret);
426
+ const result = JSON.stringify(encryptedMessage);
427
+ console.log(`\uD83D\uDD10 ✅ Encrypted message using ${this.kemName768}`);
428
+ return result;
429
+ } catch (kem768Error) {
430
+ console.error(`\uD83D\uDD10 ❌ ML-KEM-768 encryption failed:`, kem768Error);
431
+ throw kem768Error;
432
+ }
433
+ } else {
434
+ kemAlgorithm = this.kemAlgorithm;
435
+ kemName = this.kemName;
436
+ }
437
+ try {
438
+ const [kemCiphertext, sharedSecret] = await kemAlgorithm.encap(recipientPubKeyBytes);
439
+ const salt = SecureRandom.generateSalt();
440
+ const chachaKey = await HKDF.derive(sharedSecret, salt, "ChaCha20-Poly1305", 32);
441
+ const nonce = SecureRandom.generateNonce();
442
+ const plaintext = new TextEncoder().encode(message);
443
+ const messageCiphertext = await ChaCha20Poly1305.encrypt(chachaKey, nonce, plaintext);
444
+ const encryptedMessage = {
445
+ v: 3,
446
+ alg: kemName,
447
+ kem: Base64.encode(kemCiphertext),
448
+ s: Base64.encode(salt),
449
+ n: Base64.encode(nonce),
450
+ c: Base64.encode(messageCiphertext),
451
+ t: Date.now()
452
+ };
453
+ CryptoUtils.secureClear(chachaKey);
454
+ CryptoUtils.secureClear(sharedSecret);
455
+ const result = JSON.stringify(encryptedMessage);
456
+ console.log(`\uD83D\uDD10 ✅ Encrypted message using ${kemName}`);
457
+ return result;
458
+ } catch (encapError) {
459
+ console.error(`\uD83D\uDD10 ❌ ML-KEM encapsulation failed:`, encapError);
460
+ const keyInfo = this.analyzePublicKey(recipientPubKeyBytes);
461
+ console.log(`\uD83D\uDD10 [DEBUG] Public key analysis:`, keyInfo);
462
+ throw new Error(`ML-KEM encryption failed: ${encapError.message}`);
463
+ }
464
+ } catch (error) {
465
+ console.error(`\uD83D\uDD10 ❌ All encryption methods failed:`, error);
466
+ throw new Error("Unable to encrypt message with any available method");
467
+ }
468
+ }
469
+ analyzePublicKey(keyBytes) {
470
+ try {
471
+ let zeroCount = 0;
472
+ let nonAsciiCount = 0;
473
+ for (let i = 0;i < keyBytes.length; i++) {
474
+ if (keyBytes[i] === 0)
475
+ zeroCount++;
476
+ if (keyBytes[i] > 127)
477
+ nonAsciiCount++;
478
+ }
479
+ const info = {
480
+ length: keyBytes.length,
481
+ format: keyBytes.length === this.ML_KEM_1024_PUBLIC_KEY_SIZE ? "ML-KEM-1024" : keyBytes.length === this.ML_KEM_768_PUBLIC_KEY_SIZE ? "ML-KEM-768" : "unknown",
482
+ firstBytes: Array.from(keyBytes.slice(0, 8)),
483
+ zeroCount,
484
+ nonAsciiCount,
485
+ validKey: zeroCount < 50 && keyBytes.length > 1000
486
+ };
487
+ return info;
488
+ } catch (e) {
489
+ const errorMessage = e instanceof Error ? e.message : String(e);
490
+ return { error: errorMessage };
491
+ }
492
+ }
493
+ async generateUserKeys768() {
494
+ try {
495
+ console.log(`\uD83D\uDD10 Generating ${this.kemName768} key pair for backward compatibility...`);
496
+ const keyPair = await this.kemAlgorithm768.generateKeyPair();
497
+ const publicKey = Base64.encode(keyPair[0]);
498
+ const privateKey = Base64.encode(keyPair[1]);
499
+ this.userKeys768 = { publicKey, privateKey };
500
+ if (typeof window !== "undefined") {
501
+ const keyData = {
502
+ publicKey,
503
+ privateKey,
504
+ algorithm: this.kemName768,
505
+ timestamp: Date.now(),
506
+ version: "3.0"
507
+ };
508
+ await indexedDBManager.set(this.storageKey768, keyData);
509
+ }
510
+ console.log(`\uD83D\uDD10 Generated new ${this.kemName768} key pair for backward compatibility`);
511
+ return this.userKeys768;
512
+ } catch (error) {
513
+ console.error("\uD83D\uDD10 Failed to generate ML-KEM-768 keys:", error);
514
+ throw error;
515
+ }
516
+ }
517
+ async getUserKeys768() {
518
+ if (this.userKeys768) {
519
+ return this.userKeys768;
520
+ }
521
+ await this.loadUserKeys768();
522
+ if (this.userKeys768) {
523
+ return this.userKeys768;
524
+ }
525
+ return await this.generateUserKeys768();
526
+ }
527
+ async decryptFromSender(encryptedContent, senderPublicKey) {
528
+ try {
529
+ if (!this.isInitialized) {
530
+ throw new Error("Post-quantum encryption service not initialized");
531
+ }
532
+ console.log(`\uD83D\uDD10 [DEBUG] Starting decryption...`);
533
+ console.log(`\uD83D\uDD10 [DEBUG] Encrypted content type:`, typeof encryptedContent);
534
+ console.log(`\uD83D\uDD10 [DEBUG] Encrypted content length:`, encryptedContent?.length || 0);
535
+ console.log(`\uD83D\uDD10 [DEBUG] Encrypted content preview:`, encryptedContent?.substring(0, 100) || "N/A");
536
+ let messageData;
537
+ try {
538
+ messageData = JSON.parse(encryptedContent);
539
+ console.log(`\uD83D\uDD10 [DEBUG] Successfully parsed JSON, algorithm:`, messageData.alg);
540
+ } catch (parseError) {
541
+ const errorMessage = parseError instanceof Error ? parseError.message : String(parseError);
542
+ console.log("\uD83D\uDD10 [DEBUG] Content is not JSON, parse error:", errorMessage);
543
+ console.log("\uD83D\uDD10 [DEBUG] Raw content that failed to parse:", encryptedContent);
544
+ return "[Encrypted message]";
545
+ }
546
+ const version = messageData.v || messageData.version || 0;
547
+ const algorithm = messageData.alg || messageData.algorithm || "";
548
+ const kemCiphertextBase64 = messageData.kem || messageData.kemCiphertext || "";
549
+ const saltBase64 = messageData.s || messageData.salt || "";
550
+ const nonceBase64 = messageData.n || messageData.nonce || "";
551
+ const ciphertextBase64 = messageData.c || messageData.ciphertext || "";
552
+ let decryptionAlgorithm, userKeysToUse;
553
+ if (algorithm === this.kemName) {
554
+ console.log(`\uD83D\uDD10 [DEBUG] Using ${this.kemName} for decryption`);
555
+ decryptionAlgorithm = this.kemAlgorithm;
556
+ userKeysToUse = await this.getUserKeys();
557
+ } else if (algorithm === this.kemName768) {
558
+ console.log(`\uD83D\uDD10 [DEBUG] Using ${this.kemName768} for decryption`);
559
+ decryptionAlgorithm = this.kemAlgorithm768;
560
+ userKeysToUse = await this.getUserKeys768();
561
+ } else if (algorithm === "FALLBACK-AES-GCM" || algorithm === "FALLBACK-AES") {
562
+ console.log("\uD83D\uDD10 [LEGACY] AES encrypted message detected - no longer supported");
563
+ return "[Legacy encrypted message - please delete]";
564
+ } else {
565
+ if (!version || version !== 3 || !kemCiphertextBase64 || !saltBase64 || !nonceBase64 || !ciphertextBase64) {
566
+ console.log("\uD83D\uDD10 [DEBUG] Content is not post-quantum encrypted format, fields:", {
567
+ version,
568
+ algorithm,
569
+ hasKem: !!kemCiphertextBase64,
570
+ hasSalt: !!saltBase64,
571
+ hasNonce: !!nonceBase64,
572
+ hasCiphertext: !!ciphertextBase64
573
+ });
574
+ return "[Encrypted message - format error]";
575
+ }
576
+ console.log(`\uD83D\uDD10 [DEBUG] Unknown algorithm "${algorithm}", trying ML-KEM-1024 first, then ML-KEM-768 if needed`);
577
+ try {
578
+ decryptionAlgorithm = this.kemAlgorithm;
579
+ userKeysToUse = await this.getUserKeys();
580
+ console.log(`\uD83D\uDD10 [DEBUG] Using keys with algorithm ${this.kemName}`);
581
+ console.log(`\uD83D\uDD10 [DEBUG] Public key length:`, userKeysToUse?.publicKey?.length || 0);
582
+ let privateKeyBytes = Base64.decode(userKeysToUse.privateKey);
583
+ const kemCiphertext = Base64.decode(kemCiphertextBase64);
584
+ privateKeyBytes = this.stripKeyHeaderIfPresent(privateKeyBytes);
585
+ console.log(`\uD83D\uDD10 [DEBUG] Decoded private key length:`, privateKeyBytes.length);
586
+ console.log(`\uD83D\uDD10 [DEBUG] Decoded KEM ciphertext length:`, kemCiphertext.length);
587
+ console.log(`\uD83D\uDD10 [DEBUG] Starting ML-KEM decapsulation with algorithm:`, this.kemName);
588
+ const sharedSecret = await decryptionAlgorithm.decap(kemCiphertext, privateKeyBytes);
589
+ console.log(`\uD83D\uDD10 [DEBUG] ML-KEM decapsulation successful, shared secret length:`, sharedSecret.length);
590
+ } catch (err1024) {
591
+ console.warn(`\uD83D\uDD10 [DEBUG] ML-KEM-1024 decryption failed, trying ML-KEM-768. Error:`, err1024);
592
+ try {
593
+ decryptionAlgorithm = this.kemAlgorithm768;
594
+ userKeysToUse = await this.getUserKeys768();
595
+ console.log(`\uD83D\uDD10 [DEBUG] Using keys with algorithm ${this.kemName768}`);
596
+ console.log(`\uD83D\uDD10 [DEBUG] Public key length:`, userKeysToUse?.publicKey?.length || 0);
597
+ const privateKeyBytes = Base64.decode(userKeysToUse.privateKey);
598
+ const kemCiphertext = Base64.decode(kemCiphertextBase64);
599
+ console.log(`\uD83D\uDD10 [DEBUG] Decoded private key length:`, privateKeyBytes.length);
600
+ console.log(`\uD83D\uDD10 [DEBUG] Decoded KEM ciphertext length:`, kemCiphertext.length);
601
+ console.log(`\uD83D\uDD10 [DEBUG] Starting ML-KEM decapsulation with algorithm:`, this.kemName768);
602
+ const sharedSecret = await decryptionAlgorithm.decap(kemCiphertext, privateKeyBytes);
603
+ console.log(`\uD83D\uDD10 [DEBUG] ML-KEM decapsulation successful, shared secret length:`, sharedSecret.length);
604
+ } catch (err768) {
605
+ console.error(`\uD83D\uDD10 [DEBUG] ML-KEM-768 decryption also failed. Error:`, err768);
606
+ return "[Encrypted message - could not decrypt with any supported algorithm]";
607
+ }
608
+ }
609
+ }
610
+ console.log(`\uD83D\uDD10 [DEBUG] Using keys with algorithm ${userKeysToUse ? algorithm === this.kemName768 ? this.kemName768 : this.kemName : "unknown"}`);
611
+ console.log(`\uD83D\uDD10 [DEBUG] Public key length:`, userKeysToUse?.publicKey?.length || 0);
612
+ let privateKeyBytes = Base64.decode(userKeysToUse.privateKey);
613
+ const kemCiphertext = Base64.decode(kemCiphertextBase64);
614
+ privateKeyBytes = this.stripKeyHeaderIfPresent(privateKeyBytes);
615
+ const targetPrivateKeySize = algorithm === this.kemName768 ? 2400 : 3168;
616
+ if (privateKeyBytes.length !== targetPrivateKeySize) {
617
+ throw new Error(`Invalid private key size: expected ${targetPrivateKeySize} bytes, got ${privateKeyBytes.length}. ` + `Please use Nuclear Key Reset in Settings to generate new encryption keys.`);
618
+ }
619
+ console.log(`\uD83D\uDD10 [DEBUG] Decoded private key length:`, privateKeyBytes.length);
620
+ console.log(`\uD83D\uDD10 [DEBUG] Decoded KEM ciphertext length:`, kemCiphertext.length);
621
+ console.log(`\uD83D\uDD10 [DEBUG] Starting ML-KEM decapsulation with algorithm:`, algorithm === this.kemName768 ? this.kemName768 : this.kemName);
622
+ const sharedSecret = await decryptionAlgorithm.decap(kemCiphertext, privateKeyBytes);
623
+ console.log(`\uD83D\uDD10 [DEBUG] ML-KEM decapsulation successful, shared secret length:`, sharedSecret.length);
624
+ const salt = Base64.decode(saltBase64);
625
+ const chachaKey = await HKDF.derive(sharedSecret, salt, "ChaCha20-Poly1305", 32);
626
+ const nonce = Base64.decode(nonceBase64);
627
+ const messageCiphertext = Base64.decode(ciphertextBase64);
628
+ console.log(`\uD83D\uDD10 [DEBUG] Nonce length:`, nonce.length);
629
+ console.log(`\uD83D\uDD10 [DEBUG] Message ciphertext length:`, messageCiphertext.length);
630
+ console.log(`\uD83D\uDD10 [DEBUG] Starting ChaCha20-Poly1305 decryption...`);
631
+ const plaintext = await ChaCha20Poly1305.decrypt(chachaKey, nonce, messageCiphertext);
632
+ console.log(`\uD83D\uDD10 [DEBUG] ChaCha20-Poly1305 decryption successful, plaintext length:`, plaintext.length);
633
+ const messageText = new TextDecoder("utf-8").decode(plaintext);
634
+ CryptoUtils.secureClear(chachaKey);
635
+ CryptoUtils.secureClear(sharedSecret);
636
+ console.log(`\uD83D\uDD10 ✅ Successfully decrypted message using ${algorithm === this.kemName768 ? this.kemName768 : this.kemName}`);
637
+ return messageText;
638
+ } catch (error) {
639
+ console.error(`\uD83D\uDD10 ❌ Failed to decrypt message with ${this.kemName}:`, error);
640
+ if (error instanceof Error && error.stack) {
641
+ console.error(`\uD83D\uDD10 ❌ Error stack:`, error.stack);
642
+ }
643
+ const errorMsg = error instanceof Error ? error.message : String(error);
644
+ if (errorMsg.includes("Algorithm mismatch")) {
645
+ return "[Message encrypted with different keys]";
646
+ }
647
+ return "[Encrypted message - decryption failed]";
648
+ }
649
+ }
650
+ async clearUserKeys() {
651
+ this.userKeys = null;
652
+ this.userKeys768 = null;
653
+ this.publicKeyCache.clear();
654
+ if (typeof window !== "undefined") {
655
+ await Promise.all([
656
+ indexedDBManager.delete(this.storageKey),
657
+ indexedDBManager.delete(this.storageKey768)
658
+ ]);
659
+ }
660
+ console.log(`\uD83D\uDD10 Cleared all encryption keys`);
661
+ }
662
+ async exportUserKeys() {
663
+ const keys1024 = await this.getUserKeys();
664
+ const keys768 = await this.getUserKeys768();
665
+ return {
666
+ keys1024: {
667
+ publicKey: keys1024.publicKey,
668
+ privateKey: keys1024.privateKey,
669
+ algorithm: this.kemName
670
+ },
671
+ keys768: {
672
+ publicKey: keys768.publicKey,
673
+ privateKey: keys768.privateKey,
674
+ algorithm: this.kemName768
675
+ }
676
+ };
677
+ }
678
+ async importUserKeys(publicKey, privateKey, algorithm = this.kemName) {
679
+ if (algorithm === this.kemName) {
680
+ this.userKeys = { publicKey, privateKey };
681
+ if (typeof window !== "undefined") {
682
+ const keyData = {
683
+ publicKey,
684
+ privateKey,
685
+ algorithm,
686
+ timestamp: Date.now(),
687
+ version: "3.0"
688
+ };
689
+ await indexedDBManager.set(this.storageKey, keyData);
690
+ }
691
+ } else if (algorithm === this.kemName768) {
692
+ this.userKeys768 = { publicKey, privateKey };
693
+ if (typeof window !== "undefined") {
694
+ const keyData = {
695
+ publicKey,
696
+ privateKey,
697
+ algorithm,
698
+ timestamp: Date.now(),
699
+ version: "3.0"
700
+ };
701
+ await indexedDBManager.set(this.storageKey768, keyData);
702
+ }
703
+ } else {
704
+ console.warn(`\uD83D\uDD10 Importing keys with unknown algorithm ${algorithm}`);
705
+ }
706
+ console.log(`\uD83D\uDD10 Imported ${algorithm} keys`);
707
+ }
708
+ isValidPublicKey(publicKeyBytes) {
709
+ const is1024Size = Math.abs(publicKeyBytes.length - this.ML_KEM_1024_PUBLIC_KEY_SIZE) <= 32;
710
+ const is768Size = Math.abs(publicKeyBytes.length - this.ML_KEM_768_PUBLIC_KEY_SIZE) <= 32;
711
+ if (!is1024Size && !is768Size) {
712
+ console.warn(`\uD83D\uDD10 [VALIDATE] Public key has unusual length: ${publicKeyBytes.length} bytes`);
713
+ console.warn(`\uD83D\uDD10 [VALIDATE] Expected ~${this.ML_KEM_1024_PUBLIC_KEY_SIZE} or ~${this.ML_KEM_768_PUBLIC_KEY_SIZE} bytes`);
714
+ }
715
+ let zeroCount = 0;
716
+ for (let i = 0;i < Math.min(50, publicKeyBytes.length); i++) {
717
+ if (publicKeyBytes[i] === 0) {
718
+ zeroCount++;
719
+ }
720
+ }
721
+ if (zeroCount > 40) {
722
+ console.error(`\uD83D\uDD10 [VALIDATE] Public key appears to be corrupted (${zeroCount} zeros in header)`);
723
+ return false;
724
+ }
725
+ console.log(`\uD83D\uDD10 [VALIDATE] Valid key detected (${publicKeyBytes.length} bytes, ${is1024Size ? "ML-KEM-1024" : "ML-KEM-768"} format)`);
726
+ return true;
727
+ }
728
+ stripKeyHeaderIfPresent(keyBytes) {
729
+ if (keyBytes.length > 8 && keyBytes[0] === 75 && keyBytes[1] === 89 && keyBytes[2] === 66 && keyBytes[3] === 69 && keyBytes[4] === 82) {
730
+ console.log('\uD83D\uDD10 [CRITICAL] Detected "KYBER" header in key - this key format cannot be repaired');
731
+ console.log("\uD83D\uDD10 [CRITICAL] Please use the Nuclear Key Reset in settings to generate new clean keys");
732
+ return new Uint8Array(this.ML_KEM_1024_PUBLIC_KEY_SIZE);
733
+ }
734
+ return keyBytes;
735
+ }
736
+ getAlgorithmInfo() {
737
+ return {
738
+ primaryAlgorithm: {
739
+ name: this.kemName,
740
+ type: "Post-Quantum KEM + Symmetric Encryption",
741
+ keyExchange: this.kemName,
742
+ encryption: "ChaCha20-Poly1305",
743
+ quantumResistant: true,
744
+ securityLevel: 5
745
+ },
746
+ compatibilityAlgorithm: {
747
+ name: this.kemName768,
748
+ type: "Post-Quantum KEM + Symmetric Encryption",
749
+ keyExchange: this.kemName768,
750
+ encryption: "ChaCha20-Poly1305",
751
+ quantumResistant: true,
752
+ securityLevel: 3
753
+ },
754
+ multiAlgorithmSupport: true
755
+ };
756
+ }
757
+ validatePublicKeyFormat(publicKeyBase64) {
758
+ try {
759
+ if (!publicKeyBase64 || typeof publicKeyBase64 !== "string") {
760
+ console.error("\uD83D\uDD10 [VALIDATE] Public key is null or not a string");
761
+ return null;
762
+ }
763
+ let keyBytes = Base64.decode(publicKeyBase64);
764
+ keyBytes = this.stripKeyHeaderIfPresent(keyBytes);
765
+ if (!this.isValidPublicKey(keyBytes)) {
766
+ console.warn("\uD83D\uDD10 [VALIDATE] Public key failed validation checks");
767
+ return null;
768
+ }
769
+ return keyBytes;
770
+ } catch (error) {
771
+ console.error("\uD83D\uDD10 [VALIDATE] Error validating public key format:", error);
772
+ return null;
773
+ }
774
+ }
775
+ validateKeySize(keyBytes, targetSize, keyType = "key") {
776
+ if (keyBytes.length === targetSize) {
777
+ return keyBytes;
778
+ }
779
+ throw new Error(`Invalid ML-KEM ${keyType} size: expected ${targetSize} bytes, got ${keyBytes.length} bytes. ` + `Please use the Nuclear Key Reset in Settings to generate new encryption keys.`);
780
+ }
781
+ }
782
+ var postQuantumEncryption = new PostQuantumEncryptionService;
783
+
784
+ // src/cli/crypto.js
785
+ var METHODS = ["log", "info", "warn", "error", "debug"];
786
+ async function quietly(fn) {
787
+ const saved = METHODS.map((m) => console[m]);
788
+ for (const m of METHODS)
789
+ console[m] = () => {};
790
+ try {
791
+ return await fn();
792
+ } finally {
793
+ METHODS.forEach((m, i) => console[m] = saved[i]);
794
+ }
795
+ }
796
+ function keyring(keys) {
797
+ const service = new PostQuantumEncryptionService;
798
+ service.userKeys = { publicKey: keys.keys1024.publicKey, privateKey: keys.keys1024.privateKey };
799
+ if (keys.keys768)
800
+ service.userKeys768 = { publicKey: keys.keys768.publicKey, privateKey: keys.keys768.privateKey };
801
+ service.isInitialized = true;
802
+ return {
803
+ encrypt: (text, recipientPublicKey) => quietly(() => service.encryptForRecipient(text, recipientPublicKey)),
804
+ decrypt: (content) => quietly(() => service.decryptFromSender(content, ""))
805
+ };
806
+ }
807
+ async function ephemeralKeypair() {
808
+ const [publicKey, privateKey] = await new MlKem10242().generateKeyPair();
809
+ return { publicKey: Base64.encode(publicKey), privateKey: Base64.encode(privateKey) };
810
+ }
811
+ async function openKeyBlob(blob, ephemeral) {
812
+ const ring = keyring({ keys1024: ephemeral });
813
+ const text = await ring.decrypt(blob);
814
+ let keys;
815
+ try {
816
+ keys = JSON.parse(text);
817
+ } catch {
818
+ throw new Error("Could not open the keys the browser sent. Run qc login again.");
819
+ }
820
+ if (!keys?.keys1024?.publicKey || !keys?.keys1024?.privateKey)
821
+ throw new Error("The browser sent no usable keys.");
822
+ return { keys1024: keys.keys1024, keys768: keys.keys768 };
823
+ }
824
+
825
+ // src/cli/api.js
826
+ class QcError extends Error {
827
+ constructor(message, status) {
828
+ super(message);
829
+ this.name = "QcError";
830
+ this.status = status;
831
+ }
832
+ }
833
+
834
+ class QcClient {
835
+ constructor(session, options = {}) {
836
+ this.session = session;
837
+ this.base = baseUrl(session, options.env);
838
+ this.save = options.save ?? ((s) => saveSession(s, options.env));
839
+ this.fetch = options.fetch ?? globalThis.fetch;
840
+ this.ring = keyring(session.keys);
841
+ this.refreshing = null;
842
+ }
843
+ get me() {
844
+ return this.session.user;
845
+ }
846
+ async refresh() {
847
+ this.refreshing ??= (async () => {
848
+ const res = await this.fetch(`${this.base}/api/cli/token`, {
849
+ method: "POST",
850
+ headers: { "Content-Type": "application/json" },
851
+ body: JSON.stringify({ grant_type: "refresh_token", refresh_token: this.session.refresh_token })
852
+ });
853
+ const body = await res.json().catch(() => ({}));
854
+ if (!res.ok)
855
+ throw new QcError(body.error_description || "Your session has ended. Run qc login.", 401);
856
+ this.session = { ...this.session, ...body };
857
+ this.save(this.session);
858
+ })().finally(() => {
859
+ this.refreshing = null;
860
+ });
861
+ return this.refreshing;
862
+ }
863
+ async token() {
864
+ const expiresAt = Number(this.session.expires_at || 0) * 1000;
865
+ if (expiresAt && expiresAt - Date.now() < 60000)
866
+ await this.refresh();
867
+ return this.session.access_token;
868
+ }
869
+ async request(path, { method = "GET", body, signal, retry = true } = {}) {
870
+ const res = await this.fetch(`${this.base}${path}`, {
871
+ method,
872
+ signal,
873
+ headers: {
874
+ Authorization: `Bearer ${await this.token()}`,
875
+ ...body !== undefined ? { "Content-Type": "application/json" } : {}
876
+ },
877
+ body: body !== undefined ? JSON.stringify(body) : undefined
878
+ });
879
+ if (res.status === 401 && retry) {
880
+ await this.refresh();
881
+ return this.request(path, { method, body, signal, retry: false });
882
+ }
883
+ const data = await res.json().catch(() => ({}));
884
+ if (!res.ok)
885
+ throw new QcError(data.error_description || data.error || `HTTP ${res.status}`, res.status);
886
+ return data;
887
+ }
888
+ async conversations() {
889
+ const { conversations = [] } = await this.request("/api/conversations/load", { method: "POST", body: {} });
890
+ return conversations.map((c) => ({ ...c, title: conversationTitle(c, this.me) }));
891
+ }
892
+ async messages(conversationId, { limit = 100, before } = {}) {
893
+ const { messages = [], hasMore = false } = await this.request("/api/messages/load", {
894
+ method: "POST",
895
+ body: { conversationId, limit, ...before ? { before } : {} }
896
+ });
897
+ const out = [];
898
+ for (const m of messages) {
899
+ let text;
900
+ if (m.message_type === "file" || m.has_attachments) {
901
+ text = m.encrypted_content ? await this.ring.decrypt(m.encrypted_content) : "";
902
+ text = `\uD83D\uDCCE ${text && text !== "[File attachment]" ? `${text} ` : ""}(attachment: open qrypt.chat to download)`;
903
+ } else {
904
+ text = m.encrypted_content ? await this.ring.decrypt(m.encrypted_content) : "";
905
+ }
906
+ out.push({
907
+ id: m.id,
908
+ conversationId: m.conversation_id,
909
+ senderId: m.sender_id,
910
+ sender: m.sender?.display_name || m.sender?.username || "unknown",
911
+ username: m.sender?.username || "",
912
+ mine: m.sender_id === this.me?.id,
913
+ text,
914
+ at: m.created_at
915
+ });
916
+ }
917
+ return { messages: out, hasMore };
918
+ }
919
+ async send(conversationId, text) {
920
+ const { participants = [] } = await this.request(`/api/chat/conversations/${encodeURIComponent(conversationId)}/participants`);
921
+ const userIds = participants.map((p) => p.user_id);
922
+ if (!userIds.length)
923
+ throw new QcError("This conversation has no participants");
924
+ const { public_keys: keys = {} } = await this.request("/api/crypto/public-keys", { method: "POST", body: { user_ids: userIds } });
925
+ const encryptedContents = {};
926
+ const missing = [];
927
+ for (const id of userIds) {
928
+ if (!keys[id]) {
929
+ missing.push(id);
930
+ continue;
931
+ }
932
+ encryptedContents[id] = await this.ring.encrypt(text, keys[id]);
933
+ }
934
+ if (!Object.keys(encryptedContents).length)
935
+ throw new QcError("No participant has a public key yet");
936
+ const { message } = await this.request("/api/messages/send", {
937
+ method: "POST",
938
+ body: { conversationId, encryptedContents, messageType: "text" }
939
+ });
940
+ return { message, skipped: missing.length };
941
+ }
942
+ typing(conversationId, on) {
943
+ return this.request(`/api/typing/${on ? "start" : "stop"}`, { method: "POST", body: { conversationId } }).catch(() => {});
944
+ }
945
+ async events(onEvent, { signal, onStatus = () => {} } = {}) {
946
+ let delay = 1000;
947
+ while (!signal?.aborted) {
948
+ onStatus("connecting");
949
+ try {
950
+ const res = await this.fetch(`${this.base}/api/events`, {
951
+ headers: { Authorization: `Bearer ${await this.token()}`, Accept: "text/event-stream" },
952
+ signal
953
+ });
954
+ if (res.status === 401) {
955
+ await this.refresh();
956
+ continue;
957
+ }
958
+ if (!res.ok || !res.body)
959
+ throw new Error(`HTTP ${res.status}`);
960
+ onStatus("live");
961
+ delay = 1000;
962
+ await readSse(res.body, onEvent);
963
+ } catch (err) {
964
+ if (signal?.aborted)
965
+ break;
966
+ if (err instanceof QcError && err.status === 401)
967
+ throw err;
968
+ }
969
+ if (signal?.aborted)
970
+ break;
971
+ onStatus("offline");
972
+ await sleep(delay, signal);
973
+ delay = Math.min(delay * 2, 30000);
974
+ }
975
+ }
976
+ }
977
+ async function readSse(body, onEvent) {
978
+ const decoder = new TextDecoder;
979
+ let buffer = "";
980
+ for await (const chunk of body) {
981
+ buffer += decoder.decode(chunk, { stream: true });
982
+ let at;
983
+ while ((at = buffer.indexOf(`
984
+
985
+ `)) !== -1) {
986
+ const frame = buffer.slice(0, at);
987
+ buffer = buffer.slice(at + 2);
988
+ let type = "message";
989
+ const data = [];
990
+ for (const line of frame.split(`
991
+ `)) {
992
+ if (line.startsWith("event:"))
993
+ type = line.slice(6).trim();
994
+ else if (line.startsWith("data:"))
995
+ data.push(line.slice(5).trimStart());
996
+ }
997
+ if (!data.length)
998
+ continue;
999
+ let parsed;
1000
+ try {
1001
+ parsed = JSON.parse(data.join(`
1002
+ `));
1003
+ } catch {
1004
+ parsed = data.join(`
1005
+ `);
1006
+ }
1007
+ onEvent({ type, data: parsed?.payload ?? parsed?.data ?? parsed });
1008
+ }
1009
+ }
1010
+ }
1011
+ function sleep(ms, signal) {
1012
+ return new Promise((resolve) => {
1013
+ const t = setTimeout(resolve, ms);
1014
+ signal?.addEventListener("abort", () => {
1015
+ clearTimeout(t);
1016
+ resolve();
1017
+ });
1018
+ });
1019
+ }
1020
+ function conversationTitle(c, me) {
1021
+ if (c.name)
1022
+ return c.name;
1023
+ const others = (c.participants || []).filter((p) => p.user_id !== me?.id).map((p) => p.user?.display_name || p.user?.username).filter(Boolean);
1024
+ return others.length ? others.join(", ") : "Just you";
1025
+ }
1026
+
1027
+ // src/cli/login.js
1028
+ import { createHash, randomBytes } from "node:crypto";
1029
+ import { createServer } from "node:http";
1030
+ import { hostname } from "node:os";
1031
+ import { spawn } from "node:child_process";
1032
+ import { createInterface } from "node:readline/promises";
1033
+
1034
+ // src/lib/auth/cli-match.js
1035
+ var ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
1036
+ async function matchCode(challenge, kemPublicKey) {
1037
+ const bytes = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(`qc-login
1038
+ ${challenge}
1039
+ ${kemPublicKey}`)));
1040
+ let out = "";
1041
+ for (let i = 0;i < 8; i++)
1042
+ out += ALPHABET[bytes[i] % ALPHABET.length];
1043
+ return `${out.slice(0, 4)}-${out.slice(4)}`;
1044
+ }
1045
+
1046
+ // src/cli/login.js
1047
+ var b64url = (buf) => Buffer.from(buf).toString("base64url");
1048
+ var TIMEOUT_MS = 5 * 60 * 1000;
1049
+ var PAGE = (title, body) => `<!doctype html><meta charset="utf-8"><title>${title}</title>
1050
+ <style>body{font:16px system-ui,sans-serif;display:grid;place-items:center;min-height:90vh;background:#0b0d12;color:#e6e8ee}main{max-width:28rem;text-align:center}</style>
1051
+ <main><h1>${title}</h1><p>${body}</p></main>`;
1052
+ function openBrowser(url, { platform = process.platform, env = process.env } = {}) {
1053
+ if (env.QC_NO_BROWSER || (env.SSH_CONNECTION || env.SSH_TTY) && !env.DISPLAY && !env.WAYLAND_DISPLAY && platform === "linux") {
1054
+ return false;
1055
+ }
1056
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
1057
+ try {
1058
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
1059
+ child.on("error", () => {});
1060
+ child.unref();
1061
+ return true;
1062
+ } catch {
1063
+ return false;
1064
+ }
1065
+ }
1066
+ function waitForCallback(server, state) {
1067
+ return new Promise((resolve, reject) => {
1068
+ const timer = setTimeout(() => reject(new Error("Timed out waiting for the browser (5 minutes). Run qc login again.")), TIMEOUT_MS);
1069
+ server.on("request", (req, res) => {
1070
+ const url = new URL(req.url, "http://127.0.0.1");
1071
+ if (url.pathname !== "/callback") {
1072
+ res.writeHead(404).end();
1073
+ return;
1074
+ }
1075
+ const done = (status, title, body) => {
1076
+ res.writeHead(status, { "Content-Type": "text/html; charset=utf-8" }).end(PAGE(title, body));
1077
+ };
1078
+ if (url.searchParams.get("state") !== state) {
1079
+ done(400, "That was not this login", "The state did not match. Go back to your terminal.");
1080
+ return;
1081
+ }
1082
+ clearTimeout(timer);
1083
+ const error = url.searchParams.get("error");
1084
+ if (error) {
1085
+ done(200, "Not approved", "Nothing was shared. You can close this tab.");
1086
+ reject(new Error(error === "access_denied" ? "Login was denied in the browser." : `Login failed: ${error}`));
1087
+ return;
1088
+ }
1089
+ done(200, "Signed in", "You can close this tab and go back to your terminal.");
1090
+ resolve(url.searchParams.get("code"));
1091
+ });
1092
+ });
1093
+ }
1094
+ async function login(options = {}) {
1095
+ const env = options.env ?? process.env;
1096
+ const print = options.print ?? ((line) => process.stderr.write(`${line}
1097
+ `));
1098
+ const base = (options.base ?? baseUrl(null, env)).replace(/\/+$/, "");
1099
+ const verifier = b64url(randomBytes(32));
1100
+ const challenge = b64url(createHash("sha256").update(verifier).digest());
1101
+ const state = b64url(randomBytes(16));
1102
+ const ephemeral = await ephemeralKeypair();
1103
+ let server;
1104
+ let redirectUri = "oob";
1105
+ if (!options.oob) {
1106
+ server = createServer();
1107
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
1108
+ redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1109
+ }
1110
+ const url = new URL("/cli/authorize", base);
1111
+ for (const [k, v] of Object.entries({
1112
+ response_type: "code",
1113
+ code_challenge: challenge,
1114
+ code_challenge_method: "S256",
1115
+ redirect_uri: redirectUri,
1116
+ state,
1117
+ client_name: `qc on ${hostname()}`,
1118
+ kem: ephemeral.publicKey
1119
+ })) {
1120
+ url.searchParams.set(k, v);
1121
+ }
1122
+ const match = await matchCode(challenge, ephemeral.publicKey);
1123
+ print("");
1124
+ print(` Confirmation code: ${match}`);
1125
+ print(" Check that the browser shows the same code before you approve.");
1126
+ print("");
1127
+ const opened = !options.oob && openBrowser(url.toString(), { env });
1128
+ print(opened ? " Opened your browser. If it did not, open:" : " Open this link in a browser where you are signed in to qrypt.chat:");
1129
+ print(` ${url.toString()}`);
1130
+ print("");
1131
+ let code;
1132
+ try {
1133
+ if (server) {
1134
+ code = await waitForCallback(server, state);
1135
+ } else {
1136
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
1137
+ code = (await rl.question(" Paste the code from the browser: ")).trim();
1138
+ rl.close();
1139
+ }
1140
+ } finally {
1141
+ server?.close();
1142
+ }
1143
+ if (!code)
1144
+ throw new Error("No code came back from the browser.");
1145
+ const res = await fetch(`${base}/api/cli/token`, {
1146
+ method: "POST",
1147
+ headers: { "Content-Type": "application/json" },
1148
+ body: JSON.stringify({ grant_type: "authorization_code", code, code_verifier: verifier, redirect_uri: redirectUri })
1149
+ });
1150
+ const token = await res.json().catch(() => ({}));
1151
+ if (!res.ok)
1152
+ throw new Error(token.error_description || token.error || `Sign-in failed (HTTP ${res.status})`);
1153
+ const keys = await openKeyBlob(token.key_blob, ephemeral);
1154
+ const session = {
1155
+ base,
1156
+ access_token: token.access_token,
1157
+ refresh_token: token.refresh_token,
1158
+ expires_at: token.expires_at,
1159
+ user: token.user,
1160
+ keys,
1161
+ created_at: new Date().toISOString()
1162
+ };
1163
+ saveSession(session, env);
1164
+ return session;
1165
+ }
1166
+
1167
+ // src/cli/commands.js
1168
+ var HELP2 = `qc: qrypt.chat in your terminal (end-to-end encrypted, ML-KEM-1024)
1169
+
1170
+ Usage:
1171
+ qc open the chat client (signs you in first if needed)
1172
+ qc login [--oob] sign in through your browser; --oob to paste a code (SSH)
1173
+ qc logout forget this terminal's session and keys
1174
+ qc whoami who this terminal is signed in as
1175
+ qc chats list your chats
1176
+ qc read <chat> [-n 20] print the last messages of a chat
1177
+ qc send <chat> <text> send a message (text "-" reads stdin)
1178
+ qc listen print new messages as they arrive (NDJSON with --json)
1179
+ qc mcp run as an MCP server on stdio (list_chats, read_chat, send_message)
1180
+
1181
+ <chat> is a chat id or part of its name. Options: --json, --url <server> (or QC_URL).
1182
+ Keys and session live in ${"$"}QC_HOME or ~/.config/qc (mode 0600).`;
1183
+ function parseArgs2(argv) {
1184
+ const args = { _: [], flags: {} };
1185
+ for (let i = 0;i < argv.length; i++) {
1186
+ const a = argv[i];
1187
+ if (a === "--") {
1188
+ args._.push(...argv.slice(i + 1));
1189
+ break;
1190
+ }
1191
+ if (a.startsWith("--")) {
1192
+ const [k, v] = a.slice(2).split("=", 2);
1193
+ if (v !== undefined)
1194
+ args.flags[k] = v;
1195
+ else if (["url", "n", "limit"].includes(k) && argv[i + 1] !== undefined)
1196
+ args.flags[k] = argv[++i];
1197
+ else
1198
+ args.flags[k] = true;
1199
+ } else if (a === "-n" && argv[i + 1] !== undefined) {
1200
+ args.flags.n = argv[++i];
1201
+ } else if (a === "-h") {
1202
+ args.flags.help = true;
1203
+ } else if (a === "-v") {
1204
+ args.flags.version = true;
1205
+ } else {
1206
+ args._.push(a);
1207
+ }
1208
+ }
1209
+ return args;
1210
+ }
1211
+ function findChat2(chats, query) {
1212
+ const q = String(query).toLowerCase();
1213
+ const byId = chats.find((c) => c.id === query);
1214
+ if (byId)
1215
+ return byId;
1216
+ const exact = chats.filter((c) => c.title.toLowerCase() === q);
1217
+ if (exact.length === 1)
1218
+ return exact[0];
1219
+ const some = chats.filter((c) => c.title.toLowerCase().includes(q));
1220
+ if (some.length === 1)
1221
+ return some[0];
1222
+ if (some.length > 1)
1223
+ throw new Error(`"${query}" matches ${some.length} chats: ${some.map((c) => c.title).join(", ")}`);
1224
+ throw new Error(`No chat matches "${query}". Try qc chats.`);
1225
+ }
1226
+ async function client(flags, { interactive = false } = {}) {
1227
+ if (flags.url)
1228
+ process.env.QC_URL = flags.url;
1229
+ let session = loadSession();
1230
+ if (!session) {
1231
+ if (!interactive)
1232
+ throw new Error("Not signed in. Run qc login.");
1233
+ session = await login({ oob: !!flags.oob });
1234
+ }
1235
+ return new QcClient(session);
1236
+ }
1237
+ var stamp = (iso) => new Date(iso).toLocaleString(undefined, { dateStyle: "short", timeStyle: "short" });
1238
+ async function readStdin() {
1239
+ let text = "";
1240
+ for await (const chunk of process.stdin)
1241
+ text += chunk;
1242
+ return text;
1243
+ }
1244
+ async function main2(argv, { version = "0.0.0" } = {}) {
1245
+ const { _: [cmd, ...rest], flags } = parseArgs2(argv);
1246
+ const out = (line) => process.stdout.write(`${line}
1247
+ `);
1248
+ const json = (value) => out(JSON.stringify(value, null, flags.json === "compact" ? 0 : 2));
1249
+ if (flags.version)
1250
+ return out(`qc ${version}`);
1251
+ if (flags.help || cmd === "help")
1252
+ return out(HELP2);
1253
+ if (flags.url)
1254
+ process.env.QC_URL = flags.url;
1255
+ switch (cmd) {
1256
+ case undefined:
1257
+ case "tui": {
1258
+ if (!process.stdout.isTTY)
1259
+ throw new Error("qc needs a terminal. For scripts use qc chats / read / send / listen.");
1260
+ const c = await client(flags, { interactive: true });
1261
+ const { runTui } = await import("./tui.js");
1262
+ return runTui(c, { initialChat: rest[0] });
1263
+ }
1264
+ case "login": {
1265
+ const session = await login({ oob: !!flags.oob });
1266
+ return out(`Signed in as @${session.user?.username ?? "unknown"} on ${session.base}. Keys saved to ${sessionPath()}.`);
1267
+ }
1268
+ case "logout":
1269
+ clearSession();
1270
+ return out("Signed out. This terminal no longer holds your keys.");
1271
+ case "whoami": {
1272
+ const session = loadSession();
1273
+ if (!session)
1274
+ throw new Error("Not signed in. Run qc login.");
1275
+ return flags.json ? json({ user: session.user, base: session.base }) : out(`@${session.user?.username} (${session.user?.display_name ?? ""}) on ${session.base}`);
1276
+ }
1277
+ case "chats":
1278
+ case "ls": {
1279
+ const chats = await (await client(flags)).conversations();
1280
+ if (flags.json)
1281
+ return json(chats.map((c) => ({ id: c.id, title: c.title, type: c.type, updated_at: c.updated_at, participants: c.participants?.length ?? 0 })));
1282
+ for (const c of chats)
1283
+ out(`${c.id} ${c.title}`);
1284
+ return;
1285
+ }
1286
+ case "read": {
1287
+ if (!rest[0])
1288
+ throw new Error("Usage: qc read <chat> [-n 20]");
1289
+ const c = await client(flags);
1290
+ const chat = findChat2(await c.conversations(), rest[0]);
1291
+ const { messages } = await c.messages(chat.id, { limit: 100 });
1292
+ const last = messages.slice(-Math.max(1, Number(flags.n || flags.limit || 20)));
1293
+ if (flags.json)
1294
+ return json(last);
1295
+ for (const m of last)
1296
+ out(`[${stamp(m.at)}] ${m.mine ? "you" : m.sender}: ${m.text}`);
1297
+ return;
1298
+ }
1299
+ case "send": {
1300
+ if (!rest[0] || rest.length < 2)
1301
+ throw new Error('Usage: qc send <chat> <text> (text "-" reads stdin)');
1302
+ const text = rest[1] === "-" && rest.length === 2 ? (await readStdin()).trim() : rest.slice(1).join(" ");
1303
+ if (!text)
1304
+ throw new Error("Nothing to send.");
1305
+ const c = await client(flags);
1306
+ const chat = findChat2(await c.conversations(), rest[0]);
1307
+ const { message, skipped } = await c.send(chat.id, text);
1308
+ if (flags.json)
1309
+ return json({ id: message?.id, conversation: chat.id, skipped });
1310
+ return out(`Sent to ${chat.title}${skipped ? ` (${skipped} participant(s) have no key yet)` : ""}.`);
1311
+ }
1312
+ case "listen": {
1313
+ const c = await client(flags);
1314
+ const chats = await c.conversations();
1315
+ const titles = new Map(chats.map((x) => [x.id, x.title]));
1316
+ for (const x of chats)
1317
+ await c.messages(x.id, { limit: 1 }).catch(() => {});
1318
+ const seen = new Set;
1319
+ await c.events(async ({ type, data }) => {
1320
+ if (type !== "NEW_MESSAGE" || !data?.message?.conversation_id)
1321
+ return;
1322
+ const id = data.message.conversation_id;
1323
+ const { messages } = await c.messages(id, { limit: 100 });
1324
+ for (const m of messages.slice(-5)) {
1325
+ if (seen.has(m.id) || m.id !== data.message.id)
1326
+ continue;
1327
+ seen.add(m.id);
1328
+ if (flags.json)
1329
+ out(JSON.stringify({ chat: id, title: titles.get(id), ...m }));
1330
+ else
1331
+ out(`[${stamp(m.at)}] ${titles.get(id) ?? id} · ${m.mine ? "you" : m.sender}: ${m.text}`);
1332
+ }
1333
+ }, { onStatus: (s) => process.stderr.write(`qc: ${s}
1334
+ `) });
1335
+ return;
1336
+ }
1337
+ case "mcp": {
1338
+ const { serveMcp } = await import("./mcp.js");
1339
+ return serveMcp(await client(flags), { version });
1340
+ }
1341
+ default:
1342
+ throw new Error(`Unknown command "${cmd}".
1343
+
1344
+ ${HELP2}`);
1345
+ }
1346
+ }
1347
+
1348
+ export { HELP2, parseArgs2, findChat2, main2 };