@majikah/majik-key 0.3.1 → 0.3.3

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 CHANGED
@@ -1,7 +1,10 @@
1
1
  # Majik Key
2
2
 
3
+
4
+
3
5
  [![Developed by Zelijah](https://img.shields.io/badge/Developed%20by-Zelijah-red?logo=github&logoColor=white)](https://www.thezelijah.world) ![GitHub Sponsors](https://img.shields.io/github/sponsors/jedlsf?style=plastic&label=Sponsors&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fjedlsf)
4
- ![npm](https://img.shields.io/npm/v/@majikah/majik-key) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-key) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
6
+
7
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21339132.svg)](https://doi.org/10.5281/zenodo.21339132) ![npm](https://img.shields.io/npm/v/@majikah/majik-key) ![npm downloads](https://img.shields.io/npm/dm/@majikah/majik-key) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
5
8
 
6
9
  **Majik Key** turns a single BIP-39 mnemonic into a complete cryptographic identity — encryption, classical + post-quantum signing, and (experimentally) Bitcoin and Solana keys — encrypted at rest and ready to plug into the rest of the Majikah ecosystem.
7
10
 
@@ -157,10 +160,10 @@ Majik Key can derive **Bitcoin** and **Solana** key material directly from the s
157
160
  - **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.
158
161
  - **The optional peer dependencies are only needed for chain-native address/transaction objects:**
159
162
 
160
- | Chain | Peer dependency | Needed for |
161
- | :--- | :--- | :--- |
163
+ | Chain | Peer dependency | Needed for |
164
+ | :------ | :------------------ | :--------------------------------------------------------- |
162
165
  | Bitcoin | `@scure/btc-signer` | Native SegWit (bech32) address encoding, PSBT construction |
163
- | Solana | `@solana/kit` | Real `KeyPairSigner` instances, kit-native `Address` type |
166
+ | Solana | `@solana/kit` | Real `KeyPairSigner` instances, kit-native `Address` type |
164
167
 
165
168
  ```bash
166
169
  npm install @scure/btc-signer # for Bitcoin addresses
@@ -234,38 +237,38 @@ localStorage.setItem('myKey', key.toString());
234
237
 
235
238
  ### Static Methods (Lifecycle & Generation)
236
239
 
237
- | Method | Parameters | Returns | Description |
238
- | :--- | :--- | :--- | :--- |
239
- | `create()` | `mnemonic`, `passphrase`, `label?`, `mnemonicLanguage?` | `Promise<MajikKey>` | Creates a new Argon2id-protected, fully post-quantum-capable account. |
240
- | `fromJSON()` | `json` | `MajikKey` | Loads a locked key from safe JSON storage. |
241
- | `fromMnemonicJSON()` | `mnemonicJson`, `passphrase`, `label?` | `Promise<MajikKey>` | Rebuilds a key straight from a portable seed export. |
242
- | `importFromMnemonicBackup()` | `backup`, `mnemonic`, `passphrase`, `label?`, `mnemonicLanguage?` | `Promise<MajikKey>` | Full migration path — verifies the mnemonic, then re-derives and re-encrypts the complete identity with Argon2id. |
243
- | `fromDangerousJSON()` | `json` | `MajikKey` | Reconstructs an already-unlocked key from a dangerous export. Server-side only — see warning below. |
244
- | `generateMnemonic()` | `strength?` *(128 \| 256)*, `language?` | `Promise<string>` | Generates a 12- or 24-word BIP-39 phrase. |
245
- | `validateMnemonic()` | `mnemonic` | `boolean` | Validates a BIP-39 mnemonic phrase. |
246
- | `deriveStandardBitcoinFromMnemonic()` *(experimental)* | `mnemonic`, `mnemonicLanguage?` | `Promise<BitcoinKeypairMaterial>` | Derives the real BIP-84 mainnet Bitcoin key without needing a `MajikKey` instance. |
240
+ | Method | Parameters | Returns | Description |
241
+ | :----------------------------------------------------- | :---------------------------------------------------------------- | :-------------------------------- | :---------------------------------------------------------------------------------------------------------------- |
242
+ | `create()` | `mnemonic`, `passphrase`, `label?`, `mnemonicLanguage?` | `Promise<MajikKey>` | Creates a new Argon2id-protected, fully post-quantum-capable account. |
243
+ | `fromJSON()` | `json` | `MajikKey` | Loads a locked key from safe JSON storage. |
244
+ | `fromMnemonicJSON()` | `mnemonicJson`, `passphrase`, `label?` | `Promise<MajikKey>` | Rebuilds a key straight from a portable seed export. |
245
+ | `importFromMnemonicBackup()` | `backup`, `mnemonic`, `passphrase`, `label?`, `mnemonicLanguage?` | `Promise<MajikKey>` | Full migration path — verifies the mnemonic, then re-derives and re-encrypts the complete identity with Argon2id. |
246
+ | `fromDangerousJSON()` | `json` | `MajikKey` | Reconstructs an already-unlocked key from a dangerous export. Server-side only — see warning below. |
247
+ | `generateMnemonic()` | `strength?` *(128 \| 256)*, `language?` | `Promise<string>` | Generates a 12- or 24-word BIP-39 phrase. |
248
+ | `validateMnemonic()` | `mnemonic` | `boolean` | Validates a BIP-39 mnemonic phrase. |
249
+ | `deriveStandardBitcoinFromMnemonic()` *(experimental)* | `mnemonic`, `mnemonicLanguage?` | `Promise<BitcoinKeypairMaterial>` | Derives the real BIP-84 mainnet Bitcoin key without needing a `MajikKey` instance. |
247
250
 
248
251
  ### Instance Methods (State & Management)
249
252
 
250
- | Method | Parameters | Returns | Description |
251
- | :--- | :--- | :--- | :--- |
252
- | `unlock()` | `passphrase` | `Promise<this>` | Decrypts keys into memory. Chainable. |
253
- | `lock()` | None | `this` | Purges all private key material (including cached Web3 keys) from memory. Chainable. |
254
- | `verify()` | `passphrase` | `Promise<boolean>` | Tests a passphrase without keeping keys in memory or requiring an unlock. |
255
- | `updatePassphrase()` | `currentPass`, `newPass` | `Promise<this>` | Re-encrypts every stored key under a new passphrase and migrates to KDF v2 if needed. |
256
- | `migrate()` | `passphrase` | `Promise<this>` | Upgrades the X25519 key's KDF from v1 to v2 only — does **not** add ML-KEM/Ed25519/ML-DSA/Bitcoin keys. Use `importFromMnemonicBackup()` for a full upgrade. |
257
- | `updateLabel()` | `newLabel` | `this` | Updates the human-readable account label. |
253
+ | Method | Parameters | Returns | Description |
254
+ | :------------------- | :----------------------- | :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
255
+ | `unlock()` | `passphrase` | `Promise<this>` | Decrypts keys into memory. Chainable. |
256
+ | `lock()` | None | `this` | Purges all private key material (including cached Web3 keys) from memory. Chainable. |
257
+ | `verify()` | `passphrase` | `Promise<boolean>` | Tests a passphrase without keeping keys in memory or requiring an unlock. |
258
+ | `updatePassphrase()` | `currentPass`, `newPass` | `Promise<this>` | Re-encrypts every stored key under a new passphrase and migrates to KDF v2 if needed. |
259
+ | `migrate()` | `passphrase` | `Promise<this>` | Upgrades the X25519 key's KDF from v1 to v2 only — does **not** add ML-KEM/Ed25519/ML-DSA/Bitcoin keys. Use `importFromMnemonicBackup()` for a full upgrade. |
260
+ | `updateLabel()` | `newLabel` | `this` | Updates the human-readable account label. |
258
261
 
259
262
  ### Export & Integration Methods
260
263
 
261
- | Method | Returns | Description |
262
- | :--- | :--- | :--- |
263
- | `toJSON()` / `toString()` | `MajikKeyJSON` / `string` | Safe export for DB/LocalStorage. No raw keys. |
264
- | `toDangerousJSON()` | `MajikKeyDangerousJSON` | ⚠️ Contains every raw private key. Server-side secret injection only — see warning below. |
265
- | `toMnemonicJSON()` | `MnemonicJSON` | ⚠️ Contains the raw mnemonic words (and passphrase, if you pass one) in plaintext — a transport format, not an at-rest storage format. Requires the key to be unlocked. |
266
- | `exportMnemonicBackup()` | `Promise<string>` | Encrypted backup string, decryptable only with the original mnemonic — used to verify a mnemonic before `importFromMnemonicBackup()` re-derives the identity. |
267
- | `toContact()` | `MajikContact` | Extracts public identity data for sharing (the basis for Majik Universal ID). |
268
- | `toMajikMessageIdentity()` | `Promise<MajikMessageIdentity>` | Formats the key for direct use in Majik Message. Requires a `MajikUser`. |
264
+ | Method | Returns | Description |
265
+ | :------------------------- | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
266
+ | `toJSON()` / `toString()` | `MajikKeyJSON` / `string` | Safe export for DB/LocalStorage. No raw keys. |
267
+ | `toDangerousJSON()` | `MajikKeyDangerousJSON` | ⚠️ Contains every raw private key. Server-side secret injection only — see warning below. |
268
+ | `toMnemonicJSON()` | `MnemonicJSON` | ⚠️ Contains the raw mnemonic words (and passphrase, if you pass one) in plaintext — a transport format, not an at-rest storage format. Requires the key to be unlocked. |
269
+ | `exportMnemonicBackup()` | `Promise<string>` | Encrypted backup string, decryptable only with the original mnemonic — used to verify a mnemonic before `importFromMnemonicBackup()` re-derives the identity. |
270
+ | `toContact()` | `MajikContact` | Extracts public identity data for sharing (the basis for Majik Universal ID). |
271
+ | `toMajikMessageIdentity()` | `Promise<MajikMessageIdentity>` | Formats the key for direct use in Majik Message. Requires a `MajikUser`. |
269
272
 
270
273
  ### Instance Getters
271
274
 
@@ -279,14 +282,14 @@ localStorage.setItem('myKey', key.toString());
279
282
 
280
283
  ### Web3 (Experimental)
281
284
 
282
- | Member | Returns | Notes |
283
- | :--- | :--- | :--- |
284
- | `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. |
285
- | `getBitcoinKeypairMaterial()` | `BitcoinKeypairMaterial` | Raw Bitcoin keypair bytes. |
286
- | `getBitcoinWIF()` | `string` | Wallet Import Format string, pastes into any standard Bitcoin wallet. |
287
- | `getSolanaKeypairMaterial()` | `SolanaKeypairMaterial` | Raw Solana keypair bytes. |
288
- | `getSolanaKeypair()` | `Promise<any>` | Real `@solana/kit` `KeyPairSigner`. Requires `@solana/kit`. |
289
- | `getSolanaAddress()` | `string` | Base58 Solana address. No extra dependency required. |
285
+ | Member | Returns | Notes |
286
+ | :---------------------------- | :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
287
+ | `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. |
288
+ | `getBitcoinKeypairMaterial()` | `BitcoinKeypairMaterial` | Raw Bitcoin keypair bytes. |
289
+ | `getBitcoinWIF()` | `string` | Wallet Import Format string, pastes into any standard Bitcoin wallet. |
290
+ | `getSolanaKeypairMaterial()` | `SolanaKeypairMaterial` | Raw Solana keypair bytes. |
291
+ | `getSolanaKeypair()` | `Promise<any>` | Real `@solana/kit` `KeyPairSigner`. Requires `@solana/kit`. |
292
+ | `getSolanaAddress()` | `string` | Base58 Solana address. No extra dependency required. |
290
293
 
291
294
  ---
292
295
 
@@ -391,9 +394,13 @@ Developed by **Josef Elijah Fabian (Zelijah)** | [Majikah Solutions OPC](https:/
391
394
 
392
395
 
393
396
  **Developer**: [Josef Elijah Fabian](https://github.com/jedlsf)
397
+
394
398
  **GitHub**: [https://github.com/Majikah](https://github.com/Majikah)
399
+
395
400
  **Project Repository**: [https://github.com/Majikah/majik-signature](https://github.com/Majikah/majik-signature)
396
401
 
402
+ **Technical Whitepaper**: [https://zenodo.org/records/21339132](https://zenodo.org/records/21339132)
403
+
397
404
  ---
398
405
 
399
406
  ## Contact
@@ -11,10 +11,12 @@ import { argon2id as nobleArgon2id } from "@noble/hashes/argon2.js";
11
11
  import { ARGON2_PARAMS } from "./constants";
12
12
  import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
13
13
  import { argon2id as hashWasmArgon2id } from "hash-wasm";
14
+ const secureGetRandomValues = crypto.getRandomValues.bind(crypto);
15
+ const secureFill = Uint8Array.prototype.fill;
14
16
  export const IV_LENGTH = 12;
15
17
  export function generateRandomBytes(len) {
16
18
  const b = new Uint8Array(len);
17
- crypto.getRandomValues(b);
19
+ secureGetRandomValues(b);
18
20
  return b;
19
21
  }
20
22
  export function generateEd25519Keypair() {
@@ -133,8 +135,13 @@ async function _argon2id(input, salt, params) {
133
135
  */
134
136
  export async function deriveKeyFromPassphraseArgon2(passphrase, salt) {
135
137
  const pw = new TextEncoder().encode(passphrase);
136
- const result = await _argon2id(pw, salt, ARGON2_PARAMS.PASSPHRASE);
137
- return result.slice(); // ← force a copy out of WASM memory
138
+ try {
139
+ const result = await _argon2id(pw, salt, ARGON2_PARAMS.PASSPHRASE);
140
+ return result.slice();
141
+ }
142
+ finally {
143
+ secureFill.call(pw, 0);
144
+ }
138
145
  }
139
146
  /**
140
147
  * Derive a 32-byte AES key from a BIP-39 mnemonic using Argon2id.
@@ -146,8 +153,13 @@ export async function deriveKeyFromPassphraseArgon2(passphrase, salt) {
146
153
  */
147
154
  export async function deriveKeyFromMnemonicArgon2(mnemonic, salt) {
148
155
  const m = new TextEncoder().encode(mnemonic);
149
- const result = await _argon2id(m, salt, ARGON2_PARAMS.MNEMONIC);
150
- return result.slice(); // ← force a copy out of WASM memory
156
+ try {
157
+ const result = await _argon2id(m, salt, ARGON2_PARAMS.MNEMONIC);
158
+ return result.slice(); // ← force a copy out of WASM memory
159
+ }
160
+ finally {
161
+ secureFill.call(m, 0);
162
+ }
151
163
  }
152
164
  // ─── KDF v1: PBKDF2-SHA256 (legacy — do not use for new operations) ───────────
153
165
  /**
@@ -1,8 +1,8 @@
1
1
  export interface EncryptionIdentity {
2
- publicKey: CryptoKey | {
2
+ publicKey: {
3
3
  raw: Uint8Array;
4
4
  };
5
- privateKey: CryptoKey | {
5
+ privateKey: {
6
6
  raw: Uint8Array;
7
7
  };
8
8
  fingerprint: string;
@@ -7,6 +7,7 @@ import { deriveMlKemKeypairFromSeed, fingerprintFromPublicRaw, } from "./crypto-
7
7
  import { concatUint8Arrays } from "../utils";
8
8
  import { hash } from "@stablelib/sha256";
9
9
  import { MAJIK_SIGNATURE_SEED } from "./constants";
10
+ const secureFill = Uint8Array.prototype.fill;
10
11
  /**
11
12
  * EncryptionEngine
12
13
  * ----------------
@@ -35,15 +36,22 @@ export class EncryptionEngine {
35
36
  * seed[32..64] for the implicit rejection parameter `z`.
36
37
  */
37
38
  static async deriveIdentityFromMnemonic(mnemonic) {
39
+ if (typeof mnemonic !== "string" || mnemonic.trim().length === 0) {
40
+ throw new CryptoError("Mnemonic must be a non-empty string");
41
+ }
42
+ // Step 1: BIP-39 seed → 64 bytes
43
+ const seed = mnemonicToSeedSync(mnemonic); // returns Buffer (Node) or Uint8Array
44
+ const seed64 = new Uint8Array(seed); // normalize to Uint8Array
45
+ // Step 3: ML-KEM-768 keypair from FULL 64-byte seed (new)
46
+ // ml_kem768.keygen() accepts a 64-byte seed directly.
47
+ // seed[0..32] → lattice key matrix expansion (K-PKE keygen)
48
+ // seed[32..64] → implicit rejection parameter z (stored in secretKey)
49
+ const mlKemKeypair = deriveMlKemKeypairFromSeed(seed64);
50
+ const mlDsaSeedInput = concatUint8Arrays(seed64, new TextEncoder().encode(MAJIK_SIGNATURE_SEED));
51
+ const mlDsaSeed = hash(mlDsaSeedInput);
52
+ // Step 2: X25519 identity from first 32 bytes (existing path)
53
+ const seed32 = seed64.subarray(0, 32);
38
54
  try {
39
- if (typeof mnemonic !== "string" || mnemonic.trim().length === 0) {
40
- throw new CryptoError("Mnemonic must be a non-empty string");
41
- }
42
- // Step 1: BIP-39 seed → 64 bytes
43
- const seed = mnemonicToSeedSync(mnemonic); // returns Buffer (Node) or Uint8Array
44
- const seed64 = new Uint8Array(seed); // normalize to Uint8Array
45
- // Step 2: X25519 identity from first 32 bytes (existing path)
46
- const seed32 = seed64.subarray(0, 32);
47
55
  const ed = ed25519.generateKeyPairFromSeed(seed32);
48
56
  const skCurve = ed2curve.convertSecretKey(ed.secretKey);
49
57
  const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
@@ -55,12 +63,6 @@ export class EncryptionEngine {
55
63
  const publicKey = { type: "public", raw: pkCurveBytes };
56
64
  const privateKey = { type: "private", raw: skCurveBytes };
57
65
  const fingerprint = fingerprintFromPublicRaw(pkCurveBytes);
58
- // Step 3: ML-KEM-768 keypair from FULL 64-byte seed (new)
59
- // ml_kem768.keygen() accepts a 64-byte seed directly.
60
- // seed[0..32] → lattice key matrix expansion (K-PKE keygen)
61
- // seed[32..64] → implicit rejection parameter z (stored in secretKey)
62
- const mlKemKeypair = deriveMlKemKeypairFromSeed(seed64);
63
- const mlDsaSeed = hash(concatUint8Arrays(seed64, new TextEncoder().encode(MAJIK_SIGNATURE_SEED))); // 32 bytes, deterministic, domain-separated
64
66
  const mlDsaKeypair = ml_dsa87.keygen(mlDsaSeed);
65
67
  return {
66
68
  publicKey,
@@ -77,6 +79,14 @@ export class EncryptionEngine {
77
79
  catch (err) {
78
80
  throw new CryptoError("Failed to derive identity from mnemonic", err);
79
81
  }
82
+ finally {
83
+ secureFill.call(seed64, 0);
84
+ secureFill.call(seed32, 0);
85
+ secureFill.call(mlDsaSeedInput, 0);
86
+ secureFill.call(mlDsaSeed, 0);
87
+ if (seed instanceof Uint8Array)
88
+ secureFill.call(seed, 0);
89
+ }
80
90
  }
81
91
  /* ================================
82
92
  * Fingerprinting
@@ -123,3 +133,5 @@ export class CryptoError extends Error {
123
133
  this.cause = cause;
124
134
  }
125
135
  }
136
+ Object.freeze(EncryptionEngine);
137
+ Object.freeze(EncryptionEngine.prototype);
@@ -73,3 +73,5 @@ export class MajikKeyValidator {
73
73
  this.assert(typeof value === "string" && value.trim().length > 0, `${field} must be a non-empty string`);
74
74
  }
75
75
  }
76
+ Object.freeze(MajikKeyValidator);
77
+ Object.freeze(MajikKeyValidator.prototype);
@@ -26,13 +26,13 @@ export interface MajikKeyIdentity {
26
26
  /** Account identifier. Equal to `fingerprint` for accounts created by this library. */
27
27
  id: string;
28
28
  /** X25519 public key — native `CryptoKey` where WebCrypto supports it, otherwise a raw-bytes wrapper. */
29
- publicKey: CryptoKey | {
29
+ publicKey: {
30
30
  raw: Uint8Array;
31
31
  };
32
32
  /** SHA-256 fingerprint of `publicKey`. */
33
33
  fingerprint: MajikKeyFingerprint;
34
34
  /** X25519 private key, decrypted into memory. ⚠️ Live key material — do not log or serialize directly. */
35
- privateKey: CryptoKey | {
35
+ privateKey: {
36
36
  raw: Uint8Array;
37
37
  };
38
38
  /** AES-256-GCM-encrypted X25519 private key (IV + ciphertext), as stored at rest. */
@@ -106,7 +106,7 @@ export interface SerializedIdentity {
106
106
  */
107
107
  export interface MajikKeyConstructorOptions {
108
108
  id: string;
109
- publicKey: CryptoKey | {
109
+ publicKey: {
110
110
  raw: Uint8Array;
111
111
  };
112
112
  publicKeyBase64: MajikKeyAddress;
@@ -126,11 +126,9 @@ export interface MajikKeyConstructorOptions {
126
126
  encryptedMlKemSecretKey?: ArrayBuffer;
127
127
  encryptedMlKemSecretKeyBase64?: string;
128
128
  /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
129
- privateKey?: CryptoKey | {
129
+ privateKey?: {
130
130
  raw: Uint8Array;
131
131
  };
132
- /** Present only when constructing an already-unlocked instance. ⚠️ Live key material. */
133
- privateKeyBase64?: string;
134
132
  edPublicKey?: Uint8Array;
135
133
  encryptedEdSecretKey?: ArrayBuffer;
136
134
  encryptedEdSecretKeyBase64?: string;
@@ -192,7 +190,6 @@ export declare class MajikKey {
192
190
  private _encryptedMlKemSecretKey?;
193
191
  private _encryptedMlKemSecretKeyBase64?;
194
192
  private _privateKey?;
195
- private _privateKeyBase64?;
196
193
  private _edPublicKey?;
197
194
  private _edSecretKey?;
198
195
  private _encryptedEdSecretKey?;
package/dist/majik-key.js CHANGED
@@ -8,12 +8,13 @@ import { aesGcmDecrypt, aesGcmEncrypt, deriveKeyFromPassphraseArgon2, deriveKeyF
8
8
  import { EncryptionEngine } from "./core/crypto/encryption-engine";
9
9
  import { MajikContact } from "@majikah/majik-contact";
10
10
  import { arrayBufferToBase64, arrayToBase64, base64ToArrayBuffer, concatUint8Arrays, utf8ToBase64, base64ToUtf8, seedStringToArray, seedArrayToString, base64ToUint8Array, } from "./core/utils";
11
- import { KDF_VERSION, KEY_ALGO, MAJIK_MNEMONIC_SALT, } from "./core/crypto/constants";
11
+ import { KDF_VERSION, MAJIK_MNEMONIC_SALT, } from "./core/crypto/constants";
12
12
  import { MajikKeyValidator } from "./core/validator";
13
13
  import { MajikKeyError } from "./core/error";
14
14
  import { MajikMessageIdentity } from "./core/database/system/identity";
15
15
  import { WORDLISTS } from "./core/crypto/wordlist";
16
16
  import { deriveBitcoinKeypairFromSeed, signWithBitcoinMaterial, toBitcoinAddress, toWIF, deriveSolanaKeypairFromEdSecretKey, signWithSolanaMaterial, solanaAddressFromPublicKey, solanaMaterialFromEd25519SecretKey, toSolanaAddress, toSolanaKeyPairSigner, } from "./core/web3";
17
+ const secureFill = Uint8Array.prototype.fill;
17
18
  const SALT_SIZE = 32;
18
19
  /**
19
20
  * MajikKey
@@ -56,7 +57,6 @@ export class MajikKey {
56
57
  _encryptedMlKemSecretKey;
57
58
  _encryptedMlKemSecretKeyBase64;
58
59
  _privateKey;
59
- _privateKeyBase64;
60
60
  _edPublicKey;
61
61
  _edSecretKey;
62
62
  _encryptedEdSecretKey;
@@ -102,7 +102,6 @@ export class MajikKey {
102
102
  this._encryptedMlKemSecretKey = options.encryptedMlKemSecretKey;
103
103
  this._encryptedMlKemSecretKeyBase64 = options.encryptedMlKemSecretKeyBase64;
104
104
  this._privateKey = options.privateKey;
105
- this._privateKeyBase64 = options.privateKeyBase64;
106
105
  this._edPublicKey = options.edPublicKey;
107
106
  this._encryptedEdSecretKey = options.encryptedEdSecretKey;
108
107
  this._encryptedEdSecretKeyBase64 = options.encryptedEdSecretKeyBase64;
@@ -284,7 +283,6 @@ export class MajikKey {
284
283
  encryptedMlKemSecretKey: identity.encryptedMlKemSecretKey,
285
284
  encryptedMlKemSecretKeyBase64: arrayBufferToBase64(identity.encryptedMlKemSecretKey),
286
285
  privateKey: identity.privateKey,
287
- privateKeyBase64,
288
286
  edPublicKey: identity.edPublicKey,
289
287
  encryptedEdSecretKey: identity.encryptedEdSecretKey,
290
288
  encryptedEdSecretKeyBase64: arrayBufferToBase64(identity.encryptedEdSecretKey),
@@ -396,11 +394,11 @@ export class MajikKey {
396
394
  if (!this._edSecretKey ||
397
395
  !this._mlDsaSecretKey ||
398
396
  !this._mlKemSecretKey ||
399
- !this._privateKeyBase64)
397
+ !this._privateKey)
400
398
  throw new MajikKeyError("MajikKey is missing secret keys — re-import via importFromMnemonicBackup() first.");
401
399
  return {
402
400
  ...this.toJSON(),
403
- privateKeyBase64: this._privateKeyBase64,
401
+ privateKeyBase64: arrayToBase64(this._privateKey.raw),
404
402
  mlKemSecretKeyBase64: arrayToBase64(this._mlKemSecretKey),
405
403
  edSecretKeyBase64: arrayToBase64(this._edSecretKey),
406
404
  mlDsaSecretKeyBase64: arrayToBase64(this._mlDsaSecretKey),
@@ -448,7 +446,6 @@ export class MajikKey {
448
446
  publicKey: { raw: base64ToUint8Array(parsed.publicKey) },
449
447
  publicKeyBase64: parsed.publicKey,
450
448
  privateKey: { raw: privateKeyBytes },
451
- privateKeyBase64: parsed.privateKeyBase64,
452
449
  encryptedPrivateKey: new ArrayBuffer(0),
453
450
  encryptedPrivateKeyBase64: parsed.encryptedPrivateKey,
454
451
  salt: parsed.salt,
@@ -510,11 +507,13 @@ export class MajikKey {
510
507
  return this;
511
508
  }
512
509
  async updatePassphrase(currentPassphrase, newPassphrase) {
510
+ if (this.isLocked)
511
+ throw new MajikKeyError("MajikKey must be unlocked to update passphrase");
512
+ MajikKeyValidator.validatePassphrase(currentPassphrase, "Current passphrase");
513
+ MajikKeyValidator.validatePassphrase(newPassphrase, "New passphrase");
514
+ const salt = new Uint8Array(base64ToArrayBuffer(this._salt));
515
+ const privateKeyBuffer = await MajikKey._decryptPrivateKey(this._encryptedPrivateKey, currentPassphrase, salt, this._kdfVersion);
513
516
  try {
514
- MajikKeyValidator.validatePassphrase(currentPassphrase, "Current passphrase");
515
- MajikKeyValidator.validatePassphrase(newPassphrase, "New passphrase");
516
- const salt = new Uint8Array(base64ToArrayBuffer(this._salt));
517
- const privateKeyBuffer = await MajikKey._decryptPrivateKey(this._encryptedPrivateKey, currentPassphrase, salt, this._kdfVersion);
518
517
  let mlKemSecretKeyBytes;
519
518
  if (this._encryptedMlKemSecretKey) {
520
519
  mlKemSecretKeyBytes = await MajikKey._decryptMlKemSecretKey(this._encryptedMlKemSecretKey, currentPassphrase, salt);
@@ -529,6 +528,8 @@ export class MajikKey {
529
528
  const encMlKem = await MajikKey._encryptMlKemSecretKey(mlKemSecretKeyBytes, newPassphrase, newSalt);
530
529
  this._encryptedMlKemSecretKey = encMlKem;
531
530
  this._encryptedMlKemSecretKeyBase64 = arrayBufferToBase64(encMlKem);
531
+ if (this._mlKemSecretKey)
532
+ secureFill.call(this._mlKemSecretKey, 0);
532
533
  this._mlKemSecretKey = mlKemSecretKeyBytes;
533
534
  }
534
535
  if (this._encryptedEdSecretKey) {
@@ -536,6 +537,8 @@ export class MajikKey {
536
537
  const encEd = await MajikKey._encryptSigningKey(edSecretKeyBytes, newPassphrase, newSalt);
537
538
  this._encryptedEdSecretKey = encEd;
538
539
  this._encryptedEdSecretKeyBase64 = arrayBufferToBase64(encEd);
540
+ if (this._edSecretKey)
541
+ secureFill.call(this._edSecretKey, 0);
539
542
  this._edSecretKey = edSecretKeyBytes;
540
543
  }
541
544
  if (this._encryptedMlDsaSecretKey) {
@@ -543,6 +546,8 @@ export class MajikKey {
543
546
  const encDsa = await MajikKey._encryptSigningKey(mlDsaSecretKeyBytes, newPassphrase, newSalt);
544
547
  this._encryptedMlDsaSecretKey = encDsa;
545
548
  this._encryptedMlDsaSecretKeyBase64 = arrayBufferToBase64(encDsa);
549
+ if (this._mlDsaSecretKey)
550
+ secureFill.call(this._mlDsaSecretKey, 0);
546
551
  this._mlDsaSecretKey = mlDsaSecretKeyBytes;
547
552
  }
548
553
  if (this._encryptedBtcSecretKey) {
@@ -550,6 +555,8 @@ export class MajikKey {
550
555
  const encBtc = await MajikKey._encryptSigningKey(btcSecretKeyBytes, newPassphrase, newSalt);
551
556
  this._encryptedBtcSecretKey = encBtc;
552
557
  this._encryptedBtcSecretKeyBase64 = arrayBufferToBase64(encBtc);
558
+ if (this._btcSecretKey)
559
+ secureFill.call(this._btcSecretKey, 0);
553
560
  this._btcSecretKey = btcSecretKeyBytes;
554
561
  }
555
562
  return this;
@@ -559,18 +566,24 @@ export class MajikKey {
559
566
  throw err;
560
567
  throw new MajikKeyError("Failed to update passphrase", err);
561
568
  }
569
+ finally {
570
+ // Clear the temporary unencrypted buffer
571
+ secureFill.call(new Uint8Array(privateKeyBuffer), 0);
572
+ secureFill.call(salt, 0);
573
+ }
562
574
  }
563
575
  /**
564
576
  * Migrate KDF from PBKDF2 to Argon2id without changing passphrase.
565
577
  * NOTE: Does not add ML-KEM keys — use importFromMnemonicBackup() for full upgrade.
566
578
  */
567
579
  async migrate(passphrase) {
580
+ MajikKeyValidator.validatePassphrase(passphrase);
581
+ if (this._kdfVersion === KDF_VERSION.ARGON2ID)
582
+ return this;
583
+ const salt = new Uint8Array(base64ToArrayBuffer(this._salt));
584
+ let privateKeyBuffer;
568
585
  try {
569
- MajikKeyValidator.validatePassphrase(passphrase);
570
- if (this._kdfVersion === KDF_VERSION.ARGON2ID)
571
- return this;
572
- const salt = new Uint8Array(base64ToArrayBuffer(this._salt));
573
- const privateKeyBuffer = await MajikKey._decryptPrivateKey(this._encryptedPrivateKey, passphrase, salt, KDF_VERSION.PBKDF2);
586
+ privateKeyBuffer = await MajikKey._decryptPrivateKey(this._encryptedPrivateKey, passphrase, salt, KDF_VERSION.PBKDF2);
574
587
  const newSalt = generateRandomBytes(SALT_SIZE);
575
588
  const { blob } = await MajikKey._encryptPrivateKey(privateKeyBuffer, passphrase, newSalt);
576
589
  this._encryptedPrivateKey = blob;
@@ -584,12 +597,34 @@ export class MajikKey {
584
597
  throw err;
585
598
  throw new MajikKeyError("Failed to migrate MajikKey to Argon2id", err);
586
599
  }
600
+ finally {
601
+ if (privateKeyBuffer)
602
+ secureFill.call(new Uint8Array(privateKeyBuffer), 0);
603
+ secureFill.call(salt, 0);
604
+ }
587
605
  }
588
606
  // ── LOCK / UNLOCK ────────────────────────────────────────────────────────────
589
607
  // required
590
608
  lock() {
609
+ // 1. Zeroize raw bytes of all active keys
610
+ // Apply the secure fill using .call(targetArray, value)
611
+ if (this._privateKey &&
612
+ "raw" in this._privateKey &&
613
+ this._privateKey.raw instanceof Uint8Array) {
614
+ secureFill.call(this._privateKey.raw, 0);
615
+ }
616
+ if (this._mlKemSecretKey)
617
+ secureFill.call(this._mlKemSecretKey, 0);
618
+ if (this._edSecretKey)
619
+ secureFill.call(this._edSecretKey, 0);
620
+ if (this._mlDsaSecretKey)
621
+ secureFill.call(this._mlDsaSecretKey, 0);
622
+ if (this._btcSecretKey)
623
+ secureFill.call(this._btcSecretKey, 0);
624
+ if (this._solanaKeypairMaterial) {
625
+ secureFill.call(this._solanaKeypairMaterial.secretKey, 0);
626
+ }
591
627
  this._privateKey = undefined;
592
- this._privateKeyBase64 = undefined;
593
628
  this._mlKemSecretKey = undefined;
594
629
  this._edSecretKey = undefined;
595
630
  this._mlDsaSecretKey = undefined;
@@ -604,18 +639,11 @@ export class MajikKey {
604
639
  MajikKeyValidator.validatePassphrase(passphrase);
605
640
  const salt = new Uint8Array(base64ToArrayBuffer(this._salt));
606
641
  const privateKeyBuffer = await MajikKey._decryptPrivateKey(this._encryptedPrivateKey, passphrase, salt, this._kdfVersion);
607
- let privateKey;
608
- try {
609
- privateKey = await crypto.subtle.importKey("raw", privateKeyBuffer, KEY_ALGO, true, ["sign"]);
610
- }
611
- catch {
612
- privateKey = {
613
- type: "private",
614
- raw: new Uint8Array(privateKeyBuffer),
615
- };
616
- }
642
+ const privateKey = {
643
+ type: "private",
644
+ raw: new Uint8Array(privateKeyBuffer),
645
+ };
617
646
  this._privateKey = privateKey;
618
- this._privateKeyBase64 = arrayBufferToBase64(privateKeyBuffer);
619
647
  if (this._encryptedMlKemSecretKey) {
620
648
  this._mlKemSecretKey = await MajikKey._decryptMlKemSecretKey(this._encryptedMlKemSecretKey, passphrase, salt);
621
649
  }
@@ -654,7 +682,7 @@ export class MajikKey {
654
682
  getPrivateKeyBase64() {
655
683
  if (this.isLocked)
656
684
  throw new MajikKeyError("MajikKey is locked. Call unlock() first.");
657
- return this._privateKeyBase64;
685
+ return arrayToBase64(this._privateKey.raw);
658
686
  }
659
687
  getMlKemSecretKey() {
660
688
  if (this.isLocked)
@@ -849,7 +877,6 @@ export class MajikKey {
849
877
  encryptedMlKemSecretKey: identity.encryptedMlKemSecretKey,
850
878
  encryptedMlKemSecretKeyBase64: arrayBufferToBase64(identity.encryptedMlKemSecretKey),
851
879
  privateKey: identity.privateKey,
852
- privateKeyBase64,
853
880
  edPublicKey: identity.edPublicKey,
854
881
  encryptedEdSecretKey: identity.encryptedEdSecretKey,
855
882
  encryptedEdSecretKeyBase64: arrayBufferToBase64(identity.encryptedEdSecretKey),
@@ -886,19 +913,8 @@ export class MajikKey {
886
913
  static async _deriveAndEncryptFromMnemonic(mnemonic, passphrase, options) {
887
914
  const deriveBitcoin = options?.deriveBitcoin ?? true;
888
915
  const encIdentity = await EncryptionEngine.deriveIdentityFromMnemonic(mnemonic);
889
- let exportedXPrivate;
890
- try {
891
- exportedXPrivate = await crypto.subtle.exportKey("raw", encIdentity.privateKey);
892
- }
893
- catch {
894
- const anyPriv = encIdentity.privateKey;
895
- if (anyPriv?.raw instanceof Uint8Array) {
896
- exportedXPrivate = anyPriv.raw.buffer.slice(anyPriv.raw.byteOffset, anyPriv.raw.byteOffset + anyPriv.raw.byteLength);
897
- }
898
- else {
899
- throw new MajikKeyError("Cannot export private key: unsupported format");
900
- }
901
- }
916
+ const anyPriv = encIdentity.privateKey;
917
+ const exportedXPrivate = anyPriv.raw.buffer.slice(anyPriv.raw.byteOffset, anyPriv.raw.byteOffset + anyPriv.raw.byteLength);
902
918
  // Single salt — one Argon2id derivation unlocks every key below
903
919
  const salt = generateRandomBytes(SALT_SIZE);
904
920
  const { blob: encryptedPrivateKey } = await MajikKey._encryptPrivateKey(exportedXPrivate, passphrase, salt);
@@ -997,13 +1013,18 @@ export class MajikKey {
997
1013
  }
998
1014
  static async _decryptSigningKey(buffer, passphrase, salt) {
999
1015
  const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
1000
- const full = new Uint8Array(buffer);
1001
- const iv = full.slice(0, IV_LENGTH);
1002
- const ciphertext = full.slice(IV_LENGTH);
1003
- const plain = aesGcmDecrypt(keyBytes, iv, ciphertext);
1004
- if (!plain)
1005
- throw new MajikKeyError("Failed to decrypt signing key");
1006
- return plain;
1016
+ try {
1017
+ const full = new Uint8Array(buffer);
1018
+ const iv = full.slice(0, IV_LENGTH);
1019
+ const ciphertext = full.slice(IV_LENGTH);
1020
+ const plain = aesGcmDecrypt(keyBytes, iv, ciphertext);
1021
+ if (!plain)
1022
+ throw new MajikKeyError("Failed to decrypt signing key");
1023
+ return plain;
1024
+ }
1025
+ finally {
1026
+ secureFill.call(keyBytes, 0);
1027
+ }
1007
1028
  }
1008
1029
  // ── PRIVATE: Backup ──────────────────────────────────────────────────────────
1009
1030
  static async _verifyBackupDecryption(ivBase64, ciphertextBase64, mnemonic, backupKdfVersion) {
@@ -1029,26 +1050,10 @@ export class MajikKey {
1029
1050
  static async _exportMnemonicBackup(identity, mnemonic) {
1030
1051
  if (!identity?.privateKey)
1031
1052
  throw new MajikKeyError("Identity must have privateKey to export backup");
1032
- let privRawBuf;
1033
- let pubRawBuf;
1034
- try {
1035
- privRawBuf = await crypto.subtle.exportKey("raw", identity.privateKey);
1036
- pubRawBuf = await crypto.subtle.exportKey("raw", identity.publicKey);
1037
- }
1038
- catch {
1039
- const anyPriv = identity.privateKey;
1040
- const anyPub = identity.publicKey;
1041
- if (anyPriv?.raw instanceof Uint8Array) {
1042
- privRawBuf = anyPriv.raw.buffer.slice(anyPriv.raw.byteOffset, anyPriv.raw.byteOffset + anyPriv.raw.byteLength);
1043
- }
1044
- else
1045
- throw new MajikKeyError("Cannot export private key");
1046
- if (anyPub?.raw instanceof Uint8Array) {
1047
- pubRawBuf = anyPub.raw.buffer.slice(anyPub.raw.byteOffset, anyPub.raw.byteOffset + anyPub.raw.byteLength);
1048
- }
1049
- else
1050
- throw new MajikKeyError("Cannot export public key");
1051
- }
1053
+ const anyPriv = identity.privateKey;
1054
+ const anyPub = identity.publicKey;
1055
+ const privRawBuf = anyPriv.raw.buffer.slice(anyPriv.raw.byteOffset, anyPriv.raw.byteOffset + anyPriv.raw.byteLength);
1056
+ const pubRawBuf = anyPub.raw.buffer.slice(anyPub.raw.byteOffset, anyPub.raw.byteOffset + anyPub.raw.byteLength);
1052
1057
  const mnemonicSalt = new TextEncoder().encode(MAJIK_MNEMONIC_SALT);
1053
1058
  const keyBytes = await deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
1054
1059
  const iv = generateRandomBytes(IV_LENGTH);
@@ -1210,3 +1215,7 @@ export class MajikKey {
1210
1215
  return solanaAddressFromPublicKey(this.getSolanaKeypairMaterial(options).publicKey);
1211
1216
  }
1212
1217
  }
1218
+ // Freeze static methods (e.g., MajikKey.create, MajikKey.fromJSON)
1219
+ Object.freeze(MajikKey);
1220
+ // Freeze instance methods (e.g., this.lock, this.unlock)
1221
+ Object.freeze(MajikKey.prototype);
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.3.1",
5
+ "version": "0.3.3",
6
6
  "license": "Apache-2.0",
7
7
  "author": "Zelijah",
8
8
  "main": "./dist/index.js",
@@ -48,6 +48,7 @@
48
48
  "package": "npm run build && npm version patch && git push && git push --tags",
49
49
  "test": "vitest run",
50
50
  "test:watch": "vitest",
51
+ "test:compat": "npx vitest test/majik-key-cross-compat.test.ts",
51
52
  "test:web3:bitcoin": "npx vitest test/majik-key-bitcoin.test.ts",
52
53
  "test:web3:solana": "npx vitest test/majik-key-solana.test.ts",
53
54
  "test:web3": "npx vitest test/majik-key-bitcoin.test.ts test/majik-key-solana.test.ts",
@@ -74,9 +75,9 @@
74
75
  "@scure/btc-signer": "^2.2.0",
75
76
  "@solana/kit": "^7.0.0",
76
77
  "@types/ed2curve": "^0.2.4",
77
- "@types/node": "^26.1.0",
78
- "typescript": "^6.0.3",
79
- "vitest": "^4.1.9"
78
+ "@types/node": "^26.1.1",
79
+ "typescript": "^7.0.2",
80
+ "vitest": "^4.1.10"
80
81
  },
81
82
  "peerDependencies": {
82
83
  "@scure/btc-signer": "^2.2.0",