@majikah/majik-key 0.2.12 → 0.2.14

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,86 +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
- import { SolanaKeypairMaterial } from "./core/web3/solana";
31
- import { MajikKeyWeb3Namespace } from "./core/web3/types";
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
+ */
32
25
  export interface MajikKeyIdentity {
26
+ /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
33
27
  id: string;
28
+ /** X25519 public key — native `CryptoKey` where WebCrypto supports it, otherwise a raw-bytes wrapper. */
34
29
  publicKey: CryptoKey | {
35
30
  raw: Uint8Array;
36
31
  };
37
- 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. */
38
35
  privateKey: CryptoKey | {
39
36
  raw: Uint8Array;
40
37
  };
38
+ /** AES-256-GCM-encrypted X25519 private key (IV + ciphertext), as stored at rest. */
41
39
  encryptedPrivateKey: ArrayBuffer;
40
+ /** Random salt used to derive the passphrase-based encryption key. Base64. */
42
41
  salt: string;
42
+ /** KDF used to encrypt the keys on this identity: `1` = legacy PBKDF2, `2` = Argon2id. */
43
43
  kdfVersion: KDF_VERSION;
44
+ /** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. */
44
45
  mlKemPublicKey: Uint8Array;
46
+ /** ML-KEM-768 secret key, decrypted into memory. ⚠️ Live key material. */
45
47
  mlKemSecretKey?: Uint8Array;
48
+ /** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. */
46
49
  edPublicKey?: Uint8Array;
50
+ /** Ed25519 secret key, decrypted into memory. ⚠️ Live key material. */
47
51
  edSecretKey?: Uint8Array;
52
+ /** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. */
48
53
  mlDsaPublicKey?: Uint8Array;
54
+ /** ML-DSA-87 secret key, decrypted into memory. ⚠️ Live key material. */
49
55
  mlDsaSecretKey?: Uint8Array;
56
+ /** @experimental secp256k1 Bitcoin public key. Domain-separated BIP-32/84 derivation by default. */
57
+ btcPublicKey?: Uint8Array;
58
+ /** @experimental Bitcoin private key, decrypted into memory. ⚠️ Live key material. */
59
+ btcSecretKey?: Uint8Array;
50
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
+ */
51
71
  export type MajikKeyDerivedIdentity = MajikKeyIdentity & {
72
+ /** AES-256-GCM-encrypted ML-KEM-768 secret key, freshly re-encrypted. */
52
73
  encryptedMlKemSecretKey: ArrayBuffer;
74
+ /** AES-256-GCM-encrypted Ed25519 secret key, freshly re-encrypted. */
53
75
  encryptedEdSecretKey: ArrayBuffer;
76
+ /** AES-256-GCM-encrypted ML-DSA-87 secret key, freshly re-encrypted. */
54
77
  encryptedMlDsaSecretKey: ArrayBuffer;
78
+ /** @experimental AES-256-GCM-encrypted Bitcoin secret key, freshly re-encrypted. */
79
+ encryptedBtcSecretKey?: ArrayBuffer;
55
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
+ */
56
87
  export interface SerializedIdentity {
57
88
  id: string;
58
- publicKey: string;
59
- 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. */
60
93
  encryptedPrivateKey?: string;
94
+ /** Base64 salt paired with `encryptedPrivateKey`. Omitted in some contexts — check before use. */
61
95
  salt?: string;
62
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
+ */
63
107
  export interface MajikKeyConstructorOptions {
64
108
  id: string;
65
109
  publicKey: CryptoKey | {
66
110
  raw: Uint8Array;
67
111
  };
68
- publicKeyBase64: string;
69
- fingerprint: string;
112
+ publicKeyBase64: MajikKeyAddress;
113
+ fingerprint: MajikKeyFingerprint;
70
114
  encryptedPrivateKey: ArrayBuffer;
71
115
  encryptedPrivateKeyBase64: string;
72
116
  salt: string;
117
+ /** Encrypted mnemonic-verification blob — see `MajikKeyJSON.backup`. */
73
118
  backup: string;
74
119
  label?: string;
75
120
  timestamp?: Date;
121
+ /** Defaults to legacy PBKDF2 (`KDF_VERSION.PBKDF2`) if omitted — see the private constructor. */
76
122
  kdfVersion?: KDF_VERSION;
77
123
  mlKemPublicKey: Uint8Array;
124
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
78
125
  mlKemSecretKey?: Uint8Array;
79
126
  encryptedMlKemSecretKey?: ArrayBuffer;
80
127
  encryptedMlKemSecretKeyBase64?: string;
128
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
81
129
  privateKey?: CryptoKey | {
82
130
  raw: Uint8Array;
83
131
  };
132
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
84
133
  privateKeyBase64?: string;
85
134
  edPublicKey?: Uint8Array;
86
135
  encryptedEdSecretKey?: ArrayBuffer;
@@ -88,10 +137,43 @@ export interface MajikKeyConstructorOptions {
88
137
  mlDsaPublicKey?: Uint8Array;
89
138
  encryptedMlDsaSecretKey?: ArrayBuffer;
90
139
  encryptedMlDsaSecretKeyBase64?: string;
140
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
91
141
  edSecretKey?: Uint8Array;
142
+ /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
92
143
  mlDsaSecretKey?: Uint8Array;
144
+ /** @experimental secp256k1 Bitcoin public key. */
145
+ btcPublicKey?: Uint8Array;
146
+ /** @experimental AES-256-GCM-encrypted Bitcoin private key. */
147
+ encryptedBtcSecretKey?: ArrayBuffer;
148
+ /** @experimental Base64 form of `encryptedBtcSecretKey`. */
149
+ encryptedBtcSecretKeyBase64?: string;
150
+ /** @experimental Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
151
+ btcSecretKey?: Uint8Array;
93
152
  mnemonicLanguage?: MnemonicLanguage;
94
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
+ */
95
177
  export declare class MajikKey {
96
178
  private readonly _id;
97
179
  private readonly _publicKey;
@@ -119,31 +201,108 @@ export declare class MajikKey {
119
201
  private _mlDsaSecretKey?;
120
202
  private _encryptedMlDsaSecretKey?;
121
203
  private _encryptedMlDsaSecretKeyBase64?;
204
+ /**
205
+ * @experimental
206
+ */
122
207
  private _solanaKeypairMaterial?;
208
+ /**
209
+ * @experimental
210
+ */
211
+ private _btcPublicKey?;
212
+ /**
213
+ * @experimental
214
+ */
215
+ private _btcSecretKey?;
216
+ /**
217
+ * @experimental
218
+ */
219
+ private _encryptedBtcSecretKey?;
220
+ /**
221
+ * @experimental
222
+ */
223
+ private _encryptedBtcSecretKeyBase64?;
123
224
  private constructor();
225
+ /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
124
226
  get id(): string;
125
- 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. */
126
230
  get publicKey(): CryptoKey | {
127
231
  raw: Uint8Array;
128
232
  };
129
- 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()`. */
130
236
  get label(): string;
237
+ /** BIP-39 wordlist language this account's mnemonic was generated/validated against. */
131
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
+ */
132
245
  get backup(): string;
246
+ /** Account creation time. */
133
247
  get timestamp(): Date;
248
+ /** KDF currently protecting every `encrypted*` field on this account: `1` = legacy PBKDF2, `2` = Argon2id. */
134
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()`. */
135
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). */
136
253
  get isLocked(): boolean;
254
+ /** `true` if private key material is currently decrypted in memory. The inverse of `isLocked`. */
137
255
  get isUnlocked(): boolean;
256
+ /** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. Always available, even when locked. */
138
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. */
139
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. */
140
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. */
141
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
+ */
269
+ get btcPublicKey(): Uint8Array | undefined;
270
+ /** @experimental `true` if this account has a stored Bitcoin keypair. */
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
+ */
142
277
  get metadata(): MajikKeyMetadata;
278
+ /** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. Always available, even when locked. */
143
279
  get edPublicKey(): Uint8Array | undefined;
280
+ /** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. Always available, even when locked. */
144
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. */
145
283
  get hasSigningKeys(): boolean;
146
- 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>;
147
306
  static fromJSON(json: MajikKeyJSON | string): MajikKey;
148
307
  /**
149
308
  * Export a fully unlocked MajikKey with all raw private keys.
@@ -197,17 +356,18 @@ export declare class MajikKey {
197
356
  /**
198
357
  * Import a MajikKey from a mnemonic-encrypted backup.
199
358
  *
200
- * This is the FULL MIGRATION PATH for old accounts — Argon2id + ML-KEM in one step:
201
- * 1. Verify the backup decrypts correctly (proves mnemonic is correct)
202
- * 2. Re-derive the complete identity from the mnemonic (X25519 + ML-KEM-768)
203
- * 3. Encrypt both private keys with Argon2id (v2) + fresh 32-byte salt
204
- * 4. Return a fully-upgraded MajikKey with hasMlKem: true, isArgon2id: true
205
- *
206
- * Old accounts without ML-KEM keys become fully post-quantum capable
207
- * automatically — no extra user steps. The mnemonic is the source of truth.
208
359
  */
209
- 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>;
210
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
+ */
211
371
  private static _deriveAndEncryptFromMnemonic;
212
372
  private static _encryptPrivateKey;
213
373
  /**
@@ -225,16 +385,41 @@ export declare class MajikKey {
225
385
  private static _deriveLegacyMnemonicKey;
226
386
  private static _exportKeyToBase64;
227
387
  /**
228
- * @experimental Blockchain/web3 integrations are experimental — this API
229
- * may change without notice. `undefined` when the MajikKey is locked or
230
- * has no Ed25519 signing key to derive from.
231
- *
232
- * By default `web3.solana` is DOMAIN-SEPARATED from the MajikKey's message
233
- * signing key (see deriveSolanaKeypairFromEdSecretKey). Use
234
- * `getSolanaKeypairMaterial({ reuseMessageKey: true })` if you specifically
235
- * want the identical key reused for Solana instead.
388
+ * @experimental
389
+ */
390
+ getBtcSecretKey(): Uint8Array;
391
+ /**
392
+ * @experimental
236
393
  */
237
394
  get web3(): MajikKeyWeb3Namespace | undefined;
395
+ /**
396
+ * @experimental True if this MajikKey can currently produce Bitcoin
397
+ * material (i.e. it's unlocked and has a Bitcoin secret key).
398
+ */
399
+ get hasBitcoinKeypair(): boolean;
400
+ /**
401
+ * @experimental Raw Bitcoin keypair material. Pass `{ standard: true }` to
402
+ * get the REAL BIP-84 mainnet key (recoverable in any standard wallet from
403
+ * the mnemonic alone) instead of Majik's default domain-separated key.
404
+ *
405
+ * NOTE: `{ standard: true }` re-derives from the raw seed on demand and is
406
+ * NOT the same key as `web3.bitcoin` (which is always the stored,
407
+ * domain-separated default) — it requires the mnemonic to reproduce again
408
+ * outside Majik, whereas the stored default does not.
409
+ */
410
+ getBitcoinKeypairMaterial(options?: BitcoinDerivationOptions): BitcoinKeypairMaterial;
411
+ /**
412
+ * @experimental Derive the REAL BIP-84 mainnet Bitcoin keypair straight
413
+ * from a mnemonic — for one-off export/verification. Does not require
414
+ * an unlocked MajikKey instance.
415
+ */
416
+ static deriveStandardBitcoinFromMnemonic(mnemonic: string, mnemonicLanguage?: MnemonicLanguage): Promise<BitcoinKeypairMaterial>;
417
+ /**
418
+ * @experimental WIF export of the default (domain-separated) Bitcoin key.
419
+ */
420
+ getBitcoinWIF(options?: {
421
+ compressed?: boolean;
422
+ }): string;
238
423
  /**
239
424
  * @experimental True if this MajikKey can currently produce a Solana
240
425
  * keypair (i.e. it's unlocked and has an Ed25519 signing key).