@majikah/majik-key 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +249 -793
- package/dist/core/types.d.ts +94 -3
- package/dist/core/web3/bitcoin/bitcoin.d.ts +71 -0
- package/dist/core/web3/bitcoin/bitcoin.js +126 -0
- package/dist/core/web3/bitcoin/constants.d.ts +2 -0
- package/dist/core/web3/bitcoin/constants.js +7 -0
- package/dist/core/web3/bitcoin/types.d.ts +23 -0
- package/dist/core/web3/bitcoin/types.js +1 -0
- package/dist/core/web3/index.d.ts +5 -0
- package/dist/core/web3/index.js +2 -0
- package/dist/core/web3/{solana.d.ts → solana/solana.d.ts} +0 -1
- package/dist/core/web3/{solana.js → solana/solana.js} +2 -28
- package/dist/core/web3/solana/types.d.ts +18 -0
- package/dist/core/web3/solana/types.js +1 -0
- package/dist/core/web3/types.d.ts +3 -22
- package/dist/core/web3/utils.d.ts +1 -0
- package/dist/core/web3/utils.js +27 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/majik-key.d.ts +232 -47
- package/dist/majik-key.js +281 -55
- package/package.json +13 -2
- /package/dist/core/web3/{constants.d.ts → solana/constants.d.ts} +0 -0
- /package/dist/core/web3/{constants.js → solana/constants.js} +0 -0
package/dist/majik-key.js
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MajikKey.ts
|
|
3
|
+
* Seed phrase account library for the Majikah ecosystem.
|
|
3
4
|
*
|
|
4
|
-
* Seed phrase account library for Majik Message.
|
|
5
|
-
*
|
|
6
|
-
* Every account stores TWO keypairs derived deterministically from the mnemonic:
|
|
7
|
-
* 1. X25519 (Curve25519) — fingerprint, contact identity, legacy message compat
|
|
8
|
-
* 2. ML-KEM-768 (FIPS-203) — post-quantum key encapsulation for v3 envelopes
|
|
9
|
-
*
|
|
10
|
-
* Both are derived from the 64-byte BIP-39 seed:
|
|
11
|
-
* seed[0..32] → Ed25519 → X25519 via ed2curve
|
|
12
|
-
* seed[0..64] → ml_kem768.keygen(seed) — full seed, deterministic
|
|
13
|
-
*
|
|
14
|
-
* KDF versioning (passphrase encryption at rest):
|
|
15
|
-
* v1 — PBKDF2-SHA256, 250k iterations (legacy read-only)
|
|
16
|
-
* v2 — Argon2id, 128 MB / 4t / 4p (all new accounts)
|
|
17
|
-
*
|
|
18
|
-
* Migration policy:
|
|
19
|
-
* Old accounts (v1, no ML-KEM keys) are fully upgraded on first import
|
|
20
|
-
* via importFromMnemonicBackup(). The mnemonic is always available at that
|
|
21
|
-
* point, so ML-KEM keys can be deterministically re-derived and stored.
|
|
22
|
-
* No partial migration — either fully upgraded or not upgraded yet.
|
|
23
5
|
*/
|
|
24
|
-
import { generateMnemonic as bip39GenerateMnemonic, validateMnemonic, } from "@scure/bip39";
|
|
6
|
+
import { generateMnemonic as bip39GenerateMnemonic, mnemonicToSeed, validateMnemonic, } from "@scure/bip39";
|
|
25
7
|
import { aesGcmDecrypt, aesGcmEncrypt, deriveKeyFromPassphraseArgon2, deriveKeyFromMnemonicArgon2, deriveKeyFromPassphrase, generateRandomBytes, IV_LENGTH, } from "./core/crypto/crypto-provider";
|
|
26
8
|
import { EncryptionEngine } from "./core/crypto/encryption-engine";
|
|
27
9
|
import { MajikContact } from "@majikah/majik-contact";
|
|
@@ -31,9 +13,31 @@ import { MajikKeyValidator } from "./core/validator";
|
|
|
31
13
|
import { MajikKeyError } from "./core/error";
|
|
32
14
|
import { MajikMessageIdentity } from "./core/database/system/identity";
|
|
33
15
|
import { WORDLISTS } from "./core/crypto/wordlist";
|
|
34
|
-
import { deriveSolanaKeypairFromEdSecretKey, signWithSolanaMaterial, solanaAddressFromPublicKey, solanaMaterialFromEd25519SecretKey, toSolanaAddress, toSolanaKeyPairSigner, } from "./core/web3
|
|
16
|
+
import { deriveBitcoinKeypairFromSeed, signWithBitcoinMaterial, toBitcoinAddress, toWIF, deriveSolanaKeypairFromEdSecretKey, signWithSolanaMaterial, solanaAddressFromPublicKey, solanaMaterialFromEd25519SecretKey, toSolanaAddress, toSolanaKeyPairSigner, } from "./core/web3";
|
|
35
17
|
const SALT_SIZE = 32;
|
|
36
|
-
|
|
18
|
+
/**
|
|
19
|
+
* MajikKey
|
|
20
|
+
* ---
|
|
21
|
+
*
|
|
22
|
+
* Seed phrase account library for the Majikah ecosystem.
|
|
23
|
+
*
|
|
24
|
+
* Every account stores FIVE keypairs, all deterministically derived from a
|
|
25
|
+
* single BIP-39 mnemonic:
|
|
26
|
+
* 1. X25519 (Curve25519) — fingerprint, contact identity, legacy message compat
|
|
27
|
+
* 2. ML-KEM-768 (FIPS-203) — post-quantum key encapsulation for v3 envelopes
|
|
28
|
+
* 3. Ed25519 — classical signing
|
|
29
|
+
* 4. ML-DSA-87 (FIPS-204) — post-quantum signing
|
|
30
|
+
* 5. Bitcoin (secp256k1) — BIP-32/84 HD key, domain-separated by default (experimental)
|
|
31
|
+
*
|
|
32
|
+
* All derived from the 64-byte BIP-39 seed:
|
|
33
|
+
* seed[0..32] → Ed25519 keypair — used directly for signing, AND converted
|
|
34
|
+
* to X25519 via ed2curve for the encryption/identity keypair
|
|
35
|
+
* (one Ed25519 keypair, two roles)
|
|
36
|
+
* seed[0..64] → ml_kem768.keygen(seed) — full seed, deterministic
|
|
37
|
+
* hash(seed || "MajikSignatureSeedDSA") → 32-byte seed → ml_dsa87.keygen()
|
|
38
|
+
* seed[0..64] → HDKey.fromMasterSeed(seed).derive(path) — BIP-32/84 Bitcoin key
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
37
41
|
export class MajikKey {
|
|
38
42
|
_id;
|
|
39
43
|
_publicKey;
|
|
@@ -61,8 +65,26 @@ export class MajikKey {
|
|
|
61
65
|
_mlDsaSecretKey;
|
|
62
66
|
_encryptedMlDsaSecretKey;
|
|
63
67
|
_encryptedMlDsaSecretKeyBase64;
|
|
64
|
-
|
|
68
|
+
/**
|
|
69
|
+
* @experimental
|
|
70
|
+
*/
|
|
65
71
|
_solanaKeypairMaterial;
|
|
72
|
+
/**
|
|
73
|
+
* @experimental
|
|
74
|
+
*/
|
|
75
|
+
_btcPublicKey;
|
|
76
|
+
/**
|
|
77
|
+
* @experimental
|
|
78
|
+
*/
|
|
79
|
+
_btcSecretKey;
|
|
80
|
+
/**
|
|
81
|
+
* @experimental
|
|
82
|
+
*/
|
|
83
|
+
_encryptedBtcSecretKey;
|
|
84
|
+
/**
|
|
85
|
+
* @experimental
|
|
86
|
+
*/
|
|
87
|
+
_encryptedBtcSecretKeyBase64;
|
|
66
88
|
constructor(options) {
|
|
67
89
|
this._id = options.id;
|
|
68
90
|
this._publicKey = options.publicKey;
|
|
@@ -89,57 +111,99 @@ export class MajikKey {
|
|
|
89
111
|
this._encryptedMlDsaSecretKeyBase64 = options.encryptedMlDsaSecretKeyBase64;
|
|
90
112
|
this._edSecretKey = options.edSecretKey;
|
|
91
113
|
this._mlDsaSecretKey = options.mlDsaSecretKey;
|
|
114
|
+
this._btcPublicKey = options.btcPublicKey;
|
|
115
|
+
this._btcSecretKey = options.btcSecretKey;
|
|
116
|
+
this._encryptedBtcSecretKey = options.encryptedBtcSecretKey;
|
|
117
|
+
this._encryptedBtcSecretKeyBase64 = options.encryptedBtcSecretKeyBase64;
|
|
92
118
|
this._mnemonicLanguage = options.mnemonicLanguage || "en";
|
|
93
119
|
}
|
|
94
120
|
// ── Getters ─────────────────────────────────────────────────────────────────
|
|
121
|
+
/** Account identifier. Equal to `fingerprint` for accounts created by this library. */
|
|
95
122
|
get id() {
|
|
96
123
|
return this._id;
|
|
97
124
|
}
|
|
125
|
+
/** SHA-256 fingerprint of the X25519 public key. Stable identity anchor for the account. */
|
|
98
126
|
get fingerprint() {
|
|
99
127
|
return this._fingerprint;
|
|
100
128
|
}
|
|
129
|
+
/** X25519 public key — native `CryptoKey` where WebCrypto supports it, otherwise a raw-bytes wrapper. Always available, even when locked. */
|
|
101
130
|
get publicKey() {
|
|
102
131
|
return this._publicKey;
|
|
103
132
|
}
|
|
133
|
+
/** X25519 public key, base64-encoded. Always available, even when locked. */
|
|
104
134
|
get publicKeyBase64() {
|
|
105
135
|
return this._publicKeyBase64;
|
|
106
136
|
}
|
|
137
|
+
/** Human-readable, user-editable account name. Update via `updateLabel()`. */
|
|
107
138
|
get label() {
|
|
108
139
|
return this._label;
|
|
109
140
|
}
|
|
141
|
+
/** BIP-39 wordlist language this account's mnemonic was generated/validated against. */
|
|
110
142
|
get mnemonicLanguage() {
|
|
111
143
|
return this._mnemonicLanguage;
|
|
112
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Encrypted mnemonic-verification blob (base64 JSON). Decryptable only
|
|
147
|
+
* with the original mnemonic — used internally to verify a supplied
|
|
148
|
+
* mnemonic before `importFromMnemonicBackup()` re-derives the full
|
|
149
|
+
* identity. Not a general-purpose private-key backup.
|
|
150
|
+
*/
|
|
113
151
|
get backup() {
|
|
114
152
|
return this._backup;
|
|
115
153
|
}
|
|
154
|
+
/** Account creation time. */
|
|
116
155
|
get timestamp() {
|
|
117
156
|
return this._timestamp;
|
|
118
157
|
}
|
|
158
|
+
/** KDF currently protecting every `encrypted*` field on this account: `1` = legacy PBKDF2, `2` = Argon2id. */
|
|
119
159
|
get kdfVersion() {
|
|
120
160
|
return this._kdfVersion;
|
|
121
161
|
}
|
|
162
|
+
/** `true` if this account is on the current KDF (Argon2id). `false` means it's still on legacy PBKDF2 — see `migrate()` or `importFromMnemonicBackup()`. */
|
|
122
163
|
get isArgon2id() {
|
|
123
164
|
return this._kdfVersion === KDF_VERSION.ARGON2ID;
|
|
124
165
|
}
|
|
166
|
+
/** `true` if private key material is currently purged from memory (i.e. `lock()` was called, or `unlock()` hasn't been called yet). */
|
|
125
167
|
get isLocked() {
|
|
126
168
|
return this._privateKey === undefined;
|
|
127
169
|
}
|
|
170
|
+
/** `true` if private key material is currently decrypted in memory. The inverse of `isLocked`. */
|
|
128
171
|
get isUnlocked() {
|
|
129
172
|
return this._privateKey !== undefined;
|
|
130
173
|
}
|
|
174
|
+
/** ML-KEM-768 (FIPS-203) public key. Post-quantum key encapsulation. Always available, even when locked. */
|
|
131
175
|
get mlKemPublicKey() {
|
|
132
176
|
return this._mlKemPublicKey;
|
|
133
177
|
}
|
|
178
|
+
/** ML-KEM-768 secret key. `undefined` unless the account is unlocked. ⚠️ Live key material — prefer `getMlKemSecretKey()` if you want a thrown error instead of `undefined` on locked accounts. */
|
|
134
179
|
get mlKemSecretKey() {
|
|
135
180
|
return this._mlKemSecretKey;
|
|
136
181
|
}
|
|
182
|
+
/** `true` if this account has ML-KEM-768 keys (i.e. is post-quantum-encryption capable). `false` means it's a legacy account pending migration. */
|
|
137
183
|
get hasMlKem() {
|
|
138
184
|
return this._mlKemPublicKey !== undefined;
|
|
139
185
|
}
|
|
186
|
+
/** `true` if this account is on Argon2id *and* has ML-KEM-768 keys — i.e. fully migrated, nothing left to upgrade. */
|
|
140
187
|
get isFullyUpgraded() {
|
|
141
188
|
return this.isArgon2id && this.hasMlKem;
|
|
142
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* @experimental secp256k1 Bitcoin public key. `undefined` if this account
|
|
192
|
+
* has no stored Bitcoin key material (e.g. it predates Web3 support and
|
|
193
|
+
* hasn't been re-imported via `importFromMnemonicBackup()`).
|
|
194
|
+
*/
|
|
195
|
+
get btcPublicKey() {
|
|
196
|
+
return this._btcPublicKey;
|
|
197
|
+
}
|
|
198
|
+
/** @experimental `true` if this account has a stored Bitcoin keypair. */
|
|
199
|
+
get hasBitcoin() {
|
|
200
|
+
return this._btcPublicKey !== undefined;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Lightweight, non-secret snapshot of this account's state — no key bytes
|
|
204
|
+
* at all, encrypted or otherwise. Useful for account pickers, dashboards,
|
|
205
|
+
* or anywhere you want to display status without touching key material.
|
|
206
|
+
*/
|
|
143
207
|
get metadata() {
|
|
144
208
|
return {
|
|
145
209
|
id: this.id,
|
|
@@ -149,29 +213,57 @@ export class MajikKey {
|
|
|
149
213
|
isLocked: this.isLocked,
|
|
150
214
|
kdfVersion: this.kdfVersion,
|
|
151
215
|
hasMlKem: this.hasMlKem,
|
|
216
|
+
web3: {
|
|
217
|
+
hasBitcoin: this.hasBitcoin,
|
|
218
|
+
hasSolana: this.hasSolanaKeypair,
|
|
219
|
+
},
|
|
152
220
|
mnemonicLanguage: this.mnemonicLanguage || "en",
|
|
153
221
|
};
|
|
154
222
|
}
|
|
223
|
+
/** Ed25519 public key. Classical signing — same keypair the X25519 identity key is converted from. Always available, even when locked. */
|
|
155
224
|
get edPublicKey() {
|
|
156
225
|
return this._edPublicKey;
|
|
157
226
|
}
|
|
227
|
+
/** ML-DSA-87 (FIPS-204) public key. Post-quantum signing. Always available, even when locked. */
|
|
158
228
|
get mlDsaPublicKey() {
|
|
159
229
|
return this._mlDsaPublicKey;
|
|
160
230
|
}
|
|
231
|
+
/** `true` if this account has both Ed25519 and ML-DSA-87 signing keys. `false` means it's a legacy account pending migration. */
|
|
161
232
|
get hasSigningKeys() {
|
|
162
233
|
return (this._edPublicKey !== undefined && this._mlDsaPublicKey !== undefined);
|
|
163
234
|
}
|
|
164
235
|
// ── CREATE ──────────────────────────────────────────────────────────────────
|
|
165
|
-
|
|
236
|
+
/**
|
|
237
|
+
* Creates a brand-new MajikKey account from a BIP-39 mnemonic.
|
|
238
|
+
*
|
|
239
|
+
* Derives the full key set in one pass — X25519, ML-KEM-768, Ed25519,
|
|
240
|
+
* ML-DSA-87, and a domain-separated Bitcoin key (see `MAJIK_BITCOIN_DOMAIN_PATH`)
|
|
241
|
+
* — encrypts every private key with Argon2id (KDF v2), and returns an
|
|
242
|
+
* **already-unlocked** instance (no `unlock()` call needed right after
|
|
243
|
+
* `create()`).
|
|
244
|
+
*
|
|
245
|
+
* @param mnemonic - A valid BIP-39 mnemonic phrase (12 or 24 words), matching `mnemonicLanguage`. Generate one with `MajikKey.generateMnemonic()`.
|
|
246
|
+
* @param passphrase - Passphrase used to derive the Argon2id encryption key for every private key on this account. This is *not* the mnemonic — losing it without the mnemonic makes the account unrecoverable.
|
|
247
|
+
* @param label - Optional human-readable account name. Defaults to an empty string. Update later via `updateLabel()`.
|
|
248
|
+
* @param mnemonicLanguage - BIP-39 wordlist to validate `mnemonic` against. Defaults to `"en"`.
|
|
249
|
+
* @param options.deriveBitcoin - @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true`.
|
|
250
|
+
* @returns An unlocked `MajikKey` instance, ready for immediate use — call `.lock()` when you're done with it.
|
|
251
|
+
* @throws {MajikKeyError} If `mnemonic` fails validation, `passphrase`/`label` fail their validators, or `mnemonic` doesn't match `mnemonicLanguage`'s wordlist.
|
|
252
|
+
*/
|
|
253
|
+
static async create(mnemonic, passphrase, label, options = {
|
|
254
|
+
deriveBitcoin: true,
|
|
255
|
+
mnemonicLanguage: "en",
|
|
256
|
+
}) {
|
|
166
257
|
try {
|
|
167
258
|
MajikKeyValidator.validateMnemonic(mnemonic);
|
|
168
259
|
MajikKeyValidator.validatePassphrase(passphrase);
|
|
169
260
|
MajikKeyValidator.validateLabel(label);
|
|
170
|
-
const
|
|
261
|
+
const { deriveBitcoin, mnemonicLanguage } = options;
|
|
262
|
+
const wordlist = await MajikKey._getWordlist(mnemonicLanguage || "en");
|
|
171
263
|
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
172
264
|
throw new MajikKeyError("Invalid BIP39 mnemonic phrase");
|
|
173
265
|
}
|
|
174
|
-
const identity = await MajikKey._deriveAndEncryptFromMnemonic(mnemonic, passphrase);
|
|
266
|
+
const identity = await MajikKey._deriveAndEncryptFromMnemonic(mnemonic, passphrase, { deriveBitcoin: deriveBitcoin });
|
|
175
267
|
const privateKeyBase64 = await MajikKey._exportKeyToBase64(identity.privateKey);
|
|
176
268
|
const publicKeyBase64 = await MajikKey._exportKeyToBase64(identity.publicKey);
|
|
177
269
|
const backup = await MajikKey._exportMnemonicBackup(identity, mnemonic);
|
|
@@ -201,6 +293,12 @@ export class MajikKey {
|
|
|
201
293
|
encryptedMlDsaSecretKeyBase64: arrayBufferToBase64(identity.encryptedMlDsaSecretKey),
|
|
202
294
|
edSecretKey: identity.edSecretKey,
|
|
203
295
|
mlDsaSecretKey: identity.mlDsaSecretKey,
|
|
296
|
+
btcPublicKey: identity.btcPublicKey,
|
|
297
|
+
encryptedBtcSecretKey: identity.encryptedBtcSecretKey,
|
|
298
|
+
encryptedBtcSecretKeyBase64: identity.encryptedBtcSecretKey
|
|
299
|
+
? arrayBufferToBase64(identity.encryptedBtcSecretKey)
|
|
300
|
+
: undefined,
|
|
301
|
+
btcSecretKey: identity.btcSecretKey,
|
|
204
302
|
mnemonicLanguage: mnemonicLanguage,
|
|
205
303
|
});
|
|
206
304
|
}
|
|
@@ -243,6 +341,15 @@ export class MajikKey {
|
|
|
243
341
|
encryptedMlDsaSecretKeyBase64 = anyParsed.encryptedMlDsaSecretKey;
|
|
244
342
|
encryptedMlDsaSecretKey = base64ToArrayBuffer(anyParsed.encryptedMlDsaSecretKey);
|
|
245
343
|
}
|
|
344
|
+
const btcPublicKey = anyParsed.btcPublicKey
|
|
345
|
+
? base64ToUint8Array(anyParsed.btcPublicKey)
|
|
346
|
+
: undefined;
|
|
347
|
+
let encryptedBtcSecretKey;
|
|
348
|
+
let encryptedBtcSecretKeyBase64;
|
|
349
|
+
if (anyParsed.encryptedBtcSecretKey) {
|
|
350
|
+
encryptedBtcSecretKeyBase64 = anyParsed.encryptedBtcSecretKey;
|
|
351
|
+
encryptedBtcSecretKey = base64ToArrayBuffer(anyParsed.encryptedBtcSecretKey);
|
|
352
|
+
}
|
|
246
353
|
return new MajikKey({
|
|
247
354
|
id: validated.id,
|
|
248
355
|
publicKey: { raw: new Uint8Array(publicKeyBuffer) },
|
|
@@ -265,6 +372,9 @@ export class MajikKey {
|
|
|
265
372
|
mlDsaPublicKey,
|
|
266
373
|
encryptedMlDsaSecretKey,
|
|
267
374
|
encryptedMlDsaSecretKeyBase64,
|
|
375
|
+
btcPublicKey,
|
|
376
|
+
encryptedBtcSecretKey,
|
|
377
|
+
encryptedBtcSecretKeyBase64,
|
|
268
378
|
mnemonicLanguage: validated?.mnemonicLanguage || "en",
|
|
269
379
|
});
|
|
270
380
|
}
|
|
@@ -294,6 +404,9 @@ export class MajikKey {
|
|
|
294
404
|
mlKemSecretKeyBase64: arrayToBase64(this._mlKemSecretKey),
|
|
295
405
|
edSecretKeyBase64: arrayToBase64(this._edSecretKey),
|
|
296
406
|
mlDsaSecretKeyBase64: arrayToBase64(this._mlDsaSecretKey),
|
|
407
|
+
btcSecretKeyBase64: this._btcSecretKey
|
|
408
|
+
? arrayToBase64(this._btcSecretKey)
|
|
409
|
+
: undefined,
|
|
297
410
|
};
|
|
298
411
|
}
|
|
299
412
|
/**
|
|
@@ -323,6 +436,12 @@ export class MajikKey {
|
|
|
323
436
|
const mlDsaSecretKey = base64ToUint8Array(parsed.mlDsaSecretKeyBase64);
|
|
324
437
|
const mlKemPublicKey = base64ToUint8Array(parsed.mlKemPublicKey);
|
|
325
438
|
const mlKemSecretKey = base64ToUint8Array(parsed.mlKemSecretKeyBase64);
|
|
439
|
+
const btcPublicKey = parsed.btcPublicKey
|
|
440
|
+
? base64ToUint8Array(parsed.btcPublicKey)
|
|
441
|
+
: undefined;
|
|
442
|
+
const btcSecretKey = parsed.btcSecretKeyBase64
|
|
443
|
+
? base64ToUint8Array(parsed.btcSecretKeyBase64)
|
|
444
|
+
: undefined;
|
|
326
445
|
return new MajikKey({
|
|
327
446
|
id: parsed.id,
|
|
328
447
|
fingerprint: parsed.fingerprint,
|
|
@@ -341,6 +460,8 @@ export class MajikKey {
|
|
|
341
460
|
edSecretKey,
|
|
342
461
|
mlDsaPublicKey,
|
|
343
462
|
mlDsaSecretKey,
|
|
463
|
+
btcPublicKey,
|
|
464
|
+
btcSecretKey,
|
|
344
465
|
});
|
|
345
466
|
}
|
|
346
467
|
catch (err) {
|
|
@@ -421,6 +542,13 @@ export class MajikKey {
|
|
|
421
542
|
this._encryptedMlDsaSecretKeyBase64 = arrayBufferToBase64(encDsa);
|
|
422
543
|
this._mlDsaSecretKey = mlDsaSecretKeyBytes;
|
|
423
544
|
}
|
|
545
|
+
if (this._encryptedBtcSecretKey) {
|
|
546
|
+
const btcSecretKeyBytes = await MajikKey._decryptSigningKey(this._encryptedBtcSecretKey, currentPassphrase, salt);
|
|
547
|
+
const encBtc = await MajikKey._encryptSigningKey(btcSecretKeyBytes, newPassphrase, newSalt);
|
|
548
|
+
this._encryptedBtcSecretKey = encBtc;
|
|
549
|
+
this._encryptedBtcSecretKeyBase64 = arrayBufferToBase64(encBtc);
|
|
550
|
+
this._btcSecretKey = btcSecretKeyBytes;
|
|
551
|
+
}
|
|
424
552
|
return this;
|
|
425
553
|
}
|
|
426
554
|
catch (err) {
|
|
@@ -462,6 +590,7 @@ export class MajikKey {
|
|
|
462
590
|
this._mlKemSecretKey = undefined;
|
|
463
591
|
this._edSecretKey = undefined;
|
|
464
592
|
this._mlDsaSecretKey = undefined;
|
|
593
|
+
this._btcSecretKey = undefined;
|
|
465
594
|
this._solanaKeypairMaterial = undefined;
|
|
466
595
|
return this;
|
|
467
596
|
}
|
|
@@ -493,6 +622,9 @@ export class MajikKey {
|
|
|
493
622
|
if (this._encryptedMlDsaSecretKey) {
|
|
494
623
|
this._mlDsaSecretKey = await MajikKey._decryptSigningKey(this._encryptedMlDsaSecretKey, passphrase, salt);
|
|
495
624
|
}
|
|
625
|
+
if (this._encryptedBtcSecretKey) {
|
|
626
|
+
this._btcSecretKey = await MajikKey._decryptSigningKey(this._encryptedBtcSecretKey, passphrase, salt);
|
|
627
|
+
}
|
|
496
628
|
return this;
|
|
497
629
|
}
|
|
498
630
|
catch (err) {
|
|
@@ -566,6 +698,10 @@ export class MajikKey {
|
|
|
566
698
|
? arrayToBase64(this._mlDsaPublicKey)
|
|
567
699
|
: undefined,
|
|
568
700
|
encryptedMlDsaSecretKey: this._encryptedMlDsaSecretKeyBase64,
|
|
701
|
+
btcPublicKey: this._btcPublicKey
|
|
702
|
+
? arrayToBase64(this._btcPublicKey)
|
|
703
|
+
: undefined,
|
|
704
|
+
encryptedBtcSecretKey: this._encryptedBtcSecretKeyBase64,
|
|
569
705
|
mnemonicLanguage: this._mnemonicLanguage,
|
|
570
706
|
};
|
|
571
707
|
}
|
|
@@ -660,23 +796,19 @@ export class MajikKey {
|
|
|
660
796
|
/**
|
|
661
797
|
* Import a MajikKey from a mnemonic-encrypted backup.
|
|
662
798
|
*
|
|
663
|
-
* This is the FULL MIGRATION PATH for old accounts — Argon2id + ML-KEM in one step:
|
|
664
|
-
* 1. Verify the backup decrypts correctly (proves mnemonic is correct)
|
|
665
|
-
* 2. Re-derive the complete identity from the mnemonic (X25519 + ML-KEM-768)
|
|
666
|
-
* 3. Encrypt both private keys with Argon2id (v2) + fresh 32-byte salt
|
|
667
|
-
* 4. Return a fully-upgraded MajikKey with hasMlKem: true, isArgon2id: true
|
|
668
|
-
*
|
|
669
|
-
* Old accounts without ML-KEM keys become fully post-quantum capable
|
|
670
|
-
* automatically — no extra user steps. The mnemonic is the source of truth.
|
|
671
799
|
*/
|
|
672
|
-
static async importFromMnemonicBackup(backup, mnemonic, passphrase, label,
|
|
800
|
+
static async importFromMnemonicBackup(backup, mnemonic, passphrase, label, options = {
|
|
801
|
+
deriveBitcoin: true,
|
|
802
|
+
mnemonicLanguage: "en",
|
|
803
|
+
}) {
|
|
673
804
|
try {
|
|
674
805
|
if (!backup || typeof backup !== "string")
|
|
675
806
|
throw new MajikKeyError("Backup must be a non-empty string");
|
|
676
807
|
MajikKeyValidator.validateMnemonic(mnemonic);
|
|
677
808
|
MajikKeyValidator.validatePassphrase(passphrase);
|
|
678
809
|
MajikKeyValidator.validateLabel(label);
|
|
679
|
-
const
|
|
810
|
+
const { deriveBitcoin, mnemonicLanguage } = options;
|
|
811
|
+
const wordlist = await MajikKey._getWordlist(mnemonicLanguage || "en");
|
|
680
812
|
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
681
813
|
throw new MajikKeyError("Invalid BIP39 mnemonic phrase");
|
|
682
814
|
}
|
|
@@ -693,7 +825,7 @@ export class MajikKey {
|
|
|
693
825
|
// Verify mnemonic is correct before doing expensive re-derivation
|
|
694
826
|
await MajikKey._verifyBackupDecryption(parsed.iv, parsed.ciphertext, mnemonic, backupKdfVersion);
|
|
695
827
|
// Re-derive complete identity from mnemonic — gets ML-KEM for free
|
|
696
|
-
const identity = await MajikKey._deriveAndEncryptFromMnemonic(mnemonic, passphrase);
|
|
828
|
+
const identity = await MajikKey._deriveAndEncryptFromMnemonic(mnemonic, passphrase, { deriveBitcoin: deriveBitcoin });
|
|
697
829
|
const privateKeyBase64 = await MajikKey._exportKeyToBase64(identity.privateKey);
|
|
698
830
|
const publicKeyBase64 = await MajikKey._exportKeyToBase64(identity.publicKey);
|
|
699
831
|
const id = parsed.id || identity.id;
|
|
@@ -723,6 +855,12 @@ export class MajikKey {
|
|
|
723
855
|
encryptedMlDsaSecretKeyBase64: arrayBufferToBase64(identity.encryptedMlDsaSecretKey),
|
|
724
856
|
edSecretKey: identity.edSecretKey,
|
|
725
857
|
mlDsaSecretKey: identity.mlDsaSecretKey,
|
|
858
|
+
btcPublicKey: identity.btcPublicKey,
|
|
859
|
+
encryptedBtcSecretKey: identity.encryptedBtcSecretKey,
|
|
860
|
+
encryptedBtcSecretKeyBase64: identity.encryptedBtcSecretKey
|
|
861
|
+
? arrayBufferToBase64(identity.encryptedBtcSecretKey)
|
|
862
|
+
: undefined,
|
|
863
|
+
btcSecretKey: identity.btcSecretKey,
|
|
726
864
|
});
|
|
727
865
|
}
|
|
728
866
|
catch (err) {
|
|
@@ -737,7 +875,13 @@ export class MajikKey {
|
|
|
737
875
|
const mod = await loader();
|
|
738
876
|
return mod.wordlist;
|
|
739
877
|
}
|
|
740
|
-
|
|
878
|
+
/**
|
|
879
|
+
* @param mnemonic - BIP-39 mnemonic to derive the full key set from.
|
|
880
|
+
* @param passphrase - Passphrase used to derive the Argon2id encryption key shared by every private key produced here.
|
|
881
|
+
* @param options.deriveBitcoin - @experimental Set `false` to skip deriving the Bitcoin keypair. Defaults to `true`.
|
|
882
|
+
*/
|
|
883
|
+
static async _deriveAndEncryptFromMnemonic(mnemonic, passphrase, options) {
|
|
884
|
+
const deriveBitcoin = options?.deriveBitcoin ?? true;
|
|
741
885
|
const encIdentity = await EncryptionEngine.deriveIdentityFromMnemonic(mnemonic);
|
|
742
886
|
let exportedXPrivate;
|
|
743
887
|
try {
|
|
@@ -752,16 +896,30 @@ export class MajikKey {
|
|
|
752
896
|
throw new MajikKeyError("Cannot export private key: unsupported format");
|
|
753
897
|
}
|
|
754
898
|
}
|
|
755
|
-
// Single salt — one Argon2id derivation unlocks
|
|
899
|
+
// Single salt — one Argon2id derivation unlocks every key below
|
|
756
900
|
const salt = generateRandomBytes(SALT_SIZE);
|
|
757
901
|
const { blob: encryptedPrivateKey } = await MajikKey._encryptPrivateKey(exportedXPrivate, passphrase, salt);
|
|
758
902
|
const mlKemSecretKey = encIdentity.mlKemSecretKey;
|
|
759
903
|
const encryptedMlKemSecretKey = await MajikKey._encryptMlKemSecretKey(mlKemSecretKey, passphrase, salt);
|
|
760
|
-
// after the existing ML-KEM encryption block:
|
|
761
904
|
const edSecretKey = encIdentity.edSecretKey;
|
|
762
905
|
const encryptedEdSecretKey = await MajikKey._encryptSigningKey(edSecretKey, passphrase, salt);
|
|
763
906
|
const mlDsaSecretKey = encIdentity.mlDsaSecretKey;
|
|
764
907
|
const encryptedMlDsaSecretKey = await MajikKey._encryptSigningKey(mlDsaSecretKey, passphrase, salt);
|
|
908
|
+
// @experimental Bitcoin — real BIP-32/BIP-84 off the raw 64-byte BIP-39
|
|
909
|
+
// seed, using Majik's domain-separated path by default. Same salt,
|
|
910
|
+
// different IV, same pattern as ML-KEM/Ed25519/ML-DSA above. Skipped
|
|
911
|
+
// entirely when `deriveBitcoin` is false — no derivation cost paid,
|
|
912
|
+
// no key material generated.
|
|
913
|
+
let btcPublicKey;
|
|
914
|
+
let btcSecretKey;
|
|
915
|
+
let encryptedBtcSecretKey;
|
|
916
|
+
if (deriveBitcoin) {
|
|
917
|
+
const rawSeed = await mnemonicToSeed(mnemonic);
|
|
918
|
+
const btcMaterial = deriveBitcoinKeypairFromSeed(rawSeed);
|
|
919
|
+
btcPublicKey = btcMaterial.publicKey;
|
|
920
|
+
btcSecretKey = btcMaterial.privateKey;
|
|
921
|
+
encryptedBtcSecretKey = await MajikKey._encryptSigningKey(btcMaterial.privateKey, passphrase, salt);
|
|
922
|
+
}
|
|
765
923
|
return {
|
|
766
924
|
id: encIdentity.fingerprint,
|
|
767
925
|
publicKey: encIdentity.publicKey,
|
|
@@ -779,6 +937,9 @@ export class MajikKey {
|
|
|
779
937
|
mlDsaPublicKey: encIdentity.mlDsaPublicKey,
|
|
780
938
|
mlDsaSecretKey,
|
|
781
939
|
encryptedMlDsaSecretKey,
|
|
940
|
+
btcPublicKey,
|
|
941
|
+
btcSecretKey,
|
|
942
|
+
encryptedBtcSecretKey,
|
|
782
943
|
};
|
|
783
944
|
}
|
|
784
945
|
// ── PRIVATE: Encryption/Decryption ───────────────────────────────────────────
|
|
@@ -912,30 +1073,95 @@ export class MajikKey {
|
|
|
912
1073
|
}
|
|
913
1074
|
// ── WEB3 (EXPERIMENTAL) ─────────────────────────────────────────────────────
|
|
914
1075
|
/**
|
|
915
|
-
* @experimental
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1076
|
+
* @experimental
|
|
1077
|
+
*/
|
|
1078
|
+
getBtcSecretKey() {
|
|
1079
|
+
if (this.isLocked)
|
|
1080
|
+
throw new MajikKeyError("MajikKey is locked. Call unlock() first.");
|
|
1081
|
+
if (!this._btcSecretKey)
|
|
1082
|
+
throw new MajikKeyError("No Bitcoin secret key — re-import via importFromMnemonicBackup() for full migration.");
|
|
1083
|
+
return this._btcSecretKey;
|
|
1084
|
+
}
|
|
1085
|
+
// ── WEB3 (EXPERIMENTAL) — updated getter ────────────────────────────────────
|
|
1086
|
+
/**
|
|
1087
|
+
* @experimental
|
|
923
1088
|
*/
|
|
924
1089
|
get web3() {
|
|
925
1090
|
if (!this.hasSolanaKeypair)
|
|
926
1091
|
return undefined;
|
|
927
|
-
const
|
|
1092
|
+
const solanaMaterial = this._getOrDeriveSolanaMaterial();
|
|
1093
|
+
const btcMaterial = this._btcSecretKey && this._btcPublicKey
|
|
1094
|
+
? { privateKey: this._btcSecretKey, publicKey: this._btcPublicKey }
|
|
1095
|
+
: undefined;
|
|
928
1096
|
return {
|
|
929
1097
|
solana: {
|
|
930
|
-
publicKey:
|
|
931
|
-
secretKey:
|
|
932
|
-
address: solanaAddressFromPublicKey(
|
|
933
|
-
getSolanaKeypair: () => toSolanaKeyPairSigner(
|
|
934
|
-
getSolanaAddress: () => toSolanaAddress(
|
|
935
|
-
sign: (message) => signWithSolanaMaterial(
|
|
1098
|
+
publicKey: solanaMaterial.publicKey,
|
|
1099
|
+
secretKey: solanaMaterial.secretKey,
|
|
1100
|
+
address: solanaAddressFromPublicKey(solanaMaterial.publicKey),
|
|
1101
|
+
getSolanaKeypair: () => toSolanaKeyPairSigner(solanaMaterial),
|
|
1102
|
+
getSolanaAddress: () => toSolanaAddress(solanaMaterial),
|
|
1103
|
+
sign: (message) => signWithSolanaMaterial(solanaMaterial, message),
|
|
1104
|
+
},
|
|
1105
|
+
bitcoin: btcMaterial && {
|
|
1106
|
+
publicKey: btcMaterial.publicKey,
|
|
1107
|
+
privateKey: btcMaterial.privateKey,
|
|
1108
|
+
getBitcoinAddress: () => toBitcoinAddress(btcMaterial),
|
|
1109
|
+
getWIF: (options) => toWIF(btcMaterial, options),
|
|
1110
|
+
sign: (hash, scheme) => signWithBitcoinMaterial(btcMaterial, hash, scheme),
|
|
936
1111
|
},
|
|
937
1112
|
};
|
|
938
1113
|
}
|
|
1114
|
+
// ── BITCON (EXPERIMENTAL) ────────────────────────────────────
|
|
1115
|
+
/**
|
|
1116
|
+
* @experimental True if this MajikKey can currently produce Bitcoin
|
|
1117
|
+
* material (i.e. it's unlocked and has a Bitcoin secret key).
|
|
1118
|
+
*/
|
|
1119
|
+
get hasBitcoinKeypair() {
|
|
1120
|
+
return this.isUnlocked && this._btcSecretKey !== undefined;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* @experimental Raw Bitcoin keypair material. Pass `{ standard: true }` to
|
|
1124
|
+
* get the REAL BIP-84 mainnet key (recoverable in any standard wallet from
|
|
1125
|
+
* the mnemonic alone) instead of Majik's default domain-separated key.
|
|
1126
|
+
*
|
|
1127
|
+
* NOTE: `{ standard: true }` re-derives from the raw seed on demand and is
|
|
1128
|
+
* NOT the same key as `web3.bitcoin` (which is always the stored,
|
|
1129
|
+
* domain-separated default) — it requires the mnemonic to reproduce again
|
|
1130
|
+
* outside Majik, whereas the stored default does not.
|
|
1131
|
+
*/
|
|
1132
|
+
getBitcoinKeypairMaterial(options) {
|
|
1133
|
+
if (this.isLocked)
|
|
1134
|
+
throw new MajikKeyError("MajikKey is locked. Call unlock() first.");
|
|
1135
|
+
if (!options?.standard && !options?.path) {
|
|
1136
|
+
if (!this._btcSecretKey || !this._btcPublicKey)
|
|
1137
|
+
throw new MajikKeyError("No Bitcoin secret key — re-import via importFromMnemonicBackup() first.");
|
|
1138
|
+
return { privateKey: this._btcSecretKey, publicKey: this._btcPublicKey };
|
|
1139
|
+
}
|
|
1140
|
+
throw new MajikKeyError("Deriving the standard BIP-84 path requires the mnemonic — " +
|
|
1141
|
+
"use MajikKey.deriveStandardBitcoinFromMnemonic(mnemonic) instead.");
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* @experimental Derive the REAL BIP-84 mainnet Bitcoin keypair straight
|
|
1145
|
+
* from a mnemonic — for one-off export/verification. Does not require
|
|
1146
|
+
* an unlocked MajikKey instance.
|
|
1147
|
+
*/
|
|
1148
|
+
static async deriveStandardBitcoinFromMnemonic(mnemonic, mnemonicLanguage = "en") {
|
|
1149
|
+
MajikKeyValidator.validateMnemonic(mnemonic);
|
|
1150
|
+
const wordlist = await MajikKey._getWordlist(mnemonicLanguage);
|
|
1151
|
+
if (!validateMnemonic(mnemonic, wordlist)) {
|
|
1152
|
+
throw new MajikKeyError("Invalid BIP39 mnemonic phrase");
|
|
1153
|
+
}
|
|
1154
|
+
const seed = await mnemonicToSeed(mnemonic);
|
|
1155
|
+
return deriveBitcoinKeypairFromSeed(seed, { standard: true });
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* @experimental WIF export of the default (domain-separated) Bitcoin key.
|
|
1159
|
+
*/
|
|
1160
|
+
getBitcoinWIF(options) {
|
|
1161
|
+
const material = this.getBitcoinKeypairMaterial();
|
|
1162
|
+
return toWIF(material, options);
|
|
1163
|
+
}
|
|
1164
|
+
// ── SOLANA (EXPERIMENTAL) ────────────────────────────────────
|
|
939
1165
|
/**
|
|
940
1166
|
* @experimental True if this MajikKey can currently produce a Solana
|
|
941
1167
|
* keypair (i.e. it's unlocked and has an Ed25519 signing key).
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@majikah/majik-key",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "A post-quantum ready seed phrase account library for the Majikah ecosystem. Manages deterministic X25519 and ML-KEM-768 identities with Argon2id protection and seamless legacy account migration.",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.14",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -47,12 +47,18 @@
|
|
|
47
47
|
"prepublishOnly": "npm run build",
|
|
48
48
|
"package": "npm run build && npm version patch && git push && git push --tags",
|
|
49
49
|
"test": "vitest run",
|
|
50
|
-
"test:watch": "vitest"
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"test:web3:bitcoin": "npx vitest test/majik-key-bitcoin.test.ts",
|
|
52
|
+
"test:web3:solana": "npx vitest test/majik-key-solana.test.ts",
|
|
53
|
+
"test:web3": "npx vitest test/majik-key-bitcoin.test.ts test/majik-key-solana.test.ts",
|
|
54
|
+
"test:core": "npx vitest test/majik-key.test.ts"
|
|
51
55
|
},
|
|
52
56
|
"dependencies": {
|
|
53
57
|
"@majikah/majik-contact": "^0.0.6",
|
|
58
|
+
"@noble/curves": "^2.2.0",
|
|
54
59
|
"@noble/hashes": "^2.2.0",
|
|
55
60
|
"@noble/post-quantum": "^0.6.1",
|
|
61
|
+
"@scure/bip32": "^2.2.0",
|
|
56
62
|
"@scure/bip39": "^2.2.0",
|
|
57
63
|
"@stablelib/aes": "^2.0.1",
|
|
58
64
|
"@stablelib/ed25519": "^2.1.0",
|
|
@@ -65,6 +71,7 @@
|
|
|
65
71
|
"hash-wasm": "^4.12.0"
|
|
66
72
|
},
|
|
67
73
|
"devDependencies": {
|
|
74
|
+
"@scure/btc-signer": "^2.2.0",
|
|
68
75
|
"@solana/kit": "^7.0.0",
|
|
69
76
|
"@types/ed2curve": "^0.2.4",
|
|
70
77
|
"@types/node": "^26.1.0",
|
|
@@ -72,11 +79,15 @@
|
|
|
72
79
|
"vitest": "^4.1.9"
|
|
73
80
|
},
|
|
74
81
|
"peerDependencies": {
|
|
82
|
+
"@scure/btc-signer": "^2.2.0",
|
|
75
83
|
"@solana/kit": "^7.0.0"
|
|
76
84
|
},
|
|
77
85
|
"peerDependenciesMeta": {
|
|
78
86
|
"@solana/kit": {
|
|
79
87
|
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@scure/btc-signer": {
|
|
90
|
+
"optional": true
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
}
|
|
File without changes
|
|
File without changes
|