@majikah/majik-key 0.1.1 → 0.1.3

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.
@@ -1,22 +1,26 @@
1
1
  import { mnemonicToSeedSync } from "@scure/bip39";
2
2
  import * as ed25519 from "@stablelib/ed25519";
3
3
  import ed2curve from "ed2curve";
4
- import { fingerprintFromPublicRaw, } from "./crypto-provider";
4
+ import { deriveMlKemKeypairFromSeed, fingerprintFromPublicRaw, } from "./crypto-provider";
5
5
  /**
6
6
  * EncryptionEngine
7
7
  * ----------------
8
8
  * Core cryptographic engine.
9
9
  */
10
10
  export class EncryptionEngine {
11
+ /* ================================
12
+ * Identity
13
+ * ================================ */
11
14
  /* ================================
12
15
  * Identity
13
16
  * ================================ */
14
17
  /**
15
- * Generates a long-term X25519 identity keypair.
18
+ * Generates a random long-term identity keypair (X25519 only).
19
+ * ML-KEM keys are not generated here since random identities
20
+ * cannot be deterministically recovered from a mnemonic.
16
21
  */
17
22
  static async generateIdentity() {
18
23
  try {
19
- // Generate an Ed25519 keypair (stablelib) and convert to Curve25519
20
24
  const ed = ed25519.generateKeyPair();
21
25
  const skCurve = ed2curve.convertSecretKey(ed.secretKey);
22
26
  const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
@@ -25,7 +29,6 @@ export class EncryptionEngine {
25
29
  }
26
30
  const pkBytes = new Uint8Array(pkCurve);
27
31
  const skBytes = new Uint8Array(skCurve);
28
- // Use raw key wrappers (Stablelib-backed) to avoid WebCrypto import variability
29
32
  const publicKey = { type: "public", raw: pkBytes };
30
33
  const privateKey = { type: "private", raw: skBytes };
31
34
  const fingerprint = fingerprintFromPublicRaw(pkBytes);
@@ -36,32 +39,56 @@ export class EncryptionEngine {
36
39
  }
37
40
  }
38
41
  /**
39
- * Derive an identity deterministically from a BIP39 mnemonic.
40
- * Uses Stablelib Ed25519 to derive a keypair from seed and converts to X25519.
42
+ * Derive a complete identity from a BIP-39 mnemonic.
43
+ *
44
+ * Seed derivation:
45
+ * mnemonicToSeedSync(mnemonic) → 64-byte BIP-39 seed
46
+ *
47
+ * X25519 derivation (unchanged from before):
48
+ * seed[0..32] → Ed25519 keypair via generateKeyPairFromSeed
49
+ * → X25519 via ed2curve conversion
50
+ *
51
+ * ML-KEM-768 derivation (new):
52
+ * seed[0..64] → ml_kem768.keygen(seed)
53
+ * → { publicKey: 1184 bytes, secretKey: 2400 bytes }
54
+ *
55
+ * The noble library accepts the full 64-byte BIP-39 seed directly.
56
+ * Internally it uses seed[0..32] for the lattice key matrix and
57
+ * seed[32..64] for the implicit rejection parameter `z`.
41
58
  */
42
59
  static async deriveIdentityFromMnemonic(mnemonic) {
43
60
  try {
44
61
  if (typeof mnemonic !== "string" || mnemonic.trim().length === 0) {
45
62
  throw new CryptoError("Mnemonic must be a non-empty string");
46
63
  }
47
- // Convert mnemonic to seed (64 bytes) then reduce to 32 bytes
48
- const seed = mnemonicToSeedSync(mnemonic); // Buffer
49
- const seed32 = new Uint8Array(seed.slice(0, 32));
50
- // Derive Ed25519 keypair from seed (stablelib)
64
+ // Step 1: BIP-39 seed → 64 bytes
65
+ const seed = mnemonicToSeedSync(mnemonic); // returns Buffer (Node) or Uint8Array
66
+ const seed64 = new Uint8Array(seed); // normalize to Uint8Array
67
+ // Step 2: X25519 identity from first 32 bytes (existing path)
68
+ const seed32 = seed64.subarray(0, 32);
51
69
  const ed = ed25519.generateKeyPairFromSeed(seed32);
52
- // Convert Ed25519 keys to X25519 (curve25519)
53
70
  const skCurve = ed2curve.convertSecretKey(ed.secretKey);
54
71
  const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
55
72
  if (!skCurve || !pkCurve) {
56
73
  throw new CryptoError("Failed to convert derived Ed25519 keys to Curve25519");
57
74
  }
58
- // Ensure plain Uint8Array
59
75
  const pkCurveBytes = new Uint8Array(pkCurve);
60
76
  const skCurveBytes = new Uint8Array(skCurve);
61
77
  const publicKey = { type: "public", raw: pkCurveBytes };
62
78
  const privateKey = { type: "private", raw: skCurveBytes };
63
79
  const fingerprint = fingerprintFromPublicRaw(pkCurveBytes);
64
- return { publicKey, privateKey, fingerprint };
80
+ // Step 3: ML-KEM-768 keypair from FULL 64-byte seed (new)
81
+ // ml_kem768.keygen() accepts a 64-byte seed directly.
82
+ // seed[0..32] → lattice key matrix expansion (K-PKE keygen)
83
+ // seed[32..64] → implicit rejection parameter z (stored in secretKey)
84
+ const mlKemKeypair = deriveMlKemKeypairFromSeed(seed64);
85
+ return {
86
+ publicKey,
87
+ privateKey,
88
+ fingerprint,
89
+ mlKemPublicKey: mlKemKeypair.publicKey, // 1184 bytes
90
+ mlKemSecretKey: mlKemKeypair.secretKey, // 2400 bytes
91
+ };
65
92
  }
66
93
  catch (err) {
67
94
  throw new CryptoError("Failed to derive identity from mnemonic", err);
@@ -8,6 +8,7 @@ export interface MajikMessageIdentityJSON {
8
8
  label: string;
9
9
  timestamp: string;
10
10
  restricted: boolean;
11
+ kdfVersion?: number;
11
12
  }
12
13
  /**
13
14
  * MajikMessageIdentity
@@ -11,6 +11,18 @@ export interface MajikKeyJSON {
11
11
  salt: string;
12
12
  backup: string;
13
13
  timestamp: string;
14
+ kdfVersion?: number;
15
+ mlKemPublicKey?: string;
16
+ encryptedMlKemSecretKey?: string;
17
+ }
18
+ export interface MajikKeyMetadata {
19
+ id: string;
20
+ fingerprint: string;
21
+ label: string;
22
+ timestamp: Date;
23
+ isLocked: boolean;
24
+ kdfVersion: number;
25
+ hasMlKem: boolean;
14
26
  }
15
27
  export interface MnemonicJSON {
16
28
  seed: string[];
@@ -1,5 +1,29 @@
1
+ /**
2
+ * MajikKey.ts
3
+ *
4
+ * Seed phrase account library for Majik Message.
5
+ *
6
+ * Every account stores TWO keypairs derived deterministically from the mnemonic:
7
+ * 1. X25519 (Curve25519) — fingerprint, contact identity, legacy message compat
8
+ * 2. ML-KEM-768 (FIPS-203) — post-quantum key encapsulation for v3 envelopes
9
+ *
10
+ * Both are derived from the 64-byte BIP-39 seed:
11
+ * seed[0..32] → Ed25519 → X25519 via ed2curve
12
+ * seed[0..64] → ml_kem768.keygen(seed) — full seed, deterministic
13
+ *
14
+ * KDF versioning (passphrase encryption at rest):
15
+ * v1 — PBKDF2-SHA256, 250k iterations (legacy read-only)
16
+ * v2 — Argon2id, 128 MB / 4t / 4p (all new accounts)
17
+ *
18
+ * Migration policy:
19
+ * Old accounts (v1, no ML-KEM keys) are fully upgraded on first import
20
+ * via importFromMnemonicBackup(). The mnemonic is always available at that
21
+ * point, so ML-KEM keys can be deterministically re-derived and stored.
22
+ * No partial migration — either fully upgraded or not upgraded yet.
23
+ */
1
24
  import { MajikContact } from "./core/majik-contact";
2
- import { MajikKeyJSON, MnemonicJSON } from "./core/types";
25
+ import { KDF_VERSION } from "./core/crypto/constants";
26
+ import type { MajikKeyJSON, MajikKeyMetadata, MnemonicJSON } from "./core/types";
3
27
  import { MajikMessageIdentity } from "./core/database/system/identity";
4
28
  import { MajikUser } from "@thezelijah/majik-user";
5
29
  export interface MajikKeyIdentity {
@@ -13,6 +37,9 @@ export interface MajikKeyIdentity {
13
37
  };
14
38
  encryptedPrivateKey: ArrayBuffer;
15
39
  salt: string;
40
+ kdfVersion: KDF_VERSION;
41
+ mlKemPublicKey?: Uint8Array;
42
+ mlKemSecretKey?: Uint8Array;
16
43
  }
17
44
  export interface SerializedIdentity {
18
45
  id: string;
@@ -34,24 +61,16 @@ export interface MajikKeyConstructorOptions {
34
61
  backup: string;
35
62
  label?: string;
36
63
  timestamp?: Date;
64
+ kdfVersion?: KDF_VERSION;
65
+ mlKemPublicKey?: Uint8Array;
66
+ mlKemSecretKey?: Uint8Array;
67
+ encryptedMlKemSecretKey?: ArrayBuffer;
68
+ encryptedMlKemSecretKeyBase64?: string;
37
69
  privateKey?: CryptoKey | {
38
70
  raw: Uint8Array;
39
71
  };
40
72
  privateKeyBase64?: string;
41
73
  }
42
- export interface MajikKeyMetadata {
43
- id: string;
44
- fingerprint: string;
45
- label: string;
46
- timestamp: Date;
47
- isLocked: boolean;
48
- }
49
- /**
50
- * MajikKey
51
- * ----------------
52
- * A seed phrase account library for creating, managing, and parsing mnemonic-based cryptographic accounts (Majik Keys).
53
- * Generate deterministic key pairs from BIP39 seed phrases with simple, developer-friendly APIs.
54
- */
55
74
  export declare class MajikKey {
56
75
  private readonly _id;
57
76
  private readonly _publicKey;
@@ -63,6 +82,11 @@ export declare class MajikKey {
63
82
  private _encryptedPrivateKeyBase64;
64
83
  private _salt;
65
84
  private _label;
85
+ private _kdfVersion;
86
+ private _mlKemPublicKey?;
87
+ private _mlKemSecretKey?;
88
+ private _encryptedMlKemSecretKey?;
89
+ private _encryptedMlKemSecretKeyBase64?;
66
90
  private _privateKey?;
67
91
  private _privateKeyBase64?;
68
92
  private constructor();
@@ -75,195 +99,71 @@ export declare class MajikKey {
75
99
  get label(): string;
76
100
  get backup(): string;
77
101
  get timestamp(): Date;
102
+ get kdfVersion(): KDF_VERSION;
103
+ get isArgon2id(): boolean;
78
104
  get isLocked(): boolean;
79
105
  get isUnlocked(): boolean;
80
- /**
81
- * Get safe metadata (no sensitive data)
82
- */
106
+ get mlKemPublicKey(): Uint8Array | undefined;
107
+ get mlKemSecretKey(): Uint8Array | undefined;
108
+ get hasMlKem(): boolean;
109
+ get isFullyUpgraded(): boolean;
83
110
  get metadata(): MajikKeyMetadata;
84
- /**
85
- * CREATE: Generate a new MajikKey from a mnemonic phrase.
86
- * The key is created in an unlocked state with private keys available.
87
- *
88
- * @param mnemonic - BIP39 mnemonic phrase (12-24 words)
89
- * @param passphrase - Passphrase to encrypt the private key at rest
90
- * @param label - Optional label for the key
91
- * @returns A new unlocked MajikKey instance
92
- */
93
111
  static create(mnemonic: string, passphrase: string, label?: string): Promise<MajikKey>;
94
- /**
95
- * Export this MajikKey to MnemonicJSON format.
96
- * This format is useful for storing mnemonic data with an optional passphrase.
97
- *
98
- * @param mnemonic - The BIP39 mnemonic phrase
99
- * @param passphrase - Optional passphrase (encryption password, not BIP39 passphrase)
100
- * @returns MnemonicJSON object
101
- */
112
+ static fromJSON(json: MajikKeyJSON | string): MajikKey;
102
113
  toMnemonicJSON(mnemonic: string, passphrase?: string): MnemonicJSON;
103
- /**
104
- * Create a MajikKey from MnemonicJSON format.
105
- *
106
- * @param mnemonicJson - MnemonicJSON object or string
107
- * @param passphrase - Passphrase to encrypt the key at rest
108
- * @param label - Optional label for the key
109
- * @returns A new unlocked MajikKey instance
110
- */
111
114
  static fromMnemonicJSON(mnemonicJson: MnemonicJSON | string, passphrase: string, label?: string): Promise<MajikKey>;
112
- /**
113
- * READ: Load a MajikKey from JSON (locked state).
114
- * The key must be unlocked with the unlock() method before accessing private keys.
115
- *
116
- * @param json - JSON string or object
117
- * @returns A locked MajikKey instance
118
- */
119
- static fromJSON(json: MajikKeyJSON | string): MajikKey;
120
- /**
121
- * UPDATE: Change the label of this MajikKey.
122
- *
123
- * @param newLabel - New label value
124
- * @returns This instance for chaining
125
- */
126
115
  updateLabel(newLabel: string): this;
127
- /**
128
- * UPDATE: Change the passphrase used to encrypt the private key.
129
- * Requires the current passphrase for verification.
130
- *
131
- * @param currentPassphrase - Current passphrase
132
- * @param newPassphrase - New passphrase
133
- * @returns This instance for chaining
134
- */
135
116
  updatePassphrase(currentPassphrase: string, newPassphrase: string): Promise<this>;
136
117
  /**
137
- * DELETE: Securely lock this MajikKey by clearing private keys from memory.
138
- * The encrypted private key remains stored for future unlocking.
139
- *
140
- * @returns This instance for chaining
118
+ * Migrate KDF from PBKDF2 to Argon2id without changing passphrase.
119
+ * NOTE: Does not add ML-KEM keys — use importFromMnemonicBackup() for full upgrade.
141
120
  */
121
+ migrate(passphrase: string): Promise<this>;
142
122
  lock(): this;
143
- /**
144
- * Unlock this MajikKey by decrypting the private key with the passphrase.
145
- * Sets the private key in memory for cryptographic operations.
146
- *
147
- * @param passphrase - Passphrase to decrypt the private key
148
- * @returns This instance for chaining
149
- * @throws MajikKeyError if passphrase is incorrect or key is already unlocked
150
- */
151
123
  unlock(passphrase: string): Promise<this>;
152
- /**
153
- * Verify that the encrypted private key can be decrypted with passphrase
154
- */
155
124
  verify(passphrase: string): Promise<boolean>;
156
- /**
157
- * Get the private key (only available when unlocked).
158
- *
159
- * @returns The private key
160
- * @throws MajikKeyError if the key is locked
161
- */
162
125
  getPrivateKey(): CryptoKey | {
163
126
  raw: Uint8Array;
164
127
  };
165
- /**
166
- * Get the private key as base64 (only available when unlocked).
167
- *
168
- * @returns The private key in base64 format
169
- * @throws MajikKeyError if the key is locked
170
- */
171
128
  getPrivateKeyBase64(): string;
172
- /**
173
- * Export this MajikKey to JSON format (safe for storage).
174
- * Private keys are never included in the JSON output.
175
- *
176
- * @returns JSON representation of this MajikKey
177
- */
129
+ getMlKemSecretKey(): Uint8Array;
178
130
  toJSON(): MajikKeyJSON;
179
- /**
180
- * Export this MajikKey to a JSON string.
181
- *
182
- * @param pretty - Whether to pretty-print the JSON
183
- * @returns JSON string representation
184
- */
185
131
  toString(pretty?: boolean): string;
186
- /**
187
- * Generate a new BIP39 mnemonic phrase.
188
- *
189
- * @param strength - Entropy strength in bits (128 = 12 words, 256 = 24 words)
190
- * @returns A new mnemonic phrase
191
- */
192
132
  static generateMnemonic(strength?: 128 | 256): string;
193
- /**
194
- * Validate a BIP39 mnemonic phrase.
195
- *
196
- * @param mnemonic - Mnemonic phrase to validate
197
- * @returns true if valid, false otherwise
198
- */
199
133
  static validateMnemonic(mnemonic: string): boolean;
200
- /**
201
- * Create a MajikContact from this MajikKey.
202
- *
203
- * @returns A MajikContact instance
204
- */
205
134
  toContact(): MajikContact;
206
- /**
207
- * Convert to internal MajikKeyIdentity format (for backward compatibility).
208
- * Note: Only includes privateKey if unlocked.
209
- *
210
- * @returns MajikKeyIdentity object
211
- */
212
135
  toKeyIdentity(): MajikKeyIdentity;
213
- /**
214
- * Convert to internal SerializedIdentity format (for Majik Message).
215
- *
216
- * @returns SerializedIdentity object
217
- */
218
136
  toSerializedIdentity(): SerializedIdentity;
219
137
  toMajikMessageIdentity(user: MajikUser, options?: {
220
138
  label?: string;
221
139
  restricted?: boolean;
222
140
  }): Promise<MajikMessageIdentity>;
223
- /**
224
- * Export a mnemonic-encrypted backup for this MajikKey.
225
- * Requires the key to be unlocked.
226
- *
227
- * @param mnemonic - The original mnemonic phrase
228
- * @returns Base64-encoded backup string
229
- */
230
141
  exportMnemonicBackup(mnemonic: string): Promise<string>;
231
142
  /**
232
143
  * Import a MajikKey from a mnemonic-encrypted backup.
233
144
  *
234
- * @param backup - Base64-encoded backup string
235
- * @param mnemonic - The mnemonic phrase used to encrypt the backup
236
- * @param passphrase - Passphrase to encrypt the imported key
237
- * @param label - Optional label for the imported key
238
- * @returns A new unlocked MajikKey instance
145
+ * This is the FULL MIGRATION PATH for old accounts — Argon2id + ML-KEM in one step:
146
+ * 1. Verify the backup decrypts correctly (proves mnemonic is correct)
147
+ * 2. Re-derive the complete identity from the mnemonic (X25519 + ML-KEM-768)
148
+ * 3. Encrypt both private keys with Argon2id (v2) + fresh 32-byte salt
149
+ * 4. Return a fully-upgraded MajikKey with hasMlKem: true, isArgon2id: true
150
+ *
151
+ * Old accounts without ML-KEM keys become fully post-quantum capable
152
+ * automatically — no extra user steps. The mnemonic is the source of truth.
239
153
  */
240
154
  static importFromMnemonicBackup(backup: string, mnemonic: string, passphrase: string, label?: string): Promise<MajikKey>;
241
- /**
242
- * Create a deterministic identity from a mnemonic and encrypt it with passphrase.
243
- * The identity `id` is set to the fingerprint for stable referencing.
244
- */
245
- private static createIdentityFromMnemonic;
246
- /**
247
- * Export an identity encrypted with a mnemonic-derived key.
248
- * Returns a base64 string containing iv+ciphertext and publicKey/fingerprint in JSON.
249
- */
250
- private static exportIdentityMnemonicBackup;
251
- /**
252
- * Encrypt a private key with a passphrase.
253
- */
254
- private static encryptPrivateKey;
255
- /**
256
- * Decrypt a private key with a passphrase.
257
- */
258
- private static decryptPrivateKey;
259
- private static deriveKeyFromMnemonic;
260
- /**
261
- * Validate whether a passphrase can decrypt the stored private key.
262
- * Does NOT unlock or mutate any in-memory state.
263
- */
264
- private static isPassphraseValid;
265
- /**
266
- * Export a CryptoKey to base64 string.
267
- */
268
- private static exportKeyToBase64;
155
+ private static _deriveAndEncryptFromMnemonic;
156
+ private static _encryptPrivateKey;
157
+ /**
158
+ * Encrypt the ML-KEM secret key using the same Argon2id-derived key as X25519
159
+ * (same passphrase + same salt) but a DIFFERENT random IV. One Argon2id
160
+ * computation → two independently encrypted blobs.
161
+ */
162
+ private static _encryptMlKemSecretKey;
163
+ private static _decryptPrivateKey;
164
+ private static _decryptMlKemSecretKey;
165
+ private static _verifyBackupDecryption;
166
+ private static _exportMnemonicBackup;
167
+ private static _deriveLegacyMnemonicKey;
168
+ private static _exportKeyToBase64;
269
169
  }