@majikah/majik-key 0.2.13 → 0.3.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.
@@ -1,88 +1,135 @@
1
1
  /**
2
2
  * MajikKey.ts
3
+ * Seed phrase account library for the Majikah ecosystem.
3
4
  *
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
5
  */
24
6
  import { MajikContact, MajikContactMeta } from "@majikah/majik-contact";
25
7
  import { KDF_VERSION } from "./core/crypto/constants";
26
- import type { MajikKeyDangerousJSON, MajikKeyJSON, MajikKeyMetadata, MnemonicJSON } from "./core/types";
8
+ import type { MajikKeyAddress, MajikKeyDangerousJSON, MajikKeyFingerprint, MajikKeyJSON, MajikKeyMetadata, MnemonicJSON } from "./core/types";
27
9
  import { MajikMessageIdentity } from "./core/database/system/identity";
28
10
  import { MajikUser } from "@thezelijah/majik-user";
29
11
  import { MnemonicLanguage } from "./core/crypto/wordlist";
30
12
  import { MajikKeyWeb3Namespace, BitcoinDerivationOptions, BitcoinKeypairMaterial, SolanaKeypairMaterial } from "./core/web3";
13
+ /**
14
+ * In-memory identity bundle for an *unlocked* MajikKey — the raw CryptoKey/
15
+ * Uint8Array material, not the encrypted-at-rest form. This is what
16
+ * `toKeyIdentity()` returns, and what backup export starts from.
17
+ *
18
+ * `mlKemPublicKey`/`mlKemSecretKey` are required here because every account
19
+ * (even ones mid-migration) is expected to carry ML-KEM material by the time
20
+ * this shape is used. `ed*`, `mlDsa*`, and `btc*` are optional because
21
+ * accounts imported before those key types existed may not have them yet —
22
+ * check `hasSigningKeys` / `hasBitcoin` on the `MajikKey` instance before
23
+ * relying on them.
24
+ */
31
25
  export interface MajikKeyIdentity {
26
+ /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
32
27
  id: string;
28
+ /** X25519 public key — native `CryptoKey` where WebCrypto supports it, otherwise a raw-bytes wrapper. */
33
29
  publicKey: CryptoKey | {
34
30
  raw: Uint8Array;
35
31
  };
36
- fingerprint: string;
32
+ /** SHA-256 fingerprint of `publicKey`. */
33
+ fingerprint: MajikKeyFingerprint;
34
+ /** X25519 private key, decrypted into memory. ⚠️ Live key material — do not log or serialize directly. */
37
35
  privateKey: CryptoKey | {
38
36
  raw: Uint8Array;
39
37
  };
38
+ /** AES-256-GCM-encrypted X25519 private key (IV + ciphertext), as stored at rest. */
40
39
  encryptedPrivateKey: ArrayBuffer;
40
+ /** Random salt used to derive the passphrase-based encryption key. Base64. */
41
41
  salt: string;
42
+ /** KDF used to encrypt the keys on this identity: `1` = legacy PBKDF2, `2` = Argon2id. */
42
43
  kdfVersion: KDF_VERSION;
44
+ /** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. */
43
45
  mlKemPublicKey: Uint8Array;
46
+ /** ML-KEM-768 secret key, decrypted into memory. ⚠️ Live key material. */
44
47
  mlKemSecretKey?: Uint8Array;
48
+ /** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. */
45
49
  edPublicKey?: Uint8Array;
50
+ /** Ed25519 secret key, decrypted into memory. ⚠️ Live key material. */
46
51
  edSecretKey?: Uint8Array;
52
+ /** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. */
47
53
  mlDsaPublicKey?: Uint8Array;
54
+ /** ML-DSA-87 secret key, decrypted into memory. ⚠️ Live key material. */
48
55
  mlDsaSecretKey?: Uint8Array;
56
+ /** @experimental secp256k1 Bitcoin public key. Domain-separated BIP-32/84 derivation by default. */
49
57
  btcPublicKey?: Uint8Array;
58
+ /** @experimental Bitcoin private key, decrypted into memory. ⚠️ Live key material. */
50
59
  btcSecretKey?: Uint8Array;
51
60
  }
61
+ /**
62
+ * `MajikKeyIdentity` immediately after fresh derivation from a mnemonic —
63
+ * i.e. what `create()` and `importFromMnemonicBackup()` produce internally,
64
+ * before the result is wrapped into a `MajikKey` instance.
65
+ *
66
+ * Unlike the base `MajikKeyIdentity`, every `encrypted*` field here is
67
+ * required: a fresh derivation always re-derives and re-encrypts the full
68
+ * key set (ML-KEM, Ed25519, ML-DSA, and Bitcoin) in one pass, so there's no
69
+ * "partially migrated" state at this point in the flow.
70
+ */
52
71
  export type MajikKeyDerivedIdentity = MajikKeyIdentity & {
72
+ /** AES-256-GCM-encrypted ML-KEM-768 secret key, freshly re-encrypted. */
53
73
  encryptedMlKemSecretKey: ArrayBuffer;
74
+ /** AES-256-GCM-encrypted Ed25519 secret key, freshly re-encrypted. */
54
75
  encryptedEdSecretKey: ArrayBuffer;
76
+ /** AES-256-GCM-encrypted ML-DSA-87 secret key, freshly re-encrypted. */
55
77
  encryptedMlDsaSecretKey: ArrayBuffer;
56
- encryptedBtcSecretKey: ArrayBuffer;
78
+ /** @experimental AES-256-GCM-encrypted Bitcoin secret key, freshly re-encrypted. */
79
+ encryptedBtcSecretKey?: ArrayBuffer;
57
80
  };
81
+ /**
82
+ * Minimal identity export — just enough to identify the account and, if
83
+ * present, re-derive access to it. Lighter than `MajikKeyJSON`: no ML-KEM,
84
+ * Ed25519, ML-DSA, or Bitcoin fields at all. Produced by
85
+ * `toSerializedIdentity()` (unlocked keys only).
86
+ */
58
87
  export interface SerializedIdentity {
59
88
  id: string;
60
- publicKey: string;
61
- fingerprint: string;
89
+ /** X25519 public key, base64. */
90
+ publicKey: MajikKeyAddress;
91
+ fingerprint: MajikKeyFingerprint;
92
+ /** AES-256-GCM-encrypted X25519 private key, base64. Omitted in some contexts — check before use. */
62
93
  encryptedPrivateKey?: string;
94
+ /** Base64 salt paired with `encryptedPrivateKey`. Omitted in some contexts — check before use. */
63
95
  salt?: string;
64
96
  }
97
+ /**
98
+ * Internal constructor payload for `MajikKey` — every static factory
99
+ * (`create()`, `fromJSON()`, `fromDangerousJSON()`, `importFromMnemonicBackup()`,
100
+ * etc.) builds one of these and passes it to the private constructor.
101
+ *
102
+ * You won't normally build this by hand; it's exported mainly for type
103
+ * inference around the factory methods. Fields mirror `MajikKeyJSON` plus
104
+ * the live (decrypted) counterparts where a factory is constructing an
105
+ * already-unlocked instance.
106
+ */
65
107
  export interface MajikKeyConstructorOptions {
66
108
  id: string;
67
109
  publicKey: CryptoKey | {
68
110
  raw: Uint8Array;
69
111
  };
70
- publicKeyBase64: string;
71
- fingerprint: string;
112
+ publicKeyBase64: MajikKeyAddress;
113
+ fingerprint: MajikKeyFingerprint;
72
114
  encryptedPrivateKey: ArrayBuffer;
73
115
  encryptedPrivateKeyBase64: string;
74
116
  salt: string;
117
+ /** Encrypted mnemonic-verification blob — see `MajikKeyJSON.backup`. */
75
118
  backup: string;
76
119
  label?: string;
77
120
  timestamp?: Date;
121
+ /** Defaults to legacy PBKDF2 (`KDF_VERSION.PBKDF2`) if omitted — see the private constructor. */
78
122
  kdfVersion?: KDF_VERSION;
79
123
  mlKemPublicKey: Uint8Array;
124
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
80
125
  mlKemSecretKey?: Uint8Array;
81
126
  encryptedMlKemSecretKey?: ArrayBuffer;
82
127
  encryptedMlKemSecretKeyBase64?: string;
128
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
83
129
  privateKey?: CryptoKey | {
84
130
  raw: Uint8Array;
85
131
  };
132
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
86
133
  privateKeyBase64?: string;
87
134
  edPublicKey?: Uint8Array;
88
135
  encryptedEdSecretKey?: ArrayBuffer;
@@ -90,14 +137,43 @@ export interface MajikKeyConstructorOptions {
90
137
  mlDsaPublicKey?: Uint8Array;
91
138
  encryptedMlDsaSecretKey?: ArrayBuffer;
92
139
  encryptedMlDsaSecretKeyBase64?: string;
140
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
93
141
  edSecretKey?: Uint8Array;
142
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
94
143
  mlDsaSecretKey?: Uint8Array;
144
+ /** @experimental secp256k1 Bitcoin public key. */
95
145
  btcPublicKey?: Uint8Array;
146
+ /** @experimental AES-256-GCM-encrypted Bitcoin private key. */
96
147
  encryptedBtcSecretKey?: ArrayBuffer;
148
+ /** @experimental Base64 form of `encryptedBtcSecretKey`. */
97
149
  encryptedBtcSecretKeyBase64?: string;
150
+ /** @experimental Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
98
151
  btcSecretKey?: Uint8Array;
99
152
  mnemonicLanguage?: MnemonicLanguage;
100
153
  }
154
+ /**
155
+ * MajikKey
156
+ * ---
157
+ *
158
+ * Seed phrase account library for the Majikah ecosystem.
159
+ *
160
+ * Every account stores FIVE keypairs, all deterministically derived from a
161
+ * single BIP-39 mnemonic:
162
+ * 1. X25519 (Curve25519) — fingerprint, contact identity, legacy message compat
163
+ * 2. ML-KEM-768 (FIPS-203) — post-quantum key encapsulation for v3 envelopes
164
+ * 3. Ed25519 — classical signing
165
+ * 4. ML-DSA-87 (FIPS-204) — post-quantum signing
166
+ * 5. Bitcoin (secp256k1) — BIP-32/84 HD key, domain-separated by default (experimental)
167
+ *
168
+ * All derived from the 64-byte BIP-39 seed:
169
+ * seed[0..32] → Ed25519 keypair — used directly for signing, AND converted
170
+ * to X25519 via ed2curve for the encryption/identity keypair
171
+ * (one Ed25519 keypair, two roles)
172
+ * seed[0..64] → ml_kem768.keygen(seed) — full seed, deterministic
173
+ * hash(seed || "MajikSignatureSeedDSA") → 32-byte seed → ml_dsa87.keygen()
174
+ * seed[0..64] → HDKey.fromMasterSeed(seed).derive(path) — BIP-32/84 Bitcoin key
175
+ *
176
+ */
101
177
  export declare class MajikKey {
102
178
  private readonly _id;
103
179
  private readonly _publicKey;
@@ -125,37 +201,108 @@ export declare class MajikKey {
125
201
  private _mlDsaSecretKey?;
126
202
  private _encryptedMlDsaSecretKey?;
127
203
  private _encryptedMlDsaSecretKeyBase64?;
204
+ /**
205
+ * @experimental
206
+ */
128
207
  private _solanaKeypairMaterial?;
208
+ /**
209
+ * @experimental
210
+ */
129
211
  private _btcPublicKey?;
212
+ /**
213
+ * @experimental
214
+ */
130
215
  private _btcSecretKey?;
216
+ /**
217
+ * @experimental
218
+ */
131
219
  private _encryptedBtcSecretKey?;
220
+ /**
221
+ * @experimental
222
+ */
132
223
  private _encryptedBtcSecretKeyBase64?;
133
224
  private constructor();
225
+ /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
134
226
  get id(): string;
135
- get fingerprint(): string;
227
+ /** SHA-256 fingerprint of the X25519 public key. Stable identity anchor for the account. */
228
+ get fingerprint(): MajikKeyFingerprint;
229
+ /** X25519 public key — native `CryptoKey` where WebCrypto supports it, otherwise a raw-bytes wrapper. Always available, even when locked. */
136
230
  get publicKey(): CryptoKey | {
137
231
  raw: Uint8Array;
138
232
  };
139
- get publicKeyBase64(): string;
233
+ /** X25519 public key, base64-encoded. Always available, even when locked. */
234
+ get publicKeyBase64(): MajikKeyAddress;
235
+ /** Human-readable, user-editable account name. Update via `updateLabel()`. */
140
236
  get label(): string;
237
+ /** BIP-39 wordlist language this account's mnemonic was generated/validated against. */
141
238
  get mnemonicLanguage(): MnemonicLanguage;
239
+ /**
240
+ * Encrypted mnemonic-verification blob (base64 JSON). Decryptable only
241
+ * with the original mnemonic — used internally to verify a supplied
242
+ * mnemonic before `importFromMnemonicBackup()` re-derives the full
243
+ * identity. Not a general-purpose private-key backup.
244
+ */
142
245
  get backup(): string;
246
+ /** Account creation time. */
143
247
  get timestamp(): Date;
248
+ /** KDF currently protecting every `encrypted*` field on this account: `1` = legacy PBKDF2, `2` = Argon2id. */
144
249
  get kdfVersion(): KDF_VERSION;
250
+ /** `true` if this account is on the current KDF (Argon2id). `false` means it's still on legacy PBKDF2 — see `migrate()` or `importFromMnemonicBackup()`. */
145
251
  get isArgon2id(): boolean;
252
+ /** `true` if private key material is currently purged from memory (i.e. `lock()` was called, or `unlock()` hasn't been called yet). */
146
253
  get isLocked(): boolean;
254
+ /** `true` if private key material is currently decrypted in memory. The inverse of `isLocked`. */
147
255
  get isUnlocked(): boolean;
256
+ /** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. Always available, even when locked. */
148
257
  get mlKemPublicKey(): Uint8Array;
258
+ /** ML-KEM-768 secret key. `undefined` unless the account is unlocked. ⚠️ Live key material — prefer `getMlKemSecretKey()` if you want a thrown error instead of `undefined` on locked accounts. */
149
259
  get mlKemSecretKey(): Uint8Array | undefined;
260
+ /** `true` if this account has ML-KEM-768 keys (i.e. is post-quantum-encryption capable). `false` means it's a legacy account pending migration. */
150
261
  get hasMlKem(): boolean;
262
+ /** `true` if this account is on Argon2id *and* has ML-KEM-768 keys — i.e. fully migrated, nothing left to upgrade. */
151
263
  get isFullyUpgraded(): boolean;
264
+ /**
265
+ * @experimental secp256k1 Bitcoin public key. `undefined` if this account
266
+ * has no stored Bitcoin key material (e.g. it predates Web3 support and
267
+ * hasn't been re-imported via `importFromMnemonicBackup()`).
268
+ */
152
269
  get btcPublicKey(): Uint8Array | undefined;
270
+ /** @experimental `true` if this account has a stored Bitcoin keypair. */
153
271
  get hasBitcoin(): boolean;
272
+ /**
273
+ * Lightweight, non-secret snapshot of this account's state — no key bytes
274
+ * at all, encrypted or otherwise. Useful for account pickers, dashboards,
275
+ * or anywhere you want to display status without touching key material.
276
+ */
154
277
  get metadata(): MajikKeyMetadata;
278
+ /** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. Always available, even when locked. */
155
279
  get edPublicKey(): Uint8Array | undefined;
280
+ /** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. Always available, even when locked. */
156
281
  get mlDsaPublicKey(): Uint8Array | undefined;
282
+ /** `true` if this account has both Ed25519 and ML-DSA-87 signing keys. `false` means it's a legacy account pending migration. */
157
283
  get hasSigningKeys(): boolean;
158
- static create(mnemonic: string, passphrase: string, label?: string, mnemonicLanguage?: MnemonicLanguage): Promise<MajikKey>;
284
+ /**
285
+ * Creates a brand-new MajikKey account from a BIP-39 mnemonic.
286
+ *
287
+ * Derives the full key set in one pass — X25519, ML-KEM-768, Ed25519,
288
+ * ML-DSA-87, and a domain-separated Bitcoin key (see `MAJIK_BITCOIN_DOMAIN_PATH`)
289
+ * — encrypts every private key with Argon2id (KDF v2), and returns an
290
+ * **already-unlocked** instance (no `unlock()` call needed right after
291
+ * `create()`).
292
+ *
293
+ * @param mnemonic - A valid BIP-39 mnemonic phrase (12 or 24 words), matching `mnemonicLanguage`. Generate one with `MajikKey.generateMnemonic()`.
294
+ * @param passphrase - Passphrase used to derive the Argon2id encryption key for every private key on this account. This is *not* the mnemonic — losing it without the mnemonic makes the account unrecoverable.
295
+ * @param label - Optional human-readable account name. Defaults to an empty string. Update later via `updateLabel()`.
296
+ * @param mnemonicLanguage - BIP-39 wordlist to validate `mnemonic` against. Defaults to `"en"`.
297
+ * @param options.deriveBitcoin - @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true`.
298
+ * @returns An unlocked `MajikKey` instance, ready for immediate use — call `.lock()` when you're done with it.
299
+ * @throws {MajikKeyError} If `mnemonic` fails validation, `passphrase`/`label` fail their validators, or `mnemonic` doesn't match `mnemonicLanguage`'s wordlist.
300
+ */
301
+ static create(mnemonic: string, passphrase: string, label?: string, options?: {
302
+ mnemonicLanguage?: MnemonicLanguage;
303
+ /** @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true` for backward compatibility. */
304
+ deriveBitcoin?: boolean;
305
+ }): Promise<MajikKey>;
159
306
  static fromJSON(json: MajikKeyJSON | string): MajikKey;
160
307
  /**
161
308
  * Export a fully unlocked MajikKey with all raw private keys.
@@ -209,17 +356,18 @@ export declare class MajikKey {
209
356
  /**
210
357
  * Import a MajikKey from a mnemonic-encrypted backup.
211
358
  *
212
- * This is the FULL MIGRATION PATH for old accounts — Argon2id + ML-KEM in one step:
213
- * 1. Verify the backup decrypts correctly (proves mnemonic is correct)
214
- * 2. Re-derive the complete identity from the mnemonic (X25519 + ML-KEM-768)
215
- * 3. Encrypt both private keys with Argon2id (v2) + fresh 32-byte salt
216
- * 4. Return a fully-upgraded MajikKey with hasMlKem: true, isArgon2id: true
217
- *
218
- * Old accounts without ML-KEM keys become fully post-quantum capable
219
- * automatically — no extra user steps. The mnemonic is the source of truth.
220
359
  */
221
- static importFromMnemonicBackup(backup: string, mnemonic: string, passphrase: string, label?: string, mnemonicLanguage?: MnemonicLanguage): Promise<MajikKey>;
360
+ static importFromMnemonicBackup(backup: string, mnemonic: string, passphrase: string, label?: string, options?: {
361
+ mnemonicLanguage?: MnemonicLanguage;
362
+ /** @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true` for backward compatibility. */
363
+ deriveBitcoin?: boolean;
364
+ }): Promise<MajikKey>;
222
365
  private static _getWordlist;
366
+ /**
367
+ * @param mnemonic - BIP-39 mnemonic to derive the full key set from.
368
+ * @param passphrase - Passphrase used to derive the Argon2id encryption key shared by every private key produced here.
369
+ * @param options.deriveBitcoin - @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true`.
370
+ */
223
371
  private static _deriveAndEncryptFromMnemonic;
224
372
  private static _encryptPrivateKey;
225
373
  /**
@@ -236,7 +384,13 @@ export declare class MajikKey {
236
384
  private static _exportMnemonicBackup;
237
385
  private static _deriveLegacyMnemonicKey;
238
386
  private static _exportKeyToBase64;
387
+ /**
388
+ * @experimental
389
+ */
239
390
  getBtcSecretKey(): Uint8Array;
391
+ /**
392
+ * @experimental
393
+ */
240
394
  get web3(): MajikKeyWeb3Namespace | undefined;
241
395
  /**
242
396
  * @experimental True if this MajikKey can currently produce Bitcoin