@majikah/majik-key 0.2.12 → 0.2.13

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/dist/majik-key.js CHANGED
@@ -21,7 +21,7 @@
21
21
  * point, so ML-KEM keys can be deterministically re-derived and stored.
22
22
  * No partial migration — either fully upgraded or not upgraded yet.
23
23
  */
24
- import { generateMnemonic as bip39GenerateMnemonic, validateMnemonic, } from "@scure/bip39";
24
+ import { generateMnemonic as bip39GenerateMnemonic, mnemonicToSeed, validateMnemonic, } from "@scure/bip39";
25
25
  import { aesGcmDecrypt, aesGcmEncrypt, deriveKeyFromPassphraseArgon2, deriveKeyFromMnemonicArgon2, deriveKeyFromPassphrase, generateRandomBytes, IV_LENGTH, } from "./core/crypto/crypto-provider";
26
26
  import { EncryptionEngine } from "./core/crypto/encryption-engine";
27
27
  import { MajikContact } from "@majikah/majik-contact";
@@ -31,7 +31,7 @@ import { MajikKeyValidator } from "./core/validator";
31
31
  import { MajikKeyError } from "./core/error";
32
32
  import { MajikMessageIdentity } from "./core/database/system/identity";
33
33
  import { WORDLISTS } from "./core/crypto/wordlist";
34
- import { deriveSolanaKeypairFromEdSecretKey, signWithSolanaMaterial, solanaAddressFromPublicKey, solanaMaterialFromEd25519SecretKey, toSolanaAddress, toSolanaKeyPairSigner, } from "./core/web3/solana";
34
+ import { deriveBitcoinKeypairFromSeed, signWithBitcoinMaterial, toBitcoinAddress, toWIF, deriveSolanaKeypairFromEdSecretKey, signWithSolanaMaterial, solanaAddressFromPublicKey, solanaMaterialFromEd25519SecretKey, toSolanaAddress, toSolanaKeyPairSigner, } from "./core/web3";
35
35
  const SALT_SIZE = 32;
36
36
  // ─── MajikKey ─────────────────────────────────────────────────────────────────
37
37
  export class MajikKey {
@@ -63,6 +63,10 @@ export class MajikKey {
63
63
  _encryptedMlDsaSecretKeyBase64;
64
64
  //Experimental
65
65
  _solanaKeypairMaterial;
66
+ _btcPublicKey;
67
+ _btcSecretKey;
68
+ _encryptedBtcSecretKey;
69
+ _encryptedBtcSecretKeyBase64;
66
70
  constructor(options) {
67
71
  this._id = options.id;
68
72
  this._publicKey = options.publicKey;
@@ -89,6 +93,10 @@ export class MajikKey {
89
93
  this._encryptedMlDsaSecretKeyBase64 = options.encryptedMlDsaSecretKeyBase64;
90
94
  this._edSecretKey = options.edSecretKey;
91
95
  this._mlDsaSecretKey = options.mlDsaSecretKey;
96
+ this._btcPublicKey = options.btcPublicKey;
97
+ this._btcSecretKey = options.btcSecretKey;
98
+ this._encryptedBtcSecretKey = options.encryptedBtcSecretKey;
99
+ this._encryptedBtcSecretKeyBase64 = options.encryptedBtcSecretKeyBase64;
92
100
  this._mnemonicLanguage = options.mnemonicLanguage || "en";
93
101
  }
94
102
  // ── Getters ─────────────────────────────────────────────────────────────────
@@ -140,6 +148,12 @@ export class MajikKey {
140
148
  get isFullyUpgraded() {
141
149
  return this.isArgon2id && this.hasMlKem;
142
150
  }
151
+ get btcPublicKey() {
152
+ return this._btcPublicKey;
153
+ }
154
+ get hasBitcoin() {
155
+ return this._btcPublicKey !== undefined;
156
+ }
143
157
  get metadata() {
144
158
  return {
145
159
  id: this.id,
@@ -149,6 +163,10 @@ export class MajikKey {
149
163
  isLocked: this.isLocked,
150
164
  kdfVersion: this.kdfVersion,
151
165
  hasMlKem: this.hasMlKem,
166
+ web3: {
167
+ hasBitcoin: this.hasBitcoin,
168
+ hasSolana: this.hasSolanaKeypair,
169
+ },
152
170
  mnemonicLanguage: this.mnemonicLanguage || "en",
153
171
  };
154
172
  }
@@ -201,6 +219,10 @@ export class MajikKey {
201
219
  encryptedMlDsaSecretKeyBase64: arrayBufferToBase64(identity.encryptedMlDsaSecretKey),
202
220
  edSecretKey: identity.edSecretKey,
203
221
  mlDsaSecretKey: identity.mlDsaSecretKey,
222
+ btcPublicKey: identity.btcPublicKey,
223
+ encryptedBtcSecretKey: identity.encryptedBtcSecretKey,
224
+ encryptedBtcSecretKeyBase64: arrayBufferToBase64(identity.encryptedBtcSecretKey),
225
+ btcSecretKey: identity.btcSecretKey,
204
226
  mnemonicLanguage: mnemonicLanguage,
205
227
  });
206
228
  }
@@ -243,6 +265,15 @@ export class MajikKey {
243
265
  encryptedMlDsaSecretKeyBase64 = anyParsed.encryptedMlDsaSecretKey;
244
266
  encryptedMlDsaSecretKey = base64ToArrayBuffer(anyParsed.encryptedMlDsaSecretKey);
245
267
  }
268
+ const btcPublicKey = anyParsed.btcPublicKey
269
+ ? base64ToUint8Array(anyParsed.btcPublicKey)
270
+ : undefined;
271
+ let encryptedBtcSecretKey;
272
+ let encryptedBtcSecretKeyBase64;
273
+ if (anyParsed.encryptedBtcSecretKey) {
274
+ encryptedBtcSecretKeyBase64 = anyParsed.encryptedBtcSecretKey;
275
+ encryptedBtcSecretKey = base64ToArrayBuffer(anyParsed.encryptedBtcSecretKey);
276
+ }
246
277
  return new MajikKey({
247
278
  id: validated.id,
248
279
  publicKey: { raw: new Uint8Array(publicKeyBuffer) },
@@ -265,6 +296,9 @@ export class MajikKey {
265
296
  mlDsaPublicKey,
266
297
  encryptedMlDsaSecretKey,
267
298
  encryptedMlDsaSecretKeyBase64,
299
+ btcPublicKey,
300
+ encryptedBtcSecretKey,
301
+ encryptedBtcSecretKeyBase64,
268
302
  mnemonicLanguage: validated?.mnemonicLanguage || "en",
269
303
  });
270
304
  }
@@ -294,6 +328,9 @@ export class MajikKey {
294
328
  mlKemSecretKeyBase64: arrayToBase64(this._mlKemSecretKey),
295
329
  edSecretKeyBase64: arrayToBase64(this._edSecretKey),
296
330
  mlDsaSecretKeyBase64: arrayToBase64(this._mlDsaSecretKey),
331
+ btcSecretKeyBase64: this._btcSecretKey
332
+ ? arrayToBase64(this._btcSecretKey)
333
+ : undefined,
297
334
  };
298
335
  }
299
336
  /**
@@ -323,6 +360,12 @@ export class MajikKey {
323
360
  const mlDsaSecretKey = base64ToUint8Array(parsed.mlDsaSecretKeyBase64);
324
361
  const mlKemPublicKey = base64ToUint8Array(parsed.mlKemPublicKey);
325
362
  const mlKemSecretKey = base64ToUint8Array(parsed.mlKemSecretKeyBase64);
363
+ const btcPublicKey = parsed.btcPublicKey
364
+ ? base64ToUint8Array(parsed.btcPublicKey)
365
+ : undefined;
366
+ const btcSecretKey = parsed.btcSecretKeyBase64
367
+ ? base64ToUint8Array(parsed.btcSecretKeyBase64)
368
+ : undefined;
326
369
  return new MajikKey({
327
370
  id: parsed.id,
328
371
  fingerprint: parsed.fingerprint,
@@ -341,6 +384,8 @@ export class MajikKey {
341
384
  edSecretKey,
342
385
  mlDsaPublicKey,
343
386
  mlDsaSecretKey,
387
+ btcPublicKey,
388
+ btcSecretKey,
344
389
  });
345
390
  }
346
391
  catch (err) {
@@ -421,6 +466,13 @@ export class MajikKey {
421
466
  this._encryptedMlDsaSecretKeyBase64 = arrayBufferToBase64(encDsa);
422
467
  this._mlDsaSecretKey = mlDsaSecretKeyBytes;
423
468
  }
469
+ if (this._encryptedBtcSecretKey) {
470
+ const btcSecretKeyBytes = await MajikKey._decryptSigningKey(this._encryptedBtcSecretKey, currentPassphrase, salt);
471
+ const encBtc = await MajikKey._encryptSigningKey(btcSecretKeyBytes, newPassphrase, newSalt);
472
+ this._encryptedBtcSecretKey = encBtc;
473
+ this._encryptedBtcSecretKeyBase64 = arrayBufferToBase64(encBtc);
474
+ this._btcSecretKey = btcSecretKeyBytes;
475
+ }
424
476
  return this;
425
477
  }
426
478
  catch (err) {
@@ -462,6 +514,7 @@ export class MajikKey {
462
514
  this._mlKemSecretKey = undefined;
463
515
  this._edSecretKey = undefined;
464
516
  this._mlDsaSecretKey = undefined;
517
+ this._btcSecretKey = undefined;
465
518
  this._solanaKeypairMaterial = undefined;
466
519
  return this;
467
520
  }
@@ -493,6 +546,9 @@ export class MajikKey {
493
546
  if (this._encryptedMlDsaSecretKey) {
494
547
  this._mlDsaSecretKey = await MajikKey._decryptSigningKey(this._encryptedMlDsaSecretKey, passphrase, salt);
495
548
  }
549
+ if (this._encryptedBtcSecretKey) {
550
+ this._btcSecretKey = await MajikKey._decryptSigningKey(this._encryptedBtcSecretKey, passphrase, salt);
551
+ }
496
552
  return this;
497
553
  }
498
554
  catch (err) {
@@ -566,6 +622,10 @@ export class MajikKey {
566
622
  ? arrayToBase64(this._mlDsaPublicKey)
567
623
  : undefined,
568
624
  encryptedMlDsaSecretKey: this._encryptedMlDsaSecretKeyBase64,
625
+ btcPublicKey: this._btcPublicKey
626
+ ? arrayToBase64(this._btcPublicKey)
627
+ : undefined,
628
+ encryptedBtcSecretKey: this._encryptedBtcSecretKeyBase64,
569
629
  mnemonicLanguage: this._mnemonicLanguage,
570
630
  };
571
631
  }
@@ -723,6 +783,10 @@ export class MajikKey {
723
783
  encryptedMlDsaSecretKeyBase64: arrayBufferToBase64(identity.encryptedMlDsaSecretKey),
724
784
  edSecretKey: identity.edSecretKey,
725
785
  mlDsaSecretKey: identity.mlDsaSecretKey,
786
+ btcPublicKey: identity.btcPublicKey,
787
+ encryptedBtcSecretKey: identity.encryptedBtcSecretKey,
788
+ encryptedBtcSecretKeyBase64: arrayBufferToBase64(identity.encryptedBtcSecretKey),
789
+ btcSecretKey: identity.btcSecretKey,
726
790
  });
727
791
  }
728
792
  catch (err) {
@@ -762,6 +826,12 @@ export class MajikKey {
762
826
  const encryptedEdSecretKey = await MajikKey._encryptSigningKey(edSecretKey, passphrase, salt);
763
827
  const mlDsaSecretKey = encIdentity.mlDsaSecretKey;
764
828
  const encryptedMlDsaSecretKey = await MajikKey._encryptSigningKey(mlDsaSecretKey, passphrase, salt);
829
+ // Bitcoin — real BIP-32/BIP-84 off the raw 64-byte BIP-39 seed, using
830
+ // Majik's domain-separated path by default. Same salt, different IV,
831
+ // same pattern as ML-KEM/Ed25519/ML-DSA above.
832
+ const rawSeed = await mnemonicToSeed(mnemonic);
833
+ const btcMaterial = deriveBitcoinKeypairFromSeed(rawSeed);
834
+ const encryptedBtcSecretKey = await MajikKey._encryptSigningKey(btcMaterial.privateKey, passphrase, salt);
765
835
  return {
766
836
  id: encIdentity.fingerprint,
767
837
  publicKey: encIdentity.publicKey,
@@ -779,6 +849,9 @@ export class MajikKey {
779
849
  mlDsaPublicKey: encIdentity.mlDsaPublicKey,
780
850
  mlDsaSecretKey,
781
851
  encryptedMlDsaSecretKey,
852
+ btcPublicKey: btcMaterial.publicKey,
853
+ btcSecretKey: btcMaterial.privateKey,
854
+ encryptedBtcSecretKey,
782
855
  };
783
856
  }
784
857
  // ── PRIVATE: Encryption/Decryption ───────────────────────────────────────────
@@ -911,31 +984,90 @@ export class MajikKey {
911
984
  return arrayBufferToBase64(raw);
912
985
  }
913
986
  // ── WEB3 (EXPERIMENTAL) ─────────────────────────────────────────────────────
914
- /**
915
- * @experimental Blockchain/web3 integrations are experimental — this API
916
- * may change without notice. `undefined` when the MajikKey is locked or
917
- * has no Ed25519 signing key to derive from.
918
- *
919
- * By default `web3.solana` is DOMAIN-SEPARATED from the MajikKey's message
920
- * signing key (see deriveSolanaKeypairFromEdSecretKey). Use
921
- * `getSolanaKeypairMaterial({ reuseMessageKey: true })` if you specifically
922
- * want the identical key reused for Solana instead.
923
- */
987
+ getBtcSecretKey() {
988
+ if (this.isLocked)
989
+ throw new MajikKeyError("MajikKey is locked. Call unlock() first.");
990
+ if (!this._btcSecretKey)
991
+ throw new MajikKeyError("No Bitcoin secret key — re-import via importFromMnemonicBackup() for full migration.");
992
+ return this._btcSecretKey;
993
+ }
994
+ // ── WEB3 (EXPERIMENTAL) — updated getter ────────────────────────────────────
924
995
  get web3() {
925
996
  if (!this.hasSolanaKeypair)
926
997
  return undefined;
927
- const material = this._getOrDeriveSolanaMaterial();
998
+ const solanaMaterial = this._getOrDeriveSolanaMaterial();
999
+ const btcMaterial = this._btcSecretKey && this._btcPublicKey
1000
+ ? { privateKey: this._btcSecretKey, publicKey: this._btcPublicKey }
1001
+ : undefined;
928
1002
  return {
929
1003
  solana: {
930
- publicKey: material.publicKey,
931
- secretKey: material.secretKey,
932
- address: solanaAddressFromPublicKey(material.publicKey),
933
- getSolanaKeypair: () => toSolanaKeyPairSigner(material),
934
- getSolanaAddress: () => toSolanaAddress(material),
935
- sign: (message) => signWithSolanaMaterial(material, message),
1004
+ publicKey: solanaMaterial.publicKey,
1005
+ secretKey: solanaMaterial.secretKey,
1006
+ address: solanaAddressFromPublicKey(solanaMaterial.publicKey),
1007
+ getSolanaKeypair: () => toSolanaKeyPairSigner(solanaMaterial),
1008
+ getSolanaAddress: () => toSolanaAddress(solanaMaterial),
1009
+ sign: (message) => signWithSolanaMaterial(solanaMaterial, message),
1010
+ },
1011
+ bitcoin: btcMaterial && {
1012
+ publicKey: btcMaterial.publicKey,
1013
+ privateKey: btcMaterial.privateKey,
1014
+ getBitcoinAddress: () => toBitcoinAddress(btcMaterial),
1015
+ getWIF: (options) => toWIF(btcMaterial, options),
1016
+ sign: (hash, scheme) => signWithBitcoinMaterial(btcMaterial, hash, scheme),
936
1017
  },
937
1018
  };
938
1019
  }
1020
+ // ── BITCON (EXPERIMENTAL) ────────────────────────────────────
1021
+ /**
1022
+ * @experimental True if this MajikKey can currently produce Bitcoin
1023
+ * material (i.e. it's unlocked and has a Bitcoin secret key).
1024
+ */
1025
+ get hasBitcoinKeypair() {
1026
+ return this.isUnlocked && this._btcSecretKey !== undefined;
1027
+ }
1028
+ /**
1029
+ * @experimental Raw Bitcoin keypair material. Pass `{ standard: true }` to
1030
+ * get the REAL BIP-84 mainnet key (recoverable in any standard wallet from
1031
+ * the mnemonic alone) instead of Majik's default domain-separated key.
1032
+ *
1033
+ * NOTE: `{ standard: true }` re-derives from the raw seed on demand and is
1034
+ * NOT the same key as `web3.bitcoin` (which is always the stored,
1035
+ * domain-separated default) — it requires the mnemonic to reproduce again
1036
+ * outside Majik, whereas the stored default does not.
1037
+ */
1038
+ getBitcoinKeypairMaterial(options) {
1039
+ if (this.isLocked)
1040
+ throw new MajikKeyError("MajikKey is locked. Call unlock() first.");
1041
+ if (!options?.standard && !options?.path) {
1042
+ if (!this._btcSecretKey || !this._btcPublicKey)
1043
+ throw new MajikKeyError("No Bitcoin secret key — re-import via importFromMnemonicBackup() first.");
1044
+ return { privateKey: this._btcSecretKey, publicKey: this._btcPublicKey };
1045
+ }
1046
+ throw new MajikKeyError("Deriving the standard BIP-84 path requires the mnemonic — " +
1047
+ "use MajikKey.deriveStandardBitcoinFromMnemonic(mnemonic) instead.");
1048
+ }
1049
+ /**
1050
+ * @experimental Derive the REAL BIP-84 mainnet Bitcoin keypair straight
1051
+ * from a mnemonic — for one-off export/verification. Does not require
1052
+ * an unlocked MajikKey instance.
1053
+ */
1054
+ static async deriveStandardBitcoinFromMnemonic(mnemonic, mnemonicLanguage = "en") {
1055
+ MajikKeyValidator.validateMnemonic(mnemonic);
1056
+ const wordlist = await MajikKey._getWordlist(mnemonicLanguage);
1057
+ if (!validateMnemonic(mnemonic, wordlist)) {
1058
+ throw new MajikKeyError("Invalid BIP39 mnemonic phrase");
1059
+ }
1060
+ const seed = await mnemonicToSeed(mnemonic);
1061
+ return deriveBitcoinKeypairFromSeed(seed, { standard: true });
1062
+ }
1063
+ /**
1064
+ * @experimental WIF export of the default (domain-separated) Bitcoin key.
1065
+ */
1066
+ getBitcoinWIF(options) {
1067
+ const material = this.getBitcoinKeypairMaterial();
1068
+ return toWIF(material, options);
1069
+ }
1070
+ // ── SOLANA (EXPERIMENTAL) ────────────────────────────────────
939
1071
  /**
940
1072
  * @experimental True if this MajikKey can currently produce a Solana
941
1073
  * 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.12",
5
+ "version": "0.2.13",
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
  }