@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.
- package/README.md +566 -175
- package/dist/core/backup/index.d.ts +4 -4
- package/dist/core/backup/index.js +3 -3
- package/dist/core/backup/majik-key-backup.d.ts +3 -3
- package/dist/core/backup/majik-key-backup.js +5 -5
- package/dist/core/backup/types.d.ts +1 -1
- package/dist/core/backup/utils.js +1 -1
- package/dist/core/backup/validator.d.ts +1 -1
- package/dist/core/backup/validator.js +1 -1
- package/dist/core/crypto/constants.d.ts +35 -11
- package/dist/core/crypto/constants.js +33 -11
- package/dist/core/crypto/crypto-provider.js +2 -2
- package/dist/core/crypto/encryption-engine.d.ts +7 -16
- package/dist/core/crypto/encryption-engine.js +27 -62
- package/dist/core/database/system/identity.d.ts +2 -2
- package/dist/core/database/system/identity.js +1 -1
- package/dist/core/keys/hkdf-recipe.d.ts +4 -0
- package/dist/core/keys/hkdf-recipe.js +26 -0
- package/dist/core/keys/key-id.d.ts +50 -0
- package/dist/core/keys/key-id.js +64 -0
- package/dist/core/keys/key-impls.d.ts +12 -0
- package/dist/core/keys/key-impls.js +163 -0
- package/dist/core/keys/key-store.d.ts +72 -0
- package/dist/core/keys/key-store.js +264 -0
- package/dist/core/keys/keypair-handle.d.ts +36 -0
- package/dist/core/keys/keypair-handle.js +43 -0
- package/dist/core/keys/registry.d.ts +16 -0
- package/dist/core/keys/registry.js +144 -0
- package/dist/core/keys/types.d.ts +47 -0
- package/dist/core/keys/types.js +1 -0
- package/dist/core/types.d.ts +12 -1
- package/dist/core/utils.d.ts +1 -1
- package/dist/core/utils.js +1 -1
- package/dist/core/validator.d.ts +1 -1
- package/dist/core/validator.js +1 -1
- package/dist/core/web3/bitcoin/bitcoin.d.ts +1 -1
- package/dist/core/web3/bitcoin/bitcoin.js +3 -3
- package/dist/core/web3/bitcoin/types.d.ts +1 -1
- package/dist/core/web3/ethereum/constants.d.ts +1 -0
- package/dist/core/web3/ethereum/constants.js +4 -0
- package/dist/core/web3/ethereum/ethereum.d.ts +21 -0
- package/dist/core/web3/ethereum/ethereum.js +91 -0
- package/dist/core/web3/ethereum/types.d.ts +32 -0
- package/dist/core/web3/ethereum/types.js +1 -0
- package/dist/core/web3/index.d.ts +10 -5
- package/dist/core/web3/index.js +7 -2
- package/dist/core/web3/solana/solana.d.ts +2 -2
- package/dist/core/web3/solana/solana.js +4 -4
- package/dist/core/web3/solana/types.d.ts +1 -1
- package/dist/core/web3/types.d.ts +4 -2
- package/dist/index.d.ts +16 -6
- package/dist/index.js +14 -5
- package/dist/majik-key.d.ts +179 -281
- package/dist/majik-key.js +622 -726
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -2,36 +2,299 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://doi.org/10.5281/zenodo.21339132)
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
[](https://www.thezelijah.world) 
|
|
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
|
+
[](https://doi.org/10.5281/zenodo.21339132)    [](https://opensource.org/licenses/Apache-2.0)
|
|
9
10
|
|
|
10
|
-
|
|
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
|
-
[](https://doi.org/10.5281/zenodo.21339132)    [](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,
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
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
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
B -->
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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.
|
|
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
|
[](https://signature.majikah.solutions)
|
|
100
356
|
|
|
101
|
-
Majik Signature consumes a Majik Key's **Ed25519** and **ML-DSA-87**
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
|
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`)
|
|
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
|
-
```
|
|
215
|
-
|
|
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
|
|
242
|
-
|
|
243
|
-
| Method | Parameters
|
|
244
|
-
| :----------------------------------------------------- |
|
|
245
|
-
| `create()` | `mnemonic`, `passphrase`, `label?`, `
|
|
246
|
-
| `fromJSON()` | `json`
|
|
247
|
-
| `fromMnemonicJSON()` | `mnemonicJson`, `passphrase`, `label?`
|
|
248
|
-
| `importFromMnemonicBackup()` | `backup`, `mnemonic`, `passphrase`, `label?`, `
|
|
249
|
-
| `fromDangerousJSON()` | `json`
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
| `
|
|
261
|
-
| `
|
|
262
|
-
| `
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
|
268
|
-
|
|
|
269
|
-
| `
|
|
270
|
-
| `
|
|
271
|
-
| `
|
|
272
|
-
| `
|
|
273
|
-
| `
|
|
274
|
-
| `
|
|
275
|
-
|
|
276
|
-
### Instance
|
|
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`, `
|
|
281
|
-
|
|
282
|
-
*
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
309
|
-
//
|
|
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:
|
|
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.
|
|
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(
|
|
684
|
+
throw new Error('Invalid passphrase');
|
|
336
685
|
}
|
|
337
686
|
```
|
|
338
687
|
|
|
339
|
-
###
|
|
688
|
+
### 5. Completing or extending an existing account
|
|
340
689
|
|
|
341
|
-
|
|
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
|
-
###
|
|
715
|
+
### 7. Experimental Web3 usage
|
|
353
716
|
|
|
354
717
|
```typescript
|
|
355
|
-
|
|
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());
|
|
724
|
+
console.log('Bitcoin address:', await key.web3?.bitcoin?.getBitcoinAddress()); // needs @scure/btc-signer
|
|
358
725
|
|
|
359
|
-
//
|
|
360
|
-
console.log('
|
|
361
|
-
const
|
|
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
|
-
-
|
|
370
|
-
-
|
|
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
|
|
377
|
-
- Store the output of `toMnemonicJSON()` unencrypted — it is not the same as `toJSON()
|
|
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)
|