@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 +45 -38
- package/dist/core/crypto/crypto-provider.js +17 -5
- package/dist/core/crypto/encryption-engine.d.ts +2 -2
- package/dist/core/crypto/encryption-engine.js +26 -14
- package/dist/core/validator.js +2 -0
- package/dist/majik-key.d.ts +4 -7
- package/dist/majik-key.js +79 -70
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Majik Key
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
|
|
3
5
|
[](https://www.thezelijah.world) 
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
[](https://doi.org/10.5281/zenodo.21339132)    [](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
|
|
161
|
-
|
|
|
163
|
+
| Chain | Peer dependency | Needed for |
|
|
164
|
+
| :------ | :------------------ | :--------------------------------------------------------- |
|
|
162
165
|
| Bitcoin | `@scure/btc-signer` | Native SegWit (bech32) address encoding, PSBT construction |
|
|
163
|
-
| Solana
|
|
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
|
|
238
|
-
|
|
|
239
|
-
| `create()`
|
|
240
|
-
| `fromJSON()`
|
|
241
|
-
| `fromMnemonicJSON()`
|
|
242
|
-
| `importFromMnemonicBackup()`
|
|
243
|
-
| `fromDangerousJSON()`
|
|
244
|
-
| `generateMnemonic()`
|
|
245
|
-
| `validateMnemonic()`
|
|
246
|
-
| `deriveStandardBitcoinFromMnemonic()` *(experimental)* | `mnemonic`, `mnemonicLanguage?`
|
|
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
|
|
251
|
-
|
|
|
252
|
-
| `unlock()`
|
|
253
|
-
| `lock()`
|
|
254
|
-
| `verify()`
|
|
255
|
-
| `updatePassphrase()` | `currentPass`, `newPass` | `Promise<this>`
|
|
256
|
-
| `migrate()`
|
|
257
|
-
| `updateLabel()`
|
|
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
|
|
262
|
-
|
|
|
263
|
-
| `toJSON()` / `toString()`
|
|
264
|
-
| `toDangerousJSON()`
|
|
265
|
-
| `toMnemonicJSON()`
|
|
266
|
-
| `exportMnemonicBackup()`
|
|
267
|
-
| `toContact()`
|
|
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
|
|
283
|
-
|
|
|
284
|
-
| `web3` *(getter)*
|
|
285
|
-
| `getBitcoinKeypairMaterial()` | `BitcoinKeypairMaterial`
|
|
286
|
-
| `getBitcoinWIF()`
|
|
287
|
-
| `getSolanaKeypairMaterial()`
|
|
288
|
-
| `getSolanaKeypair()`
|
|
289
|
-
| `getSolanaAddress()`
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
/**
|
|
@@ -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);
|
package/dist/core/validator.js
CHANGED
package/dist/majik-key.d.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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?:
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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.
|
|
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
|
-
|
|
890
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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.
|
|
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.
|
|
78
|
-
"typescript": "^
|
|
79
|
-
"vitest": "^4.1.
|
|
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",
|