@majikah/majik-key 0.7.0 → 1.0.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 (55) hide show
  1. package/README.md +566 -175
  2. package/dist/core/backup/index.d.ts +4 -4
  3. package/dist/core/backup/index.js +3 -3
  4. package/dist/core/backup/majik-key-backup.d.ts +3 -3
  5. package/dist/core/backup/majik-key-backup.js +5 -5
  6. package/dist/core/backup/types.d.ts +1 -1
  7. package/dist/core/backup/utils.js +1 -1
  8. package/dist/core/backup/validator.d.ts +1 -1
  9. package/dist/core/backup/validator.js +1 -1
  10. package/dist/core/crypto/constants.d.ts +35 -11
  11. package/dist/core/crypto/constants.js +33 -11
  12. package/dist/core/crypto/crypto-provider.js +2 -2
  13. package/dist/core/crypto/encryption-engine.d.ts +7 -16
  14. package/dist/core/crypto/encryption-engine.js +27 -62
  15. package/dist/core/database/system/identity.d.ts +2 -2
  16. package/dist/core/database/system/identity.js +1 -1
  17. package/dist/core/keys/hkdf-recipe.d.ts +4 -0
  18. package/dist/core/keys/hkdf-recipe.js +26 -0
  19. package/dist/core/keys/key-id.d.ts +50 -0
  20. package/dist/core/keys/key-id.js +64 -0
  21. package/dist/core/keys/key-impls.d.ts +12 -0
  22. package/dist/core/keys/key-impls.js +163 -0
  23. package/dist/core/keys/key-store.d.ts +72 -0
  24. package/dist/core/keys/key-store.js +264 -0
  25. package/dist/core/keys/keypair-handle.d.ts +36 -0
  26. package/dist/core/keys/keypair-handle.js +43 -0
  27. package/dist/core/keys/registry.d.ts +16 -0
  28. package/dist/core/keys/registry.js +144 -0
  29. package/dist/core/keys/types.d.ts +47 -0
  30. package/dist/core/keys/types.js +1 -0
  31. package/dist/core/types.d.ts +12 -1
  32. package/dist/core/utils.d.ts +1 -1
  33. package/dist/core/utils.js +1 -1
  34. package/dist/core/validator.d.ts +1 -1
  35. package/dist/core/validator.js +1 -1
  36. package/dist/core/web3/bitcoin/bitcoin.d.ts +1 -1
  37. package/dist/core/web3/bitcoin/bitcoin.js +3 -3
  38. package/dist/core/web3/bitcoin/types.d.ts +1 -1
  39. package/dist/core/web3/ethereum/constants.d.ts +1 -0
  40. package/dist/core/web3/ethereum/constants.js +4 -0
  41. package/dist/core/web3/ethereum/ethereum.d.ts +21 -0
  42. package/dist/core/web3/ethereum/ethereum.js +91 -0
  43. package/dist/core/web3/ethereum/types.d.ts +32 -0
  44. package/dist/core/web3/ethereum/types.js +1 -0
  45. package/dist/core/web3/index.d.ts +10 -5
  46. package/dist/core/web3/index.js +7 -2
  47. package/dist/core/web3/solana/solana.d.ts +2 -2
  48. package/dist/core/web3/solana/solana.js +4 -4
  49. package/dist/core/web3/solana/types.d.ts +1 -1
  50. package/dist/core/web3/types.d.ts +4 -2
  51. package/dist/index.d.ts +16 -6
  52. package/dist/index.js +14 -5
  53. package/dist/majik-key.d.ts +179 -281
  54. package/dist/majik-key.js +622 -726
  55. package/package.json +20 -5
package/README.md CHANGED
@@ -2,36 +2,299 @@
2
2
 
3
3
  [![ZENODO](https://img.shields.io/badge/Read_the_Technical_Whitepaper_Here-1682D4?style=for-the-badge&logo=zenodo&logoColor=white)](https://doi.org/10.5281/zenodo.21339132)
4
4
 
5
-
6
5
  [![Developed by Zelijah](https://img.shields.io/badge/Developed%20by-Zelijah-red?logo=github&logoColor=white)](https://www.thezelijah.world) ![GitHub Sponsors](https://img.shields.io/github/sponsors/jedlsf?style=plastic&label=Sponsors&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fjedlsf)
7
6
 
7
+ **Majik Key** turns a single BIP-39 mnemonic into a complete, **multi-algorithm cryptographic identity** — classical and post-quantum encryption, classical and post-quantum signing, and (experimentally) Bitcoin, Ethereum and Solana keys — encrypted at rest and ready to plug into the rest of the Majikah ecosystem.
8
8
 
9
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21339132.svg)](https://doi.org/10.5281/zenodo.21339132) ![npm](https://img.shields.io/npm/v/@majikah/majik-key) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-key) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
9
10
 
10
- **Majik Key** turns a single BIP-39 mnemonic into a complete cryptographic identity — encryption, classical + post-quantum signing, and (experimentally) Bitcoin and Solana keys — encrypted at rest and ready to plug into the rest of the Majikah ecosystem.
11
+ ---
12
+
13
+ ## Table of Contents
14
+
15
+ - [Majik Key](#majik-key)
16
+ - [Table of Contents](#table-of-contents)
17
+ - [Why Majik Key](#why-majik-key)
18
+ - [What you get by default](#what-you-get-by-default)
19
+ - [Quick Start](#quick-start)
20
+ - [Multi-key support](#multi-key-support)
21
+ - [Supported keys](#supported-keys)
22
+ - [SLH-DSA performance](#slh-dsa-performance)
23
+ - [Choosing keys at creation](#choosing-keys-at-creation)
24
+ - [Reading keys](#reading-keys)
25
+ - [Adding keys later](#adding-keys-later)
26
+ - [Backward compatibility \& recovery](#backward-compatibility--recovery)
27
+ - [🚨 If anything ever goes wrong, re-import your seed phrase 🚨](#-if-anything-ever-goes-wrong-re-import-your-seed-phrase-)
28
+ - [Security Architecture](#security-architecture)
29
+ - [How keys are derived](#how-keys-are-derived)
30
+ - [Architecture](#architecture)
31
+ - [Powering the Majikah Ecosystem](#powering-the-majikah-ecosystem)
32
+ - [Majik Signature — Flagship](#majik-signature--flagship)
33
+ - [Majik Message](#majik-message)
34
+ - [Majik Buwiz](#majik-buwiz)
35
+ - [Majik Universal ID \& Majik SLink](#majik-universal-id--majik-slink)
36
+ - [Experimental Web3 Support](#experimental-web3-support)
37
+ - [What needs an extra install](#what-needs-an-extra-install)
38
+ - [Ethereum](#ethereum)
39
+ - [Two Bitcoin paths, on purpose](#two-bitcoin-paths-on-purpose)
40
+ - [Two Solana paths, on purpose](#two-solana-paths-on-purpose)
41
+ - [Installation](#installation)
42
+ - [API Reference](#api-reference)
43
+ - [Static methods](#static-methods)
44
+ - [Instance methods — state \& management](#instance-methods--state--management)
45
+ - [Instance methods — key registry](#instance-methods--key-registry)
46
+ - [Export \& integration methods](#export--integration-methods)
47
+ - [Instance getters](#instance-getters)
48
+ - [Web3 (experimental)](#web3-experimental)
49
+ - [⚠️ Deprecated (still supported until the next major)](#️-deprecated-still-supported-until-the-next-major)
50
+ - [Serialized shape](#serialized-shape)
51
+ - [Usage Examples](#usage-examples)
52
+ - [1. Secure backup \& recovery workflow](#1-secure-backup--recovery-workflow)
53
+ - [2. Multi-algorithm account](#2-multi-algorithm-account)
54
+ - [3. Scoped secret access with `withAutoLock`](#3-scoped-secret-access-with-withautolock)
55
+ - [4. Password verification before action](#4-password-verification-before-action)
56
+ - [5. Completing or extending an existing account](#5-completing-or-extending-an-existing-account)
57
+ - [6. Server-side secret injection (Dangerous JSON)](#6-server-side-secret-injection-dangerous-json)
58
+ - [7. Experimental Web3 usage](#7-experimental-web3-usage)
59
+ - [Upgrading from earlier versions](#upgrading-from-earlier-versions)
60
+ - [Security Best Practices](#security-best-practices)
61
+ - [Ecosystem](#ecosystem)
62
+ - [License](#license)
63
+ - [Author](#author)
64
+ - [Contact](#contact)
11
65
 
12
- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21339132.svg)](https://doi.org/10.5281/zenodo.21339132) ![npm](https://img.shields.io/npm/v/@majikah/majik-key) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-key) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
13
66
 
14
67
  ---
15
68
 
16
69
  ## Why Majik Key
17
70
 
18
- - **One seed, one identity, five key pairs.** A 12- or 24-word mnemonic deterministically derives X25519, ML-KEM-768, Ed25519, ML-DSA-87, and (by default) a domain-separated Bitcoin key — all reproducible from the mnemonic alone.
19
- - **Post-quantum from day one.** Every account gets an ML-KEM-768 (FIPS-203) encryption keypair and an ML-DSA-87 signing keypair alongside their classical counterparts (X25519, Ed25519) — no separate migration project required later.
20
- - **Encrypted at rest, always.** Private key material is never persisted in plaintext. Everything is AES-256-GCM encrypted using a key derived with Argon2id.
71
+ - **One seed, one identity, as many keys as you need.** A 12- or 24-word mnemonic deterministically derives every keypair on the account. Lose the device, keep the phrase, and everything comes back.
72
+ - **A key registry, not a fixed key set.** Every key has a namespaced id (`pq:ml-dsa-87`, `classic:ed25519`, `web3:eth`, …). New algorithms are added to the registry without ever changing the keys you already have.
73
+ - **Post-quantum from day one.** Every account gets ML-KEM-768 (FIPS 203) for encryption and ML-DSA-87 (FIPS 204) for signing alongside X25519 and Ed25519 — no separate migration project later.
74
+ - **Encrypted at rest, always.** Private key material is never persisted in plaintext. Everything is AES-256-GCM encrypted with a key derived by Argon2id.
75
+ - **Backward compatible.** Accounts created by earlier versions load and work unchanged. If anything ever goes wrong, re-importing the same seed phrase restores the same keys.
21
76
  - **Local-first.** Key generation and derivation run entirely offline — no network request is made in the process, verifiable directly in source.
22
77
  - **Built for the Majikah ecosystem**, but usable standalone in any TypeScript/JavaScript project.
23
78
 
24
79
  ---
25
80
 
81
+ ## What you get by default
82
+
83
+ Every new account automatically contains **these four keypairs** — you don't ask for them and you can't create an account without them:
84
+
85
+ | Key id | Algorithm | Purpose |
86
+ | :---------------- | :-------------------- | :--------------------------------------------- |
87
+ | `classic:x25519` | X25519 | Identity, fingerprint, classical key agreement |
88
+ | `classic:ed25519` | Ed25519 | Classical signing |
89
+ | `pq:ml-kem-768` | ML-KEM-768 (FIPS 203) | Post-quantum encryption (key encapsulation) |
90
+ | `pq:ml-dsa-87` | ML-DSA-87 (FIPS 204) | Post-quantum signing |
91
+
92
+ These four are the **core set** (`CORE_KEYS`). A Solana keypair (`web3:sol`) is also always available as a *derived view* of your Ed25519 key — it costs no extra storage.
93
+
94
+ **Want more? Use `KeyId` and pass it to `create()` via `keys`:**
95
+
96
+ ```typescript
97
+ import { MajikKey, KeyId } from '@majikah/majik-key';
98
+
99
+ const key = await MajikKey.create(mnemonic, passphrase, 'My Account', {
100
+ keys: [KeyId.ETH, KeyId.BTC, KeyId.ML_KEM_1024], // extras ON TOP of the core four
101
+ });
102
+ ```
103
+
104
+ `keys` is **additive**: the core four are always included, duplicates collapse, and an unusable id (reserved, unsupported, unknown) is rejected up front, before any derivation work happens.
105
+
106
+ ---
107
+
108
+ ## Quick Start
109
+
110
+ ```typescript
111
+ import { MajikKey, KeyId } from '@majikah/majik-key';
112
+
113
+ // 1. Generate & create — the core four keypairs are derived automatically
114
+ const mnemonic = await MajikKey.generateMnemonic(); // 12 words (128-bit)
115
+ const key = await MajikKey.create(mnemonic, 'super-secure-passphrase', 'My PQ Account');
116
+
117
+ // 2. Identity
118
+ console.log('Fingerprint:', key.fingerprint);
119
+ console.log('Unlocked?', key.isUnlocked); // true — create() returns an unlocked key
120
+ console.log(key.availableKeys());
121
+ // ['classic:x25519', 'classic:ed25519', 'pq:ml-kem-768', 'pq:ml-dsa-87', 'web3:sol']
122
+
123
+ // 3. Use any key by id
124
+ const publicKey = key.getPublicKey(KeyId.ML_DSA_87); // Uint8Array — works even when locked
125
+ const privateKey = key.getPrivateKey(KeyId.ML_DSA_87); // Uint8Array — throws if locked
126
+
127
+ // 4. Lock to purge every secret from memory (zeroized in place)
128
+ key.lock();
129
+
130
+ // 5. Unlock again when you need cryptographic operations
131
+ await key.unlock('super-secure-passphrase');
132
+
133
+ // 6. Safe storage — toJSON()/toString() never contain raw private keys
134
+ localStorage.setItem('myKey', key.toString());
135
+ const restored = MajikKey.fromJSON(localStorage.getItem('myKey')!); // starts locked
136
+ ```
137
+
138
+ ---
139
+
140
+ ## Multi-key support
141
+
142
+ ### Supported keys
143
+
144
+ Every key is addressed by a namespaced id, exposed as the `KeyId` constant (`<family>:<name>`).
145
+
146
+ | Family | `KeyId` | Algorithm | Status |
147
+ | :------------- | :------------------------------------------------ | :----------------------------- | :--------------------------------------------------------------- |
148
+ | classic | `KeyId.X25519`, `KeyId.ED25519` | X25519, Ed25519 | ✅ core — always created |
149
+ | pq (KEM) | `KeyId.ML_KEM_768` | ML-KEM-768 | ✅ core — always created |
150
+ | pq (KEM) | `KeyId.ML_KEM_512`, `KeyId.ML_KEM_1024` | ML-KEM-512 / 1024 | ✅ stable, opt-in |
151
+ | pq (signature) | `KeyId.ML_DSA_87` | ML-DSA-87 | ✅ core — always created |
152
+ | pq (signature) | `KeyId.ML_DSA_44`, `KeyId.ML_DSA_65` | ML-DSA-44 / 65 | ✅ stable, opt-in |
153
+ | pq (signature) | `KeyId.SLH_DSA_SHAKE_128F`, … (12 parameter sets) | SLH-DSA (FIPS 205, hash-based) | ✅ stable, opt-in — see [performance note](#slh-dsa-performance) |
154
+ | pq (signature) | `KeyId.FALCON_512`, `KeyId.FALCON_1024` | Falcon (NIST Round 3) | 🧪 experimental — **not** FIPS 206 |
155
+ | web3 | `KeyId.BTC` | Bitcoin (secp256k1, BIP-32/84) | 🧪 experimental, opt-in |
156
+ | web3 | `KeyId.ETH` | Ethereum (secp256k1, BIP-44) | 🧪 experimental, opt-in |
157
+ | web3 | `KeyId.SOL` | Solana (Ed25519-derived) | 🧪 experimental, derived view — always available |
158
+ | pq (KEM) | `KeyId.HQC_128`, `_192`, `_256` | HQC | ⏳ reserved — standard not final, no vetted JS implementation yet |
159
+ | pq (signature) | `KeyId.FN_DSA_512`, `KeyId.FN_DSA_1024` | FN-DSA (FIPS 206) | ⏳ reserved until FIPS 206 is final |
160
+ | pq (signature) | `KeyId.LMS` | LMS / HSS (SP 800-208) | 🚫 not supported — see below |
161
+
162
+ `MajikKey.supportedKeys()` returns every id this library version can actually create.
163
+
164
+ **Why LMS is not offered.** LMS is a *stateful* signature scheme: every signature consumes a one-time key index that must never be reused. Mnemonic recovery, backups, restores and multi-device use all reset that state, which makes index reuse — and with it, total forgery — likely. That directly conflicts with "recover everything from the mnemonic," so Majik Key refuses it rather than offer a footgun.
165
+
166
+ **Why Falcon is named `falcon`, not `fn-dsa`.** What libraries ship today is Falcon as submitted to NIST Round 3. The final FN-DSA (FIPS 206) is expected to be incompatible, so it will get its own ids later instead of silently changing what `pq:falcon-*` means.
167
+
168
+ #### SLH-DSA performance
169
+
170
+ SLH-DSA key generation in JavaScript takes **~60–330 ms for the `f` ("fast") variants** but **~4–8 seconds for the `s` ("small") variants**. The `s` variants have smaller signatures (~8 KB vs ~17 KB) but will block a browser tab during `create()`/`addKeys()` — run them in a Web Worker. We recommend `KeyId.SLH_DSA_SHAKE_128F` as the default choice and `KeyId.SLH_DSA_SHAKE_256F` for high assurance.
171
+
172
+ ### Choosing keys at creation
173
+
174
+ ```typescript
175
+ const key = await MajikKey.create(mnemonic, passphrase, 'Label', {
176
+ mnemonicLanguage: 'en',
177
+ keys: [KeyId.ML_KEM_1024, KeyId.ML_DSA_65, KeyId.SLH_DSA_SHAKE_128F, KeyId.ETH],
178
+ });
179
+ ```
180
+
181
+ The same `keys` option works on `fromMnemonicJSON()` and `importFromMnemonicBackup()`.
182
+
183
+ ### Reading keys
184
+
185
+ ```typescript
186
+ // Presence checks — work while the account is locked
187
+ key.hasKey(KeyId.ETH); // boolean
188
+ key.hasKeys([KeyId.ED25519, KeyId.ML_DSA_87]); // boolean (all present?)
189
+ key.missingKeys([KeyId.ETH, KeyId.ML_KEM_1024]);// KeyId[] that are NOT on this account
190
+ key.isCoreComplete; // true when all four core keys exist
191
+
192
+ // Inventory
193
+ key.availableKeys(); // KeyId[] in canonical order
194
+ key.availableKeys({ family: 'pq' }); // filter by namespace: 'classic' | 'pq' | 'web3'
195
+ key.listKeys(); // [{ id, family, purpose, kind, status, publicKeyBase64 }] — no secrets
196
+
197
+ // Bytes
198
+ key.getPublicKey(KeyId.ML_KEM_1024); // Uint8Array — works while locked
199
+ key.getPrivateKey(KeyId.ML_KEM_1024); // Uint8Array — throws if locked
200
+
201
+ // Or a handle with .public / .private
202
+ const kp = key.getKeypair(KeyId.ML_DSA_65);
203
+ kp.public; // Uint8Array
204
+ kp.publicBase64; // string
205
+ kp.private; // Uint8Array — throws if the account is locked
206
+ kp.algorithm; // 'pq:ml-dsa-65' kp.family; kp.purpose; kp.status; kp.isUnlocked
207
+ ```
208
+
209
+ A `getKeypair()` handle **reads live from the account** instead of copying key bytes, so a handle you grabbed before `lock()` can never expose stale or zeroized material — after `lock()`, `.private` throws and `.public` still works.
210
+
211
+ ### Adding keys later
212
+
213
+ New algorithms are derived from the seed, and **the seed is never stored** — so adding a key to an existing account requires the mnemonic (and the current passphrase):
214
+
215
+ ```typescript
216
+ const added = await key.addKeys([KeyId.ETH, KeyId.ML_KEM_1024], mnemonic, passphrase);
217
+ console.log(added); // ['pq:ml-kem-1024', 'web3:eth'] — only what was actually missing
218
+ ```
219
+
220
+ `addKeys()` is safe by construction: the passphrase must decrypt your X25519 key **and** the mnemonic must reproduce your account's fingerprint before anything is added. It's idempotent, skips keys you already have, and works on a locked account (the new secret stays encrypted). The account must be on Argon2id — call `migrate(passphrase)` first on very old accounts.
221
+
222
+ ---
223
+
224
+ ## Backward compatibility & recovery
225
+
226
+ **Everything created by earlier versions keeps working — nothing to migrate by hand.**
227
+
228
+ | You have… | What happens |
229
+ | :----------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
230
+ | JSON from an older version (no `keys` field) | `fromJSON()` upgrades it **automatically, in memory** — no passphrase or mnemonic needed. Call `toJSON()` to persist the upgraded shape. |
231
+ | An account on the legacy PBKDF2 KDF (v1) | Still unlocks. `updatePassphrase()` or `migrate()` moves it to Argon2id. |
232
+ | An account missing newer core keys (e.g. no ML-KEM/Ed25519/ML-DSA) | Loads fine; `key.missingKeys()` lists what's absent and `addKeys(CORE_KEYS, mnemonic, passphrase)` completes it. |
233
+ | A mnemonic backup created before this version | `importFromMnemonicBackup()` still reads it (legacy salts are retained read-only). |
234
+ | An older-version call like `key.getEdSecretKey()` | Still works — marked `@deprecated`, a thin wrapper over the registry. |
235
+ | Entries written by a *newer* library version | Preserved untouched on a round-trip, never dropped (they're just not usable until you upgrade). |
236
+
237
+ ## <h1 style="color:#d1242f;">🚨 If anything ever goes wrong, re-import your seed phrase 🚨</h1>
238
+
239
+
240
+
241
+ >
242
+ > Every key is **deterministically derived** from the mnemonic. Re-importing the same seed phrase always reproduces the same keys, the same fingerprint and the same addresses — under any passphrase you choose.
243
+
244
+ ```typescript
245
+ // Same mnemonic → identical keys.
246
+ // Pass the same extra `keys` you used before.
247
+ const restored = await MajikKey.create(
248
+ mnemonic,
249
+ newPassphrase,
250
+ 'Restored', {
251
+ keys: [KeyId.ETH, KeyId.ML_KEM_1024],
252
+ });
253
+
254
+ // Or verify against an existing backup blob first, then re-derive:
255
+ const restored2 = await MajikKey.importFromMnemonicBackup(
256
+ backup,
257
+ mnemonic,
258
+ newPassphrase,
259
+ 'Restored',
260
+ {
261
+ keys: [KeyId.ETH, KeyId.ML_KEM_1024],
262
+ });
263
+
264
+ ```
265
+
266
+ > Re-importing derives the **core four plus whatever you pass in `keys`**. If your account had extra keys, pass them again — or call `addKeys()` afterwards. Accounts created before this version had **Bitcoin by default**; to get it back on a re-import, pass `keys: [KeyId.BTC]` (or the legacy `deriveBitcoin: true`).
267
+
268
+ The legacy derivation recipes for X25519, Ed25519, ML-KEM-768, ML-DSA-87 and Bitcoin are **frozen and pinned by known-answer test vectors**: no release can change the keys a given mnemonic produces.
269
+
270
+ ---
271
+
26
272
  ## Security Architecture
27
273
 
28
- - **Encrypted at rest, not "hashed."** Private keys are **AES-256-GCM encrypted**, using a 256-bit key **derived via Argon2id** from your passphrase. (Argon2id is a key-derivation function, not applied to the private key directly — the private key itself is encrypted, not hashed.)
29
- - **Argon2id KDF (v2), memory-hard by design.** Passphrase-based encryption uses Argon2id at **64 MB memory / 3 iterations / 4 parallel lanes**, tuned to resist GPU/ASIC brute-force attacks. A WASM implementation (`hash-wasm`) is used when available in the runtime, with an automatic, transparent fallback to a pure-JS implementation (`@noble/hashes`) — output is bit-identical either way, so switching implementations never breaks decryption.
30
- - **Post-quantum ready.** ML-KEM-768 (FIPS-203) is derived from the full 64-byte BIP-39 seed for encryption/key-encapsulation, and ML-DSA-87 is derived from a domain-separated hash of that same seed for signing — both deterministic and fully recoverable from the mnemonic.
31
- - **Legacy KDF read support.** Older accounts encrypted with KDF v1 (PBKDF2-SHA256, 200k–250k iterations) can still be unlocked. New accounts, and any account whose passphrase is changed via `updatePassphrase()`, always land on Argon2id (v2).
32
- - **Full migration path.** `importFromMnemonicBackup()` re-derives a complete identity straight from the mnemonic — X25519, ML-KEM-768, Ed25519, ML-DSA-87, and Bitcoin — and re-encrypts everything with Argon2id in one step, so an old account becomes fully post-quantum capable automatically. A lighter `migrate()` method is also available if you only want to upgrade the KDF version without re-deriving the newer key types.
33
- - **Isomorphic by design.** Uses native WebCrypto (ECDH/X25519) where the runtime supports it, and transparently falls back to a raw keypair representation where it doesn't (e.g. Node environments without X25519 in WebCrypto) — the public API is identical either way.
34
- - **Multi-language mnemonics.** BIP-39 wordlists for English, French, Spanish, Italian, Japanese, Korean, Czech, Portuguese, Simplified Chinese, and Traditional Chinese are supported, lazy-loaded per language so you only pay for the ones you use.
274
+ - **Encrypted at rest, not "hashed."** Private keys are **AES-256-GCM encrypted** with a 256-bit key **derived via Argon2id** from your passphrase. (Argon2id is a key-derivation function; the private key itself is encrypted, not hashed.)
275
+ - **One KDF run per operation.** `create()`, `unlock()` and `updatePassphrase()` derive the vault key **once** and use it for every key on the account, so unlock time does not grow as you add algorithms. Each key is sealed with its own random IV.
276
+ - **Argon2id KDF (v2), memory-hard by design.** Passphrase encryption uses Argon2id at **64 MB memory / 3 iterations / 4 parallel lanes**. A WASM implementation (`hash-wasm`) is used when available, with an automatic, transparent fallback to pure JS (`@noble/hashes`) — output is bit-identical either way.
277
+ - **Atomic by design.** `unlock()` is all-or-nothing: if any key fails to decrypt, the account stays fully locked. `updatePassphrase()` and `migrate()` decrypt everything first and only then commit — a failure can never leave a new salt paired with ciphertext from the old one.
278
+ - **Zeroization.** `lock()` zeroizes secret buffers in place, and intermediate key material is wiped after use.
279
+ - **Post-quantum ready.** ML-KEM-768 is derived from the full 64-byte BIP-39 seed; ML-DSA-87 from a domain-separated hash of it. Every algorithm added since uses a documented, versioned HKDF recipe (below).
280
+ - **Legacy KDF read support.** Accounts encrypted with KDF v1 (PBKDF2-SHA256) can still be unlocked. New accounts, and any account whose passphrase changes, always land on Argon2id.
281
+ - **Multi-language mnemonics.** BIP-39 wordlists for English, French, Spanish, Italian, Japanese, Korean, Czech, Portuguese, Simplified Chinese and Traditional Chinese, lazy-loaded per language. The language is remembered on the account and survives JSON, backup and import round-trips.
282
+ - **Isomorphic by design.** Uses native WebCrypto where the runtime supports it and a raw-key fallback where it doesn't; the public API is identical either way.
283
+
284
+ ### How keys are derived
285
+
286
+ | Keys | Recipe | Stability |
287
+ | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- | :----------------------------- |
288
+ | X25519 | converted (ed2curve) from the Ed25519 key | 🔒 frozen (`legacy-v1`) |
289
+ | Ed25519 | BIP-39 seed `[0..32]` | 🔒 frozen |
290
+ | ML-KEM-768 | the full 64-byte BIP-39 seed | 🔒 frozen |
291
+ | ML-DSA-87 | `sha256(seed ‖ "MajikSignatureSeedDSA")` | 🔒 frozen |
292
+ | Bitcoin | BIP-32 `m/84'/1971'/0'/0/0` (Majik domain path) | 🔒 frozen |
293
+ | Ethereum | BIP-32 `m/44'/60'/0'/0/0` (standard path) | stable |
294
+ | Solana | `sha256(edSeed ‖ "MajikKeySolanaSeed")` — derived on demand | stable |
295
+ | Everything else (ML-KEM-512/1024, ML-DSA-44/65, SLH-DSA, Falcon) | `HKDF-SHA512(seed, salt = "MajikKey/hkdf-sha512/v1", info = "majik/v1/<key id>")` → the algorithm's seed | stable, pinned by test vectors |
296
+
297
+ Because each algorithm gets its own HKDF `info` string, no two keys ever receive related seed material, and adding a new algorithm can never change an existing key. Each stored key also records its own `derivation` recipe in the JSON.
35
298
 
36
299
  ---
37
300
 
@@ -39,47 +302,40 @@
39
302
 
40
303
  ```mermaid
41
304
  flowchart TD
42
- A[12/24-word BIP-39 Seed Phrase] --> B[Majik Key]
43
-
44
- %% Signing branch
45
- B --> S[Signing]
46
- S --> S1[Ed25519]
47
- S --> S2[ML-DSA-87]
48
-
49
- %% Encryption branch
50
- B --> E[Encryption]
51
- E --> E1[ML-KEM-768]
52
- E --> E2[AES-256-GCM]
53
-
54
- %% Identity branch
55
- B --> I[Identity]
56
- I --> I1[BIP-39]
57
- I --> I2[X25519]
58
-
59
- %% Experimental Web3 branch
60
- B -.-> W[Web3 - Experimental]
61
- W -.-> W1[Bitcoin - BIP-32/84]
62
- W -.-> W2[Solana - Ed25519-derived]
63
-
64
- %% Products (fan-in)
65
- S1 --> P1[Majik Signature]
66
- S2 --> P1
67
-
68
- S1 --> P2[Majik Buwiz]
69
- S2 --> P2
70
- E1 --> P2
71
- E2 --> P2
72
- I1 --> P2
73
- I2 --> P2
74
-
75
-
76
- E1 --> P3[Majik Message]
77
- E2 --> P3
78
-
79
- I1 --> P4[Majik Universal ID]
80
- I2 --> P4
81
-
82
- P4 --> P5[Majik SLink]
305
+ A["12/24-word BIP-39 seed phrase"] --> B["Majik Key · key registry"]
306
+
307
+ B --> C["Core — always created"]
308
+ C --> C1["X25519 · identity & key agreement"]
309
+ C --> C2["Ed25519 · classical signing"]
310
+ C --> C3["ML-KEM-768 · post-quantum encryption"]
311
+ C --> C4["ML-DSA-87 · post-quantum signing"]
312
+
313
+ B --> O["Optional — pass KeyId in keys"]
314
+ O --> O1["ML-KEM-512 / 1024"]
315
+ O --> O2["ML-DSA-44 / 65"]
316
+ O --> O3["SLH-DSA · 12 parameter sets"]
317
+ O --> O4["Falcon-512 / 1024 · experimental"]
318
+
319
+ B -.-> W["Web3 · experimental"]
320
+ W -.-> W1["Bitcoin · BIP-32/84"]
321
+ W -.-> W2["Ethereum · BIP-44"]
322
+ W -.-> W3["Solana · Ed25519-derived"]
323
+
324
+ C2 --> P1["Majik Signature"]
325
+ C4 --> P1
326
+
327
+ C2 --> P2["Majik Buwiz"]
328
+ C3 --> P2
329
+ C4 --> P2
330
+ C1 --> P2
331
+ W1 -.-> P2
332
+ W2 -.-> P2
333
+ W3 -.-> P2
334
+
335
+ C3 --> P3["Majik Message"]
336
+
337
+ C1 --> P4["Majik Universal ID"]
338
+ P4 --> P5["Majik SLink"]
83
339
  ```
84
340
 
85
341
  Your Majik Key is generated entirely offline. No network request is made during key creation — verifiable in source code.
@@ -88,7 +344,7 @@ Your Majik Key is generated entirely offline. No network request is made during
88
344
 
89
345
  ## Powering the Majikah Ecosystem
90
346
 
91
- Majik Key is the shared identity layer underneath every Majikah product. Here's what each one draws from it.
347
+ Majik Key is the shared identity layer underneath every Majikah product. The **core four keys** are everything these products need — extra keypairs are opt-in.
92
348
 
93
349
  ### [Majik Signature](https://majikah.solutions/products/majik-signature) — Flagship
94
350
 
@@ -98,7 +354,7 @@ Majik Key is the shared identity layer underneath every Majikah product. Here's
98
354
 
99
355
  [![Majik Signature Hero](https://github.com/user-attachments/assets/781bb778-9535-4b1f-bbc5-820550ecc864)](https://signature.majikah.solutions)
100
356
 
101
- Majik Signature consumes a Majik Key's **Ed25519** and **ML-DSA-87** signing keypairs to require *both* a classical and a post-quantum signature before a file verifies — hybrid security with forward secrecy against future quantum attacks on either scheme alone.
357
+ Majik Signature consumes a Majik Key's **Ed25519** and **ML-DSA-87** keypairs to require *both* a classical and a post-quantum signature before a file verifies — hybrid security that holds even if one scheme is later broken.
102
358
 
103
359
  ```typescript
104
360
  import { MajikKey } from '@majikah/majik-key';
@@ -125,9 +381,9 @@ console.log("File sealed at:", sealInfo.sealTimestamp);
125
381
 
126
382
  **Post-quantum secure messaging envelopes.**
127
383
 
128
- Majik Key derives an ML-KEM-768 keypair specifically so it can be used for Majik Message's v3 secure envelopes — the ML-KEM-768 keypair handles post-quantum key encapsulation, and AES-256-GCM handles the actual payload encryption once a shared secret is established.
384
+ Majik Key's ML-KEM-768 keypair is used for Majik Message's v3 secure envelopes: ML-KEM-768 handles post-quantum key encapsulation, and AES-256-GCM handles the payload once a shared secret is established.
129
385
 
130
- Majik Key ships a direct integration point for this: `toMajikMessageIdentity()` converts an unlocked key into a `MajikMessageIdentity`, ready to hand to Majik Message.
386
+ `toMajikMessageIdentity()` converts an unlocked key into a `MajikMessageIdentity`, ready to hand to Majik Message.
131
387
 
132
388
  ```typescript
133
389
  import { MajikKey } from '@majikah/majik-key';
@@ -143,41 +399,58 @@ const identity = await key.toMajikMessageIdentity(user, {
143
399
 
144
400
  **Multi-key custody built on the full Majik Key stack.**
145
401
 
146
- Majik Buwiz is built on Majik Key's complete key set: Ed25519/ML-DSA-87 for signing, ML-KEM-768/AES-256-GCM for encryption, and X25519/BIP-39 for identity — plus the experimental Bitcoin and Solana keys described below for multi-chain support. Everything a Buwiz account needs is derivable from, and recoverable with, the same mnemonic.
402
+ Majik Buwiz is built on Majik Key's complete key set: Ed25519 / ML-DSA-87 for signing, ML-KEM-768 / AES-256-GCM for encryption, and X25519 / BIP-39 for identity — plus the optional Bitcoin, Ethereum and Solana keys for multi-chain support. Everything a Buwiz account needs is derivable from, and recoverable with, the same mnemonic.
147
403
 
148
404
  ### Majik Universal ID & Majik SLink
149
405
 
150
406
  **A portable identity primitive, and shareable links built on top of it.**
151
407
 
152
- Majik Universal ID is built on the Identity branch of Majik Key — the BIP-39-derived X25519 keypair, public key, and fingerprint, exportable via `toContact()` as a `MajikContact` for use across apps. Majik SLink extends that identity layer downstream, per the architecture above. Both are separate Majikah packages; consult [majikah.solutions](https://majikah.solutions) for the latest on their APIs.
408
+ Majik Universal ID is built on the identity branch of Majik Key — the BIP-39-derived X25519 keypair, public key and fingerprint, exportable via `toContact()` as a `MajikContact` for use across apps. Majik SLink extends that identity layer downstream. Both are separate Majikah packages; consult [majikah.solutions](https://majikah.solutions) for the latest on their APIs.
153
409
 
154
410
  ---
155
411
 
156
412
  ## Experimental Web3 Support
157
413
 
158
- Majik Key can derive **Bitcoin** and **Solana** key material directly from the same mnemonic. This is marked experimental — the shape of the `web3` namespace may change without a major version bump.
414
+ Majik Key can derive **Bitcoin**, **Ethereum** and **Solana** key material from the same mnemonic. This is marked experimental — the shape of the `web3` namespace may change without a major version bump.
159
415
 
160
- ### What's built in vs. what needs an extra install
416
+ | Chain | How it's provided | Opt in with |
417
+ | :------- | :----------------------------------------------------------------- | :------------------ |
418
+ | Bitcoin | Stored, encrypted key (BIP-32/84, Majik domain path by default) | `keys: [KeyId.BTC]` |
419
+ | Ethereum | Stored, encrypted key (BIP-44 **standard** path) | `keys: [KeyId.ETH]` |
420
+ | Solana | **Derived on demand** from your Ed25519 key — nothing extra stored | always available |
161
421
 
162
- - **Bitcoin key derivation is automatic.** Every account created via `create()` or `importFromMnemonicBackup()` also derives and encrypts a Bitcoin keypair (real BIP-32 HD derivation off the raw 64-byte seed, using a Majik-specific domain-separated path by default). This works out of the box — no extra install needed for the private key, public key, or WIF export.
163
- - **Solana key derivation is on-demand.** Rather than storing a separate Solana keypair, Majik Key derives it deterministically from your Ed25519 signing key each time you access `key.web3.solana` (and caches it in memory for as long as the key stays unlocked). The base58 Solana address also works with no extra install.
164
- - **The optional peer dependencies are only needed for chain-native address/transaction objects:**
422
+ > **Change from earlier versions:** Bitcoin is no longer derived by default. Pass `keys: [KeyId.BTC]` (or the deprecated `deriveBitcoin: true`) to create it. Existing accounts that already have a Bitcoin key keep it.
165
423
 
166
- | Chain | Peer dependency | Needed for |
167
- | :------ | :------------------ | :--------------------------------------------------------- |
168
- | Bitcoin | `@scure/btc-signer` | Native SegWit (bech32) address encoding, PSBT construction |
169
- | Solana | `@solana/kit` | Real `KeyPairSigner` instances, kit-native `Address` type |
424
+ ### What needs an extra install
170
425
 
171
- ```bash
172
- npm install @scure/btc-signer # for Bitcoin addresses
173
- npm install @solana/kit # for Solana signer/address objects
426
+ Raw key bytes, WIF export, message signing, and Ethereum/Solana addresses work with **zero extra dependencies**. Optional peer dependencies are only needed for chain-native address/transaction objects:
427
+
428
+ | Chain | Peer dependency | Needed for |
429
+ | :------- | :------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
430
+ | Bitcoin | `@scure/btc-signer` | Native SegWit (bech32) address encoding, PSBT construction |
431
+ | Solana | `@solana/kit` | Real `KeyPairSigner` instances, kit-native `Address` type |
432
+ | Ethereum | *(none)* | Address (EIP-55), `signHash`, EIP-191 `signMessage` are built in. EIP-712 typed data and transaction building are not included yet — use viem/ethers with the exported key. |
433
+
434
+ ### Ethereum
435
+
436
+ Ethereum uses the **standard path** `m/44'/60'/0'/0/0`, so the address is **the same one MetaMask, Ledger or Trezor show for the same mnemonic**.
437
+
438
+ ```typescript
439
+ const key = await MajikKey.create(mnemonic, passphrase, 'Wallet', { keys: [KeyId.ETH] });
440
+
441
+ key.getEthereumAddress(); // '0xf39F…' (EIP-55) — public-only, works while locked
442
+ key.getEthereumPrivateKeyHex(); // '0x…' — paste into any wallet's "import private key"
443
+
444
+ const eth = key.web3?.ethereum; // present when unlocked
445
+ eth?.signMessage('hello'); // EIP-191 personal_sign → { r, s, v, recovery, serialized }
446
+ eth?.signHash(hash32); // sign a 32-byte hash (tx hash, EIP-712 digest)
174
447
  ```
175
448
 
176
- Everything else — raw key bytes, WIF export, message signing (ECDSA/Schnorr for Bitcoin, Ed25519 for Solana), and base58 Solana addresses — works with zero extra dependencies.
449
+ > ⚠️ Because the path is standard, **anyone holding the mnemonic controls the funds** at that address. There is no Majik-specific separation — that's what makes it wallet-compatible.
177
450
 
178
451
  ### Two Bitcoin paths, on purpose
179
452
 
180
- By default, Bitcoin keys use `MAJIK_BITCOIN_DOMAIN_PATH` — a real, standard BIP-32 derivation, but not the path a generic wallet would derive by default, so it stays effectively private to Majik. Pass `{ standard: true }` to derive the actual BIP-84 mainnet path instead — the address any standard wallet would show for the same mnemonic:
453
+ By default Bitcoin keys use `MAJIK_BITCOIN_DOMAIN_PATH` (`m/84'/1971'/0'/0/0`) — real BIP-32 derivation, but not the path a generic wallet would derive, so it stays effectively private to Majik. For the actual BIP-84 mainnet key (the address any standard wallet shows for the mnemonic):
181
454
 
182
455
  ```typescript
183
456
  // Majik's default (domain-separated, stored on the key)
@@ -189,7 +462,7 @@ const standardBtc = await MajikKey.deriveStandardBitcoinFromMnemonic(mnemonic);
189
462
 
190
463
  ### Two Solana paths, on purpose
191
464
 
192
- By default (`deriveSolanaKeypairFromEdSecretKey`), the Solana keypair is domain-separated from your Ed25519 message-signing key via `SHA256(edSeed || "MajikKeySolanaSeed")`, so the same private key never secures two different protocols. You can opt into reusing your Ed25519 message-signing key directly instead:
465
+ By default (`deriveSolanaKeypairFromEdSecretKey`) the Solana keypair is domain-separated from your Ed25519 message-signing key via `SHA256(edSeed ‖ "MajikKeySolanaSeed")`, so the same private key never secures two protocols. You can opt into reusing the Ed25519 key directly:
193
466
 
194
467
  ```typescript
195
468
  // Recommended: domain-separated Solana key
@@ -207,122 +480,198 @@ const reusedAddress = key.getSolanaAddress({ reuseMessageKey: true });
207
480
  npm install @majikah/majik-key
208
481
  ```
209
482
 
210
- ---
211
-
212
- ## Quick Start (Core Identity)
483
+ Optional peer dependencies (only for the features listed in the Web3 table above):
213
484
 
214
- ```typescript
215
- import { MajikKey } from '@majikah/majik-key';
216
-
217
- // 1. Generate & Create
218
- const mnemonic = await MajikKey.generateMnemonic(); // 12 words (128-bit)
219
- const key = await MajikKey.create(mnemonic, 'super-secure-passphrase', 'My PQ Account');
220
-
221
- // 2. Access Identity
222
- console.log('Fingerprint:', key.fingerprint);
223
- console.log('Key ID:', key.id);
224
- console.log('Unlocked?', key.isUnlocked); // true — create() returns an already-unlocked key
225
-
226
- // 3. Lock to purge private key material from memory
227
- key.lock();
228
-
229
- // 4. Unlock again when cryptographic operations are needed
230
- await key.unlock('super-secure-passphrase');
231
- const privateKeyBase64 = key.getPrivateKeyBase64();
232
-
233
- // 5. Safe storage — toJSON()/toString() never include raw private keys
234
- localStorage.setItem('myKey', key.toString());
485
+ ```bash
486
+ npm install @scure/btc-signer # Bitcoin addresses / PSBTs
487
+ npm install @solana/kit # Solana signer/address objects
235
488
  ```
236
489
 
237
490
  ---
238
491
 
239
492
  ## API Reference
240
493
 
241
- ### Static Methods (Lifecycle & Generation)
242
-
243
- | Method | Parameters | Returns | Description |
244
- | :----------------------------------------------------- | :---------------------------------------------------------------- | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
245
- | `create()` | `mnemonic`, `passphrase`, `label?`, `mnemonicLanguage?` | `Promise<MajikKey>` | Creates a new Argon2id-protected, fully post-quantum-capable account. |
246
- | `fromJSON()` | `json` | `MajikKey` | Loads a locked key from safe JSON storage. |
247
- | `fromMnemonicJSON()` | `mnemonicJson`, `passphrase`, `label?` | `Promise<MajikKey>` | Rebuilds a key straight from a portable seed export. |
248
- | `importFromMnemonicBackup()` | `backup`, `mnemonic`, `passphrase`, `label?`, `mnemonicLanguage?` | `Promise<MajikKey>` | Full migration path — verifies the mnemonic, then re-derives and re-encrypts the complete identity with Argon2id. |
249
- | `fromDangerousJSON()` | `json` | `MajikKey` | Reconstructs an already-unlocked key from a dangerous export. Server-side only — see warning below. |
250
- | `generateMnemonic()` | `strength?` *(128 \| 256)*, `language?` | `Promise<string>` | Generates a 12- or 24-word BIP-39 phrase. |
251
- | `validateMnemonic()` | `mnemonic` | `boolean` | Validates a BIP-39 mnemonic phrase. |
252
- | `deriveStandardBitcoinFromMnemonic()` *(experimental)* | `mnemonic`, `mnemonicLanguage?` | `Promise<BitcoinKeypairMaterial>` | Derives the real BIP-84 mainnet Bitcoin key without needing a `MajikKey` instance. |
253
-
254
- ### Instance Methods (State & Management)
255
-
256
- | Method | Parameters | Returns | Description |
257
- | :------------------- | :----------------------- | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
- | `unlock()` | `passphrase` | `Promise<this>` | Decrypts keys into memory. Chainable. |
259
- | `lock()` | None | `this` | Purges all private key material (including cached Web3 keys) from memory. Chainable. |
260
- | `verify()` | `passphrase` | `Promise<boolean>` | Tests a passphrase without keeping keys in memory or requiring an unlock. |
261
- | `updatePassphrase()` | `currentPass`, `newPass` | `Promise<this>` | Re-encrypts every stored key under a new passphrase and migrates to KDF v2 if needed. |
262
- | `migrate()` | `passphrase` | `Promise<this>` | Upgrades the X25519 key's KDF from v1 to v2 only — does **not** add ML-KEM/Ed25519/ML-DSA/Bitcoin keys. Use `importFromMnemonicBackup()` for a full upgrade. |
263
- | `updateLabel()` | `newLabel` | `this` | Updates the human-readable account label. |
264
-
265
- ### Export & Integration Methods
266
-
267
- | Method | Returns | Description |
268
- | :------------------------- | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
269
- | `toJSON()` / `toString()` | `MajikKeyJSON` / `string` | Safe export for DB/LocalStorage. No raw keys. |
270
- | `toDangerousJSON()` | `MajikKeyDangerousJSON` | ⚠️ Contains every raw private key. Server-side secret injection only — see warning below. |
271
- | `toMnemonicJSON()` | `MnemonicJSON` | ⚠️ Contains the raw mnemonic words (and passphrase, if you pass one) in plaintext — a transport format, not an at-rest storage format. Requires the key to be unlocked. |
272
- | `exportMnemonicBackup()` | `Promise<string>` | Encrypted backup string, decryptable only with the original mnemonic — used to verify a mnemonic before `importFromMnemonicBackup()` re-derives the identity. |
273
- | `toContact()` | `MajikContact` | Extracts public identity data for sharing (the basis for Majik Universal ID). |
274
- | `toMajikMessageIdentity()` | `Promise<MajikMessageIdentity>` | Formats the key for direct use in Majik Message. Requires a `MajikUser`. |
275
-
276
- ### Instance Getters
494
+ ### Static methods
495
+
496
+ | Method | Parameters | Returns | Description |
497
+ | :----------------------------------------------------- | :------------------------------------------------------- | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- |
498
+ | `create()` | `mnemonic`, `passphrase`, `label?`, `options?` | `Promise<MajikKey>` | Creates an Argon2id-protected account with the **core four** keys plus any `options.keys`. Returns it unlocked. |
499
+ | `fromJSON()` | `json` | `MajikKey` | Loads a **locked** key from safe JSON (registry or legacy shape — auto-migrates). |
500
+ | `fromMnemonicJSON()` | `mnemonicJson`, `passphrase`, `label?`, `options?` | `Promise<MajikKey>` | Rebuilds a key from a portable seed export. |
501
+ | `importFromMnemonicBackup()` | `backup`, `mnemonic`, `passphrase`, `label?`, `options?` | `Promise<MajikKey>` | Verifies the mnemonic against the backup, then re-derives and re-encrypts the identity (core four + `options.keys`) under Argon2id. |
502
+ | `fromDangerousJSON()` | `json` | `MajikKey` | Reconstructs an already-unlocked key from a dangerous export. Server-side only. |
503
+ | `withAutoLock()` | `key`, `operation` | `Promise<T>` | Runs `operation` against an unlocked key and **always** re-locks it afterwards, even if the operation throws. |
504
+ | `generateMnemonic()` | `strength?` *(128 \| 256)*, `language?` | `Promise<string>` | Generates a 12- or 24-word BIP-39 phrase. |
505
+ | `validateMnemonic()` | `mnemonic` | `boolean` | Validates a BIP-39 mnemonic phrase. |
506
+ | `supportedKeys()` | — | `KeyId[]` | Every key id this version can create. |
507
+ | `deriveStandardBitcoinFromMnemonic()` *(experimental)* | `mnemonic`, `mnemonicLanguage?` | `Promise<BitcoinKeypairMaterial>` | Derives the real BIP-84 mainnet key without a `MajikKey` instance. |
508
+
509
+ **`options`** (`MajikKeyCreateOptions`):
510
+
511
+ | Option | Type | Description |
512
+ | :----------------- | :----------------- | :------------------------------------------------------------------- |
513
+ | `mnemonicLanguage` | `MnemonicLanguage` | BIP-39 wordlist to validate against. Default `"en"`. |
514
+ | `keys` | `KeyId[]` | **Extra** keypairs on top of the core four. Default none. |
515
+ | `deriveBitcoin` | `boolean` | ⚠️ *Deprecated.* `true` adds `KeyId.BTC`. Prefer `keys: [KeyId.BTC]`. |
516
+
517
+ ### Instance methods — state & management
518
+
519
+ | Method | Parameters | Returns | Description |
520
+ | :------------------- | :------------------------------ | :----------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
521
+ | `unlock()` | `passphrase` | `Promise<this>` | Decrypts every key into memory with a single KDF run. Atomic. |
522
+ | `lock()` | — | `this` | Zeroizes and purges all secret material, including cached Web3 keys. |
523
+ | `verify()` | `passphrase` | `Promise<boolean>` | Tests a passphrase without unlocking or keeping keys in memory. |
524
+ | `updatePassphrase()` | `currentPass`, `newPass` | `Promise<this>` | Re-encrypts **every** stored key under a new passphrase and salt (and moves to Argon2id if needed). Atomic. |
525
+ | `migrate()` | `passphrase` | `Promise<this>` | Upgrades the KDF from PBKDF2 to Argon2id for every stored key. No-op if already on Argon2id. Does **not** add keys — use `addKeys()`. |
526
+ | `addKeys()` | `ids`, `mnemonic`, `passphrase` | `Promise<KeyId[]>` | Adds missing keys. Requires the mnemonic **and** the passphrase. Returns the ids actually added. |
527
+ | `updateLabel()` | `newLabel` | `this` | Updates the human-readable account label. |
528
+
529
+ ### Instance methods — key registry
530
+
531
+ | Method | Returns | Description |
532
+ | :---------------------------- | :------------- | :------------------------------------------------------------------------------------------------------------------ |
533
+ | `hasKey(id)` / `hasKeys(ids)` | `boolean` | Is the key (or are all keys) present? Works while locked. |
534
+ | `missingKeys(ids?)` | `KeyId[]` | Which of `ids` (default: the core four) are absent. |
535
+ | `availableKeys({ family? })` | `KeyId[]` | Every available key id, canonical order, optionally filtered by family. |
536
+ | `listKeys()` | `KeyInfo[]` | `id`, `family`, `purpose`, `kind` (`stored`/`derived`), `status`, `publicKeyBase64`. No secrets. |
537
+ | `getPublicKey(id)` | `Uint8Array` | Public key bytes. Works while locked (derived views like `web3:sol` need an unlocked account). |
538
+ | `getPrivateKey(id)` | `Uint8Array` | Secret key bytes. Throws if locked or absent. |
539
+ | `getKeypair(id)` | `MajikKeypair` | Live handle: `.public`, `.publicBase64`, `.private`, `.algorithm`, `.family`, `.purpose`, `.status`, `.isUnlocked`. |
540
+
541
+ ### Export & integration methods
542
+
543
+ | Method | Returns | Description |
544
+ | :------------------------------------------- | :---------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- |
545
+ | `toJSON(options?)` / `toString()` | `MajikKeyJSON` / `string` | Safe export for DB/LocalStorage. No raw keys. `toJSON({ legacy: false })` omits the pre-registry flat fields (see below). |
546
+ | `toDangerousJSON()` | `MajikKeyDangerousJSON` | ⚠️ Contains every raw private key. Server-side secret injection only. |
547
+ | `toMnemonicJSON()` | `MnemonicJSON` | ⚠️ Contains the raw mnemonic words (and passphrase, if given) in plaintext — a transport format, not an at-rest format. Requires an unlocked key. |
548
+ | `exportMnemonicBackup()` | `Promise<string>` | Encrypted backup string, decryptable only with the original mnemonic. |
549
+ | `toContact()` | `MajikContact` | Public identity data for sharing (the basis for Majik Universal ID). |
550
+ | `toKeyIdentity()` / `toSerializedIdentity()` | `MajikKeyIdentity` / `SerializedIdentity` | Identity bundles for other Majikah packages. Require an unlocked key. |
551
+ | `toMajikMessageIdentity()` | `Promise<MajikMessageIdentity>` | Formats the key for Majik Message. Requires a `MajikUser`. |
552
+
553
+ ### Instance getters
277
554
 
278
555
  *Public — available at any time, regardless of lock state:*
279
556
 
280
- `id`, `fingerprint`, `publicKey`, `publicKeyBase64`, `label`, `backup`, `timestamp`, `mnemonicLanguage`, `kdfVersion`, `isArgon2id`, `isLocked`, `isUnlocked`, `isFullyUpgraded`, `mlKemPublicKey`, `hasMlKem`, `edPublicKey`, `mlDsaPublicKey`, `hasSigningKeys`, `btcPublicKey`, `hasBitcoin`, `hasSolanaKeypair`, `hasBitcoinKeypair`, `metadata`.
281
-
282
- *Restricted — throws `MajikKeyError` if locked (or if that key type isn't present, e.g. on an account not yet fully migrated):*
283
-
284
- `getPrivateKey()`, `getPrivateKeyBase64()`, `getMlKemSecretKey()`, `getEdSecretKey()`, `getMlDsaSecretKey()`, `getBtcSecretKey()`.
285
-
286
- ### Web3 (Experimental)
557
+ `id`, `fingerprint`, `publicKey`, `publicKeyBase64`, `label`, `backup`, `timestamp`, `mnemonicLanguage`, `kdfVersion`, `isArgon2id`, `isLocked`, `isUnlocked`, `isCoreComplete`, `isFullyUpgraded`, `hasBitcoin`, `hasEthereum`, `metadata` (includes `keys: KeyId[]`).
558
+
559
+ *Unlocked-only capabilities:* `hasBitcoinKeypair`, `hasSolanaKeypair`, `web3`.
560
+
561
+ ### Web3 (experimental)
562
+
563
+ | Member | Returns | Notes |
564
+ | :------------------------------------------------ | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- |
565
+ | `web3` *(getter)* | `{ solana, bitcoin?, ethereum? } \| undefined` | `undefined` if locked or there's no Ed25519 key. `bitcoin` / `ethereum` appear only if the account holds those keys. |
566
+ | `getEthereumAddress()` | `string` | EIP-55 address. Public-only — works while locked. |
567
+ | `getEthereumPrivateKeyHex()` | `string` | `0x…` private key. Requires an unlocked key. |
568
+ | `getEthereumKeypairMaterial()` | `EthereumKeypairMaterial` | Raw bytes. Requires an unlocked key. |
569
+ | `getBitcoinKeypairMaterial()` / `getBitcoinWIF()` | `BitcoinKeypairMaterial` / `string` | Stored (domain-separated) key. |
570
+ | `getSolanaKeypairMaterial()` | `SolanaKeypairMaterial` | Raw Solana keypair bytes. |
571
+ | `getSolanaKeypair()` | `Promise<any>` | Real `@solana/kit` `KeyPairSigner`. Requires `@solana/kit`. |
572
+ | `getSolanaAddress()` | `string` | Base58 address. No extra dependency. |
573
+
574
+ ### ⚠️ Deprecated (still supported until the next major)
575
+
576
+ These keep working as thin wrappers over the registry. Prefer the replacement.
577
+
578
+ | Deprecated | Use instead |
579
+ | :------------------------------------------------------------------------------------------------------ | :----------------------------------- |
580
+ | `mlKemPublicKey`, `edPublicKey`, `mlDsaPublicKey`, `btcPublicKey` | `getPublicKey(KeyId.…)` |
581
+ | `mlKemSecretKey`, `getMlKemSecretKey()`, `getEdSecretKey()`, `getMlDsaSecretKey()`, `getBtcSecretKey()` | `getPrivateKey(KeyId.…)` |
582
+ | `getPrivateKey()` *(no argument)*, `getPrivateKeyBase64()` | `getPrivateKey(KeyId.X25519)` |
583
+ | `hasMlKem`, `hasSigningKeys`, `hasBitcoin` | `hasKey(KeyId.…)` / `hasKeys([...])` |
584
+ | `deriveBitcoin: true` *(option)* | `keys: [KeyId.BTC]` |
585
+
586
+ ### Serialized shape
587
+
588
+ `toJSON()` writes the registry under `keys`:
589
+
590
+ ```jsonc
591
+ {
592
+ "id": "…", "label": "…", "fingerprint": "…", "publicKey": "…", // X25519 public key
593
+ "salt": "…", "backup": "…", "timestamp": "…", "kdfVersion": 2, "mnemonicLanguage": "en",
594
+ "keysVersion": 1,
595
+ "keys": [
596
+ {
597
+ "id": "pq:ml-dsa-87",
598
+ "publicKey": "…", // base64
599
+ "encryptedSecretKey": "…", // base64, AES-256-GCM (IV ‖ ciphertext) — never a raw key
600
+ "derivation": { "scheme": "legacy-v1", "version": 1, "note": "…" },
601
+ "createdAt": "…"
602
+ }
603
+ // …one entry per stored key
604
+ ]
605
+ }
606
+ ```
287
607
 
288
- | Member | Returns | Notes |
289
- | :---------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
290
- | `web3` *(getter)* | `{ solana, bitcoin? } \| undefined` | `undefined` if locked or has no Ed25519 signing key. `bitcoin` is present only if the account has stored Bitcoin key material. |
291
- | `getBitcoinKeypairMaterial()` | `BitcoinKeypairMaterial` | Raw Bitcoin keypair bytes. |
292
- | `getBitcoinWIF()` | `string` | Wallet Import Format string, pastes into any standard Bitcoin wallet. |
293
- | `getSolanaKeypairMaterial()` | `SolanaKeypairMaterial` | Raw Solana keypair bytes. |
294
- | `getSolanaKeypair()` | `Promise<any>` | Real `@solana/kit` `KeyPairSigner`. Requires `@solana/kit`. |
295
- | `getSolanaAddress()` | `string` | Base58 Solana address. No extra dependency required. |
608
+ **By default `toJSON()` also writes the original flat fields** (`encryptedMlKemSecretKey`, `edPublicKey`, …) so older versions of the library — and other implementations such as the Rust port — can still read what you save. Keys that didn't exist before the registry (ML-KEM-1024, Ethereum, SLH-DSA, …) are written **only** to `keys`. Pass `toJSON({ legacy: false })` for registry-only output. The flat fields will stop being written by default in the next major version.
296
609
 
297
610
  ---
298
611
 
299
612
  ## Usage Examples
300
613
 
301
- ### 1. Secure Backup & Recovery Workflow
614
+ ### 1. Secure backup & recovery workflow
302
615
 
303
616
  ```typescript
304
- import { MajikKey } from '@majikah/majik-key';
617
+ import { MajikKey, KeyId } from '@majikah/majik-key';
305
618
 
306
619
  // -- EXPORTING --
307
- // ⚠️ jsonData contains the raw mnemonic (and passphrase, if provided) in
308
- // plaintext. Treat this exactly like the mnemonic itself — encrypt the
309
- // file yourself, or keep it offline. This is a transport format, not a
310
- // safe-storage format.
620
+ // ⚠️ jsonData contains the raw mnemonic (and passphrase, if provided) in plaintext.
621
+ // Treat it exactly like the mnemonic itself — encrypt the file yourself, or keep it offline.
622
+ // It is a transport format, not a safe-storage format.
311
623
  const jsonData = key.toMnemonicJSON(mnemonic, 'password123');
312
- const blob = new Blob([JSON.stringify(jsonData)], { type: "application/json" });
624
+ const blob = new Blob([JSON.stringify(jsonData)], { type: 'application/json' });
313
625
  // Save blob to a secure location...
314
626
 
315
627
  // -- RECOVERING --
316
628
  const recoveredData = JSON.parse(await blob.text());
317
629
  const recoveredKey = await MajikKey.importFromMnemonicBackup(
318
630
  recoveredData.id,
319
- recoveredData.seed.join(" "),
631
+ recoveredData.seed.join(' '),
320
632
  recoveredData.phrase,
321
633
  'Recovered Key',
634
+ { mnemonicLanguage: recoveredData.language, keys: [KeyId.ETH] }, // re-create the same extras
322
635
  );
323
636
  ```
324
637
 
325
- ### 2. Password Verification Before Action
638
+ ### 2. Multi-algorithm account
639
+
640
+ ```typescript
641
+ import { MajikKey, KeyId } from '@majikah/majik-key';
642
+ import { ml_kem1024 } from '@noble/post-quantum/ml-kem.js';
643
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
644
+
645
+ const key = await MajikKey.create(mnemonic, passphrase, 'PQ+', {
646
+ keys: [KeyId.ML_KEM_1024, KeyId.ML_DSA_65],
647
+ });
648
+
649
+ // Encapsulate to yourself with ML-KEM-1024
650
+ const kem = key.getKeypair(KeyId.ML_KEM_1024);
651
+ const { cipherText, sharedSecret } = ml_kem1024.encapsulate(kem.public);
652
+ const same = ml_kem1024.decapsulate(cipherText, kem.private); // === sharedSecret
653
+
654
+ // Sign with ML-DSA-65
655
+ const dsa = key.getKeypair(KeyId.ML_DSA_65);
656
+ const msg = new TextEncoder().encode('hello');
657
+ const sig = ml_dsa65.sign(msg, dsa.private);
658
+ ml_dsa65.verify(sig, msg, dsa.public); // true
659
+ ```
660
+
661
+ ### 3. Scoped secret access with `withAutoLock`
662
+
663
+ ```typescript
664
+ await key.unlock(passphrase);
665
+
666
+ const signature = await MajikKey.withAutoLock(key, async (k) => {
667
+ const secret = k.getPrivateKey(KeyId.ED25519);
668
+ return mySign(secret, payload);
669
+ });
670
+
671
+ key.isLocked; // true — locked even if mySign threw
672
+ ```
673
+
674
+ ### 4. Password verification before action
326
675
 
327
676
  ```typescript
328
677
  const key = MajikKey.fromJSON(storedJson);
@@ -332,13 +681,27 @@ if (await key.verify('user-input-password')) {
332
681
  // ... proceed with signing/encryption
333
682
  key.lock(); // Always clean up!
334
683
  } else {
335
- throw new Error("Invalid passphrase");
684
+ throw new Error('Invalid passphrase');
336
685
  }
337
686
  ```
338
687
 
339
- ### 3. Server-Side Secret Injection (Dangerous JSON)
688
+ ### 5. Completing or extending an existing account
340
689
 
341
- `toDangerousJSON()` / `fromDangerousJSON()` skip encryption entirely — no KDF, no AES-GCM, instant reconstruction. This exists for one narrow case: injecting a pre-unlocked signing key into a server process, not for anything that touches a database, log, or the network.
690
+ ```typescript
691
+ const key = MajikKey.fromJSON(oldStoredJson); // any previous version
692
+
693
+ if (!key.isCoreComplete) {
694
+ console.log('Missing:', key.missingKeys()); // e.g. ['classic:ed25519', 'pq:ml-dsa-87']
695
+ await key.addKeys(CORE_KEYS, mnemonic, passphrase);
696
+ }
697
+
698
+ await key.addKeys([KeyId.ETH], mnemonic, passphrase);
699
+ localStorage.setItem('myKey', key.toString()); // persists the registry shape
700
+ ```
701
+
702
+ ### 6. Server-side secret injection (Dangerous JSON)
703
+
704
+ `toDangerousJSON()` / `fromDangerousJSON()` skip encryption entirely — no KDF, no AES-GCM, instant reconstruction. This exists for one narrow case: injecting a pre-unlocked signing key into a server process, not for anything that touches a database, log or the network. The export includes the raw secret of **every** stored key (a `secretKeys` map, plus the legacy fields).
342
705
 
343
706
  ```typescript
344
707
  // At deploy time, generated once and stored in your secrets manager:
@@ -349,32 +712,61 @@ const serverKey = MajikKey.fromDangerousJSON(process.env.MAJIK_SIGNING_KEY!);
349
712
  // serverKey is already unlocked — no passphrase needed, no KDF cost.
350
713
  ```
351
714
 
352
- ### 4. Experimental Web3 Usage
715
+ ### 7. Experimental Web3 usage
353
716
 
354
717
  ```typescript
355
- // Bitcoin — key material is already derived and stored on any account
718
+ const key = await MajikKey.create(mnemonic, passphrase, 'Multi-chain', {
719
+ keys: [KeyId.BTC, KeyId.ETH],
720
+ });
721
+
722
+ // Bitcoin
356
723
  console.log('Bitcoin WIF:', key.getBitcoinWIF());
357
- console.log('Bitcoin address:', await key.web3?.bitcoin?.getBitcoinAddress()); // needs @scure/btc-signer
724
+ console.log('Bitcoin address:', await key.web3?.bitcoin?.getBitcoinAddress()); // needs @scure/btc-signer
358
725
 
359
- // Solana — derived on demand from your Ed25519 signing key
360
- console.log('Solana address:', key.getSolanaAddress()); // no extra dependency
361
- const solanaSigner = await key.getSolanaKeypair(); // needs @solana/kit
726
+ // Ethereum (no extra dependency)
727
+ console.log('ETH address:', key.getEthereumAddress());
728
+ const sig = key.web3?.ethereum?.signMessage('hello');
729
+
730
+ // Solana — derived on demand from your Ed25519 key
731
+ console.log('Solana address:', key.getSolanaAddress()); // no extra dependency
732
+ const solanaSigner = await key.getSolanaKeypair(); // needs @solana/kit
362
733
  ```
363
734
 
364
735
  ---
365
736
 
737
+ ## Upgrading from earlier versions
738
+
739
+ **No migration step is required.** Install the new version and your existing stored keys, backups and code keep working. The things that behave differently:
740
+
741
+ | Change | Impact | What to do |
742
+ | :------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------- |
743
+ | **Bitcoin is no longer created by default** | New accounts have the core four only. Existing accounts keep any Bitcoin key they have. | Pass `keys: [KeyId.BTC]` (or `deriveBitcoin: true`) where you rely on `web3.bitcoin`. |
744
+ | **`toJSON()` now includes `keys`** (plus the old flat fields by default) | Larger JSON (roughly double while both are written). Older readers still work. | Nothing — or `toJSON({ legacy: false })` once all your readers are updated. |
745
+ | **Mnemonic backups use renamed salts** (`MajikKey…` instead of `MajikMessage…`) | Backups written by this version record a `backupSaltVersion`; older backups are still readable. **Older library versions can't read backups written by this version.** | Update every consumer (and the Rust port) before relying on cross-version backup import. |
746
+ | **Wrong-passphrase error text** | Message now reads *"Failed to decrypt classic:x25519 secret key — incorrect passphrase or corrupted data"*. | Match on `incorrect passphrase or corrupted data` rather than the old prefix. |
747
+ | **`getPrivateKey()` gained an overload** | The no-argument form still returns the X25519 key wrapper; `getPrivateKey(id)` returns bytes. | Prefer `getPrivateKey(KeyId.X25519)`. |
748
+ | **Per-algorithm getters are deprecated** | Still supported until the next major. | Migrate to `getPublicKey(id)` / `getPrivateKey(id)` at your pace. |
749
+ | **`importFromMnemonicBackup()` keeps the mnemonic language** | Previously a non-English account silently reset to `"en"`. | Pass `{ mnemonicLanguage }` as the 5th-argument option. |
750
+
751
+ If anything looks off after upgrading, **re-import your seed phrase** (see [Backward compatibility & recovery](#backward-compatibility--recovery)) — the keys it produces never change.
752
+
753
+ ---
754
+
366
755
  ## Security Best Practices
367
756
 
368
757
  ✅ **DO:**
369
- - Call `.lock()` immediately after signing or decrypting payloads to free key material from memory.
370
- - Use `mlKemPublicKey` for all new communication protocols to stay post-quantum ready.
758
+ - Back up your **mnemonic** offline. It is the master secret and the only thing needed to recover every key.
759
+ - Call `.lock()` immediately after signing or decrypting, or use `MajikKey.withAutoLock()` so it happens even on errors.
760
+ - Use `mlKemPublicKey` / `getPublicKey(KeyId.ML_KEM_768)` for all new communication protocols to stay post-quantum ready.
761
+ - Enable only the extra algorithms you need — every key you add is one more secret to protect.
762
+ - Run SLH-DSA `s` variants in a Web Worker.
371
763
  - Keep `@scure/bip39` and the underlying crypto dependencies up to date.
372
- - Treat any `toMnemonicJSON()` export, and the mnemonic itself, as the master secret — it recovers everything.
373
764
 
374
765
  ❌ **DON'T:**
375
- - Log `mnemonic` phrases, `privateKeyBase64`, or any `*SecretKeyBase64` value in production.
376
- - Use `toDangerousJSON()` / `fromDangerousJSON()` outside of controlled, server-side secret injection.
377
- - Store the output of `toMnemonicJSON()` unencrypted — it is not the same as `toJSON()`/`toString()`.
766
+ - Log `mnemonic` phrases, `privateKeyBase64`, or any `*SecretKeyBase64` / `secretKeys` value in production.
767
+ - Use `toDangerousJSON()` / `fromDangerousJSON()` outside controlled, server-side secret injection.
768
+ - Store the output of `toMnemonicJSON()` unencrypted — it is not the same as `toJSON()` / `toString()`.
769
+ - Reuse a funded Ethereum or Bitcoin mnemonic for anything you don't fully trust: standard-path wallets are controlled by the mnemonic alone.
378
770
 
379
771
  ---
380
772
 
@@ -395,7 +787,6 @@ const solanaSigner = await key.getSolanaKeypair(); // needs @solana/kit
395
787
 
396
788
  Developed by **Josef Elijah Fabian (Zelijah)** | [Majikah Solutions OPC](https://majikah.solutions/about)
397
789
 
398
-
399
790
  **Developer**: [Josef Elijah Fabian](https://github.com/jedlsf)
400
791
 
401
792
  **GitHub**: [https://github.com/Majikah](https://github.com/Majikah)