@omnituum/pqc-shared 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/crypto/index.cjs +39 -71
  2. package/dist/crypto/index.d.cts +48 -45
  3. package/dist/crypto/index.d.ts +48 -45
  4. package/dist/crypto/index.js +33 -72
  5. package/dist/crypto/safe/index.cjs +556 -0
  6. package/dist/crypto/safe/index.d.cts +341 -0
  7. package/dist/crypto/safe/index.d.ts +341 -0
  8. package/dist/crypto/safe/index.js +524 -0
  9. package/dist/fs/index.cjs +6 -34
  10. package/dist/fs/index.js +6 -34
  11. package/dist/index.cjs +39 -71
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +33 -72
  15. package/dist/{integrity-Dx9jukMH.d.cts → integrity-BenoFsmP.d.cts} +1 -1
  16. package/dist/{integrity-CCYjrap3.d.ts → integrity-D9J98Bty.d.ts} +1 -1
  17. package/dist/{types-61c7Q9ri.d.ts → types-CRka8PC7.d.cts} +54 -2
  18. package/dist/{types-Ch0y-n7K.d.cts → types-CRka8PC7.d.ts} +54 -2
  19. package/dist/utils/index.d.cts +2 -3
  20. package/dist/utils/index.d.ts +2 -3
  21. package/dist/vault/index.cjs +13 -41
  22. package/dist/vault/index.d.cts +2 -3
  23. package/dist/vault/index.d.ts +2 -3
  24. package/dist/vault/index.js +13 -41
  25. package/package.json +13 -5
  26. package/src/crypto/dilithium.ts +8 -4
  27. package/src/crypto/hybrid.ts +13 -29
  28. package/src/crypto/index.ts +9 -1
  29. package/src/crypto/kyber.ts +80 -118
  30. package/src/crypto/safe/adapters.ts +306 -0
  31. package/src/crypto/safe/dilithium.ts +74 -0
  32. package/src/crypto/safe/index.ts +97 -0
  33. package/src/crypto/safe/kyber.ts +47 -0
  34. package/src/crypto/safe/manifests.ts +192 -0
  35. package/src/crypto/safe/secretbox.ts +24 -0
  36. package/src/crypto/safe/types.ts +88 -0
  37. package/src/crypto/safe/x25519.ts +31 -0
  38. package/src/index.ts +12 -4
  39. package/src/version.ts +9 -4
  40. package/dist/version-BygzPVGs.d.cts +0 -55
  41. package/dist/version-BygzPVGs.d.ts +0 -55
@@ -2,8 +2,10 @@
2
2
 
3
3
  require('@noble/hashes/sha2.js');
4
4
  require('@noble/hashes/hmac.js');
5
+ var envelopeRegistry = require('@omnituum/envelope-registry');
5
6
  var hashWasm = require('hash-wasm');
6
7
  var nacl = require('tweetnacl');
8
+ var mlKem_js = require('@noble/post-quantum/ml-kem.js');
7
9
  require('@noble/hashes/blake3.js');
8
10
  require('@noble/ciphers/chacha.js');
9
11
  require('@noble/hashes/hkdf.js');
@@ -39,9 +41,12 @@ function fromB64(str) {
39
41
  function toHex(bytes) {
40
42
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
41
43
  }
44
+ function randN(n) {
45
+ return globalThis.crypto.getRandomValues(new Uint8Array(n));
46
+ }
42
47
  var b64 = toB64;
43
-
44
- // src/version.ts
48
+ envelopeRegistry.OMNI_VERSIONS.HYBRID_V1;
49
+ envelopeRegistry.DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
45
50
  var VAULT_VERSION = "omnituum.vault.v1";
46
51
  var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
47
52
  var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
@@ -364,46 +369,13 @@ function generateX25519Keypair() {
364
369
  secretBytes: kp.secretKey
365
370
  };
366
371
  }
367
- var kyberModule = null;
368
- async function loadKyber() {
369
- if (kyberModule) return kyberModule;
370
- try {
371
- const m = await import('kyber-crystals');
372
- const k = m.default ?? m;
373
- kyberModule = k.kyber ?? k;
374
- return kyberModule;
375
- } catch (e) {
376
- console.warn("[Kyber] Failed to load kyber-crystals:", e);
377
- return null;
378
- }
379
- }
380
372
  async function generateKyberKeypair() {
381
- try {
382
- const mod = await loadKyber();
383
- if (!mod) return null;
384
- if (mod?.ready?.then) {
385
- await mod.ready;
386
- }
387
- const fn = mod?.keypair ?? mod?.keyPair ?? mod?.generateKeyPair ?? null;
388
- if (typeof fn !== "function") {
389
- console.warn("[Kyber] No keypair function found");
390
- return null;
391
- }
392
- const kp = await fn.call(mod);
393
- const pub = kp?.publicKey ?? kp?.public ?? kp?.pk;
394
- const priv = kp?.privateKey ?? kp?.secretKey ?? kp?.secret ?? kp?.sk;
395
- if (!pub || !priv) {
396
- console.warn("[Kyber] Invalid keypair result");
397
- return null;
398
- }
399
- return {
400
- publicB64: b64(new Uint8Array(pub)),
401
- secretB64: b64(new Uint8Array(priv))
402
- };
403
- } catch (e) {
404
- console.warn("[Kyber] Key generation failed:", e);
405
- return null;
406
- }
373
+ const seed = randN(64);
374
+ const kp = mlKem_js.ml_kem1024.keygen(seed);
375
+ return {
376
+ publicB64: b64(kp.publicKey),
377
+ secretB64: b64(kp.secretKey)
378
+ };
407
379
  }
408
380
 
409
381
  // src/vault/manager.ts
@@ -1,6 +1,5 @@
1
- import { O as OmnituumVault, E as EncryptedVaultFile, a as EncryptedVaultFileV2, H as HybridIdentityRecord, V as VaultSettings, b as VaultSession } from '../types-Ch0y-n7K.cjs';
2
- export { D as DEFAULT_VAULT_SETTINGS, c as EncryptedVaultFileV1, d as HealthStatus, I as IdentityHealth, P as PBKDF2_ITERATIONS } from '../types-Ch0y-n7K.cjs';
3
- import '../version-BygzPVGs.cjs';
1
+ import { O as OmnituumVault, E as EncryptedVaultFile, k as EncryptedVaultFileV2, H as HybridIdentityRecord, l as VaultSettings, m as VaultSession } from '../types-CRka8PC7.cjs';
2
+ export { D as DEFAULT_VAULT_SETTINGS, n as EncryptedVaultFileV1, o as HealthStatus, I as IdentityHealth, P as PBKDF2_ITERATIONS } from '../types-CRka8PC7.cjs';
4
3
 
5
4
  /**
6
5
  * Omnituum PQC Shared - Vault Encryption
@@ -1,6 +1,5 @@
1
- import { O as OmnituumVault, E as EncryptedVaultFile, a as EncryptedVaultFileV2, H as HybridIdentityRecord, V as VaultSettings, b as VaultSession } from '../types-61c7Q9ri.js';
2
- export { D as DEFAULT_VAULT_SETTINGS, c as EncryptedVaultFileV1, d as HealthStatus, I as IdentityHealth, P as PBKDF2_ITERATIONS } from '../types-61c7Q9ri.js';
3
- import '../version-BygzPVGs.js';
1
+ import { O as OmnituumVault, E as EncryptedVaultFile, k as EncryptedVaultFileV2, H as HybridIdentityRecord, l as VaultSettings, m as VaultSession } from '../types-CRka8PC7.js';
2
+ export { D as DEFAULT_VAULT_SETTINGS, n as EncryptedVaultFileV1, o as HealthStatus, I as IdentityHealth, P as PBKDF2_ITERATIONS } from '../types-CRka8PC7.js';
4
3
 
5
4
  /**
6
5
  * Omnituum PQC Shared - Vault Encryption
@@ -1,7 +1,9 @@
1
1
  import '@noble/hashes/sha2.js';
2
2
  import '@noble/hashes/hmac.js';
3
+ import { OMNI_VERSIONS, DEPRECATED_VERSIONS } from '@omnituum/envelope-registry';
3
4
  import { argon2id } from 'hash-wasm';
4
5
  import nacl from 'tweetnacl';
6
+ import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js';
5
7
  import '@noble/hashes/blake3.js';
6
8
  import '@noble/ciphers/chacha.js';
7
9
  import '@noble/hashes/hkdf.js';
@@ -33,9 +35,12 @@ function fromB64(str) {
33
35
  function toHex(bytes) {
34
36
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
35
37
  }
38
+ function randN(n) {
39
+ return globalThis.crypto.getRandomValues(new Uint8Array(n));
40
+ }
36
41
  var b64 = toB64;
37
-
38
- // src/version.ts
42
+ OMNI_VERSIONS.HYBRID_V1;
43
+ DEPRECATED_VERSIONS.PQC_DEMO_HYBRID_V1;
39
44
  var VAULT_VERSION = "omnituum.vault.v1";
40
45
  var VAULT_ENCRYPTED_VERSION = "omnituum.vault.enc.v1";
41
46
  var VAULT_ENCRYPTED_VERSION_V2 = "omnituum.vault.enc.v2";
@@ -358,46 +363,13 @@ function generateX25519Keypair() {
358
363
  secretBytes: kp.secretKey
359
364
  };
360
365
  }
361
- var kyberModule = null;
362
- async function loadKyber() {
363
- if (kyberModule) return kyberModule;
364
- try {
365
- const m = await import('kyber-crystals');
366
- const k = m.default ?? m;
367
- kyberModule = k.kyber ?? k;
368
- return kyberModule;
369
- } catch (e) {
370
- console.warn("[Kyber] Failed to load kyber-crystals:", e);
371
- return null;
372
- }
373
- }
374
366
  async function generateKyberKeypair() {
375
- try {
376
- const mod = await loadKyber();
377
- if (!mod) return null;
378
- if (mod?.ready?.then) {
379
- await mod.ready;
380
- }
381
- const fn = mod?.keypair ?? mod?.keyPair ?? mod?.generateKeyPair ?? null;
382
- if (typeof fn !== "function") {
383
- console.warn("[Kyber] No keypair function found");
384
- return null;
385
- }
386
- const kp = await fn.call(mod);
387
- const pub = kp?.publicKey ?? kp?.public ?? kp?.pk;
388
- const priv = kp?.privateKey ?? kp?.secretKey ?? kp?.secret ?? kp?.sk;
389
- if (!pub || !priv) {
390
- console.warn("[Kyber] Invalid keypair result");
391
- return null;
392
- }
393
- return {
394
- publicB64: b64(new Uint8Array(pub)),
395
- secretB64: b64(new Uint8Array(priv))
396
- };
397
- } catch (e) {
398
- console.warn("[Kyber] Key generation failed:", e);
399
- return null;
400
- }
367
+ const seed = randN(64);
368
+ const kp = ml_kem1024.keygen(seed);
369
+ return {
370
+ publicB64: b64(kp.publicKey),
371
+ secretB64: b64(kp.secretKey)
372
+ };
401
373
  }
402
374
 
403
375
  // src/vault/manager.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnituum/pqc-shared",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Omnituum PQC Shared Library - Crypto, Vault, File Encryption, and Utilities",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -31,6 +31,11 @@
31
31
  "types": "./dist/fs/index.d.ts",
32
32
  "import": "./dist/fs/index.js",
33
33
  "require": "./dist/fs/index.cjs"
34
+ },
35
+ "./safe": {
36
+ "types": "./dist/crypto/safe/index.d.ts",
37
+ "import": "./dist/crypto/safe/index.js",
38
+ "require": "./dist/crypto/safe/index.cjs"
34
39
  }
35
40
  },
36
41
  "files": [
@@ -40,16 +45,17 @@
40
45
  "dependencies": {
41
46
  "@noble/ciphers": "~2.0.0",
42
47
  "@noble/hashes": "~2.0.0",
48
+ "@omnituum/envelope-registry": "github:Omnituum/envelope-registry#v0.1.2",
43
49
  "@noble/post-quantum": "~0.5.4",
44
50
  "hash-wasm": "^4.11.0",
45
- "kyber-crystals": "^1.0.7",
46
51
  "tweetnacl": "^1.0.3"
47
52
  },
48
53
  "devDependencies": {
49
54
  "@types/node": "^25.0.9",
50
55
  "tsup": "^8.5.0",
51
56
  "tsx": "^4.19.0",
52
- "typescript": "^5.9.3"
57
+ "typescript": "^5.9.3",
58
+ "vitest": "^3.1.0"
53
59
  },
54
60
  "keywords": [
55
61
  "pqc",
@@ -80,7 +86,9 @@
80
86
  "typecheck": "tsc --noEmit",
81
87
  "test:golden:generate": "pnpm -s build && tsx tests/golden/generate-vectors.ts",
82
88
  "test:golden:verify": "pnpm -s build && tsx tests/golden/verify-vectors.ts",
83
- "test:golden": "npm run test:golden:verify",
84
- "pretest": "npm run build"
89
+ "test:golden": "pnpm test:golden:verify",
90
+ "test": "vitest run tests/",
91
+ "test:watch": "vitest tests/",
92
+ "pretest": "pnpm build"
85
93
  }
86
94
  }
@@ -140,7 +140,8 @@ export async function dilithiumSign(
140
140
  const sk = fromB64(secretKeyB64);
141
141
  const msg = typeof message === 'string' ? new TextEncoder().encode(message) : message;
142
142
 
143
- const signature = mod.sign(sk, msg);
143
+ // noble API: sign(message, secretKey)
144
+ const signature = mod.sign(msg, sk);
144
145
 
145
146
  return {
146
147
  signature: toB64(signature),
@@ -160,7 +161,8 @@ export async function dilithiumSignRaw(
160
161
  throw new Error('Dilithium library not available');
161
162
  }
162
163
 
163
- return mod.sign(secretKey, message);
164
+ // noble API: sign(message, secretKey)
165
+ return mod.sign(message, secretKey);
164
166
  }
165
167
 
166
168
  // ═══════════════════════════════════════════════════════════════════════════
@@ -190,7 +192,8 @@ export async function dilithiumVerify(
190
192
  const msg = typeof message === 'string' ? new TextEncoder().encode(message) : message;
191
193
 
192
194
  try {
193
- return mod.verify(pk, msg, sig);
195
+ // noble API: verify(signature, message, publicKey)
196
+ return mod.verify(sig, msg, pk);
194
197
  } catch {
195
198
  return false;
196
199
  }
@@ -210,7 +213,8 @@ export async function dilithiumVerifyRaw(
210
213
  }
211
214
 
212
215
  try {
213
- return mod.verify(publicKey, message, signature);
216
+ // noble API: verify(signature, message, publicKey)
217
+ return mod.verify(signature, message, publicKey);
214
218
  } catch {
215
219
  return false;
216
220
  }
@@ -75,38 +75,22 @@ export interface HybridSecretKeys {
75
75
  kyberSecB64: string;
76
76
  }
77
77
 
78
- export interface HybridEnvelope {
79
- /** Version identifier (FROZEN - see pqc-docs/specs/envelope.v1.md) */
80
- v: typeof ENVELOPE_VERSION;
81
- /** Algorithm suite */
82
- suite: typeof ENVELOPE_SUITE;
83
- /** AEAD algorithm */
84
- aead: typeof ENVELOPE_AEAD;
85
- /** X25519 ephemeral public key (hex) */
86
- x25519Epk: string;
87
- /** X25519 wrapped content key */
88
- x25519Wrap: {
89
- nonce: string;
90
- wrapped: string;
91
- };
92
- /** Kyber KEM ciphertext (base64) */
93
- kyberKemCt: string;
94
- /** Kyber wrapped content key */
95
- kyberWrap: {
96
- nonce: string;
97
- wrapped: string;
98
- };
99
- /** Content encryption nonce (base64) */
100
- contentNonce: string;
101
- /** Encrypted content (base64) */
102
- ciphertext: string;
103
- /** Metadata */
104
- meta: {
105
- createdAt: string;
78
+ /**
79
+ * HybridEnvelope -- OmniHybridV1 from the registry with
80
+ * app-semantic meta fields (senderName, senderId).
81
+ * The Omni registry type defines only the crypto-relevant surface.
82
+ *
83
+ * Intentionally a type alias (not interface extends) to discourage
84
+ * further field additions here. New app-semantic fields belong in
85
+ * product-level types, not in the shared crypto layer.
86
+ */
87
+ import type { OmniHybridV1 } from '@omnituum/envelope-registry';
88
+ export type HybridEnvelope = Omit<OmniHybridV1, 'meta'> & {
89
+ meta: OmniHybridV1['meta'] & {
106
90
  senderName?: string;
107
91
  senderId?: string;
108
92
  };
109
- }
93
+ };
110
94
 
111
95
  // ═══════════════════════════════════════════════════════════════════════════
112
96
  // HELPERS
@@ -71,7 +71,7 @@ export {
71
71
  } from './x25519';
72
72
 
73
73
  // ═══════════════════════════════════════════════════════════════════════════
74
- // KYBER ML-KEM-768 (Post-Quantum KEM)
74
+ // KYBER ML-KEM-1024 (FIPS 203 — Post-Quantum KEM)
75
75
  // ═══════════════════════════════════════════════════════════════════════════
76
76
 
77
77
  export type {
@@ -83,11 +83,19 @@ export type {
83
83
  export {
84
84
  isKyberAvailable,
85
85
  generateKyberKeypair,
86
+ generateKyberKeypairFromSeed,
86
87
  kyberEncapsulate,
87
88
  kyberDecapsulate,
88
89
  kyberWrapKey,
89
90
  kyberUnwrapKey,
91
+ KYBER_SUITE,
92
+ KYBER_PUBLIC_KEY_SIZE,
93
+ KYBER_SECRET_KEY_SIZE,
94
+ KYBER_CIPHERTEXT_SIZE,
95
+ KYBER_SHARED_SECRET_SIZE,
96
+ KYBER_SEED_SIZE,
90
97
  } from './kyber';
98
+ export type { KyberSuite } from './kyber';
91
99
 
92
100
  // ═══════════════════════════════════════════════════════════════════════════
93
101
  // DILITHIUM ML-DSA-65 (Post-Quantum Signatures)
@@ -1,11 +1,25 @@
1
1
  /**
2
- * Omnituum PQC Shared - Kyber ML-KEM-768
2
+ * Omnituum PQC Shared Kyber (ML-KEM-1024, FIPS 203)
3
3
  *
4
- * Browser-compatible Kyber operations using kyber-crystals.
5
- * NIST Level 3 security - quantum-resistant.
4
+ * Backend: @noble/post-quantum `ml_kem1024`. NIST FIPS 203 (final, 2024).
5
+ *
6
+ * Wire-format note: previous releases (<= 0.3.x) of this package were backed by
7
+ * `kyber-crystals`, which implements an earlier draft of Kyber and is NOT
8
+ * interoperable with FIPS 203 — see `tests/interop/historical/` and
9
+ * `package-docs/GAPS_AND_TASKS.md` (PQC-02 result). 0.4.0 is an intentional
10
+ * clean break: legacy draft-Kyber material cannot be read by this version.
11
+ *
12
+ * Sizes (ML-KEM-1024):
13
+ * publicKey 1568 bytes
14
+ * secretKey 3168 bytes
15
+ * ciphertext 1568 bytes
16
+ * sharedSecret 32 bytes
17
+ *
18
+ * Suite tag for new material: "ML-KEM-1024-FIPS203".
6
19
  */
7
20
 
8
- import { b64, ub64, sha256 } from './primitives';
21
+ import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js';
22
+ import { b64, ub64, sha256, randN } from './primitives';
9
23
  import nacl from 'tweetnacl';
10
24
 
11
25
  // ═══════════════════════════════════════════════════════════════════════════
@@ -27,33 +41,41 @@ export interface KyberEncapsulation {
27
41
  sharedSecret: Uint8Array;
28
42
  }
29
43
 
30
- // ═══════════════════════════════════════════════════════════════════════════
31
- // KYBER LIBRARY LOADING
32
- // ═══════════════════════════════════════════════════════════════════════════
44
+ /** Canonical suite identifier for new material produced by this package. */
45
+ export const KYBER_SUITE = 'ML-KEM-1024-FIPS203' as const;
46
+ export type KyberSuite = typeof KYBER_SUITE;
33
47
 
34
- let kyberModule: any = null;
48
+ // ─── Wire-format size constants (ML-KEM-1024, FIPS 203) ──────────────────────
49
+ // Exported so consumers can validate inputs and allocate buffers without
50
+ // restating magic literals. Tracked under PQC-08; mirrors the precedent
51
+ // established by DILITHIUM_PUBLIC_KEY_SIZE et al. in `./dilithium`.
35
52
 
36
- async function loadKyber(): Promise<any> {
37
- if (kyberModule) return kyberModule;
53
+ /** ML-KEM-1024 public key size (bytes). */
54
+ export const KYBER_PUBLIC_KEY_SIZE = 1568;
38
55
 
39
- try {
40
- const m = await import('kyber-crystals');
41
- const k = (m as any).default ?? m;
42
- kyberModule = k.kyber ?? k;
43
- return kyberModule;
44
- } catch (e) {
45
- console.warn('[Kyber] Failed to load kyber-crystals:', e);
46
- return null;
47
- }
48
- }
56
+ /** ML-KEM-1024 secret key size (bytes). */
57
+ export const KYBER_SECRET_KEY_SIZE = 3168;
58
+
59
+ /** ML-KEM-1024 ciphertext size (bytes). */
60
+ export const KYBER_CIPHERTEXT_SIZE = 1568;
61
+
62
+ /** ML-KEM-1024 shared-secret size (bytes). */
63
+ export const KYBER_SHARED_SECRET_SIZE = 32;
64
+
65
+ /** Seed size accepted by `generateKyberKeypairFromSeed` (bytes). */
66
+ export const KYBER_SEED_SIZE = 64;
49
67
 
50
68
  // ═══════════════════════════════════════════════════════════════════════════
51
- // AVAILABILITY CHECK
69
+ // AVAILABILITY
52
70
  // ═══════════════════════════════════════════════════════════════════════════
53
71
 
72
+ /**
73
+ * @deprecated Always returns true. Retained for API stability across the
74
+ * 0.3.x → 0.4.x cut. The noble backend is statically imported and always
75
+ * present; there is no runtime feature-detection path.
76
+ */
54
77
  export async function isKyberAvailable(): Promise<boolean> {
55
- const mod = await loadKyber();
56
- return mod !== null;
78
+ return true;
57
79
  }
58
80
 
59
81
  // ═══════════════════════════════════════════════════════════════════════════
@@ -61,139 +83,79 @@ export async function isKyberAvailable(): Promise<boolean> {
61
83
  // ═══════════════════════════════════════════════════════════════════════════
62
84
 
63
85
  /**
64
- * Generate a Kyber ML-KEM-768 keypair.
86
+ * Generate a fresh ML-KEM-1024 keypair using a cryptographically random seed.
65
87
  */
66
88
  export async function generateKyberKeypair(): Promise<KyberKeypairB64 | null> {
67
- try {
68
- const mod = await loadKyber();
69
- if (!mod) return null;
70
-
71
- if (mod?.ready?.then) {
72
- await mod.ready;
73
- }
74
-
75
- const fn = mod?.keypair ?? mod?.keyPair ?? mod?.generateKeyPair ?? null;
76
- if (typeof fn !== 'function') {
77
- console.warn('[Kyber] No keypair function found');
78
- return null;
79
- }
80
-
81
- const kp = await fn.call(mod);
82
- const pub = kp?.publicKey ?? kp?.public ?? kp?.pk;
83
- const priv = kp?.privateKey ?? kp?.secretKey ?? kp?.secret ?? kp?.sk;
84
-
85
- if (!pub || !priv) {
86
- console.warn('[Kyber] Invalid keypair result');
87
- return null;
88
- }
89
-
90
- return {
91
- publicB64: b64(new Uint8Array(pub)),
92
- secretB64: b64(new Uint8Array(priv)),
93
- };
94
- } catch (e) {
95
- console.warn('[Kyber] Key generation failed:', e);
96
- return null;
89
+ const seed = randN(64);
90
+ const kp = ml_kem1024.keygen(seed);
91
+ return {
92
+ publicB64: b64(kp.publicKey),
93
+ secretB64: b64(kp.secretKey),
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Derive a deterministic ML-KEM-1024 keypair from a 64-byte seed.
99
+ * Same seed → byte-identical keypair, across runtimes (Node, browser).
100
+ *
101
+ * The seed is passed verbatim to the FIPS 203 keygen. Callers that need
102
+ * domain separation between Kyber and other key types must apply it
103
+ * upstream (e.g. via SHA-256 with a domain tag) before calling.
104
+ */
105
+ export function generateKyberKeypairFromSeed(seed: Uint8Array): KyberKeypair {
106
+ if (seed.length !== 64) {
107
+ throw new Error('Kyber seed must be 64 bytes');
97
108
  }
109
+ const kp = ml_kem1024.keygen(seed);
110
+ return { publicKey: kp.publicKey, secretKey: kp.secretKey };
98
111
  }
99
112
 
100
113
  // ═══════════════════════════════════════════════════════════════════════════
101
114
  // ENCAPSULATION / DECAPSULATION
102
115
  // ═══════════════════════════════════════════════════════════════════════════
103
116
 
104
- /**
105
- * Encapsulate a shared secret using Kyber ML-KEM-768.
106
- */
107
117
  export async function kyberEncapsulate(pubKeyB64: string): Promise<KyberEncapsulation> {
108
- const kyber = await loadKyber();
109
- if (!kyber?.encrypt) {
110
- throw new Error('Kyber encrypt not available');
111
- }
112
-
113
118
  const pk = ub64(pubKeyB64);
114
- const r: any = await kyber.encrypt(pk);
115
-
116
- // Normalize ciphertext
117
- const ctRaw =
118
- r?.ciphertext ?? r?.cyphertext ?? r?.ct ??
119
- r?.bytes?.ciphertext ?? r?.bytes?.cyphertext ?? r?.bytes?.ct ??
120
- (Array.isArray(r) ? r[0] : undefined);
121
-
122
- // Normalize shared secret
123
- const ssRaw =
124
- r?.key ?? r?.sharedSecret ?? r?.secret ??
125
- r?.bytes?.key ?? r?.bytes?.sharedSecret ??
126
- (Array.isArray(r) ? r[1] : undefined);
127
-
128
- if (!ctRaw || !ssRaw) {
129
- throw new Error('Kyber encapsulate failed: missing ciphertext or shared secret');
130
- }
131
-
119
+ const enc = ml_kem1024.encapsulate(pk);
132
120
  return {
133
- ciphertext: new Uint8Array(ctRaw),
134
- sharedSecret: new Uint8Array(ssRaw),
121
+ ciphertext: enc.cipherText,
122
+ sharedSecret: enc.sharedSecret,
135
123
  };
136
124
  }
137
125
 
138
- /**
139
- * Decapsulate a shared secret using Kyber ML-KEM-768.
140
- */
141
126
  export async function kyberDecapsulate(
142
127
  kemCiphertextB64: string,
143
- secretKeyB64: string
128
+ secretKeyB64: string,
144
129
  ): Promise<Uint8Array> {
145
- const kyber = await loadKyber();
146
- if (!kyber?.decrypt && !kyber?.decapsulate) {
147
- throw new Error('Kyber decrypt/decapsulate not available');
148
- }
149
-
150
130
  const ct = ub64(kemCiphertextB64);
151
131
  const sk = ub64(secretKeyB64);
152
-
153
- const r: any = kyber.decrypt
154
- ? await kyber.decrypt(ct, sk)
155
- : await kyber.decapsulate(ct, sk);
156
-
157
- const key = (r && (r.key ?? r.sharedSecret)) ? (r.key ?? r.sharedSecret) : r;
158
- return new Uint8Array(key);
132
+ return ml_kem1024.decapsulate(ct, sk);
159
133
  }
160
134
 
161
135
  // ═══════════════════════════════════════════════════════════════════════════
162
- // KEY WRAPPING HELPERS
136
+ // KEY WRAPPING (library-agnostic; NaCl secretbox over derived KEK)
163
137
  // ═══════════════════════════════════════════════════════════════════════════
164
138
 
165
- /**
166
- * Wrap a message key using Kyber shared secret.
167
- */
168
- export function kyberWrapKey(sharedSecret: Uint8Array, msgKey32: Uint8Array): {
169
- nonce: string;
170
- wrapped: string;
171
- } {
139
+ export function kyberWrapKey(
140
+ sharedSecret: Uint8Array,
141
+ msgKey32: Uint8Array,
142
+ ): { nonce: string; wrapped: string } {
172
143
  if (msgKey32.length !== 32) {
173
144
  throw new Error('Message key must be 32 bytes');
174
145
  }
175
-
176
- const kek = sha256(sharedSecret); // Derive KEK from shared secret
146
+ const kek = sha256(sharedSecret);
177
147
  const nonce = globalThis.crypto.getRandomValues(new Uint8Array(24));
178
148
  const wrapped = nacl.secretbox(msgKey32, nonce, kek);
179
-
180
- return {
181
- nonce: b64(nonce),
182
- wrapped: b64(wrapped),
183
- };
149
+ return { nonce: b64(nonce), wrapped: b64(wrapped) };
184
150
  }
185
151
 
186
- /**
187
- * Unwrap a message key using Kyber shared secret.
188
- */
189
152
  export function kyberUnwrapKey(
190
153
  sharedSecret: Uint8Array,
191
154
  nonceB64: string,
192
- wrappedB64: string
155
+ wrappedB64: string,
193
156
  ): Uint8Array | null {
194
157
  const kek = sha256(sharedSecret);
195
158
  const nonce = ub64(nonceB64);
196
159
  const wrapped = ub64(wrappedB64);
197
-
198
160
  return nacl.secretbox.open(wrapped, nonce, kek) || null;
199
161
  }