@majikah/majik-key 0.3.1 → 0.3.2
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 +2 -1
- package/dist/core/crypto/encryption-engine.js +19 -8
- package/dist/core/validator.js +2 -0
- package/dist/majik-key.js +32 -4
- package/package.json +2 -1
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,11 @@ 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);
|
|
14
15
|
export const IV_LENGTH = 12;
|
|
15
16
|
export function generateRandomBytes(len) {
|
|
16
17
|
const b = new Uint8Array(len);
|
|
17
|
-
|
|
18
|
+
secureGetRandomValues(b);
|
|
18
19
|
return b;
|
|
19
20
|
}
|
|
20
21
|
export function generateEd25519Keypair() {
|
|
@@ -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,15 @@ 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 2: X25519 identity from first 32 bytes (existing path)
|
|
46
|
+
const seed32 = seed64.subarray(0, 32);
|
|
38
47
|
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
48
|
const ed = ed25519.generateKeyPairFromSeed(seed32);
|
|
48
49
|
const skCurve = ed2curve.convertSecretKey(ed.secretKey);
|
|
49
50
|
const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
|
|
@@ -77,6 +78,14 @@ export class EncryptionEngine {
|
|
|
77
78
|
catch (err) {
|
|
78
79
|
throw new CryptoError("Failed to derive identity from mnemonic", err);
|
|
79
80
|
}
|
|
81
|
+
finally {
|
|
82
|
+
// CRITICAL: Zeroize the master seed
|
|
83
|
+
secureFill.call(seed64, 0);
|
|
84
|
+
secureFill.call(seed32, 0);
|
|
85
|
+
if (seed instanceof Uint8Array) {
|
|
86
|
+
secureFill.call(seed, 0);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
80
89
|
}
|
|
81
90
|
/* ================================
|
|
82
91
|
* Fingerprinting
|
|
@@ -123,3 +132,5 @@ export class CryptoError extends Error {
|
|
|
123
132
|
this.cause = cause;
|
|
124
133
|
}
|
|
125
134
|
}
|
|
135
|
+
Object.freeze(EncryptionEngine);
|
|
136
|
+
Object.freeze(EncryptionEngine.prototype);
|
package/dist/core/validator.js
CHANGED
package/dist/majik-key.js
CHANGED
|
@@ -14,6 +14,7 @@ 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
|
|
@@ -510,11 +511,11 @@ export class MajikKey {
|
|
|
510
511
|
return this;
|
|
511
512
|
}
|
|
512
513
|
async updatePassphrase(currentPassphrase, newPassphrase) {
|
|
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);
|
|
513
518
|
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
519
|
let mlKemSecretKeyBytes;
|
|
519
520
|
if (this._encryptedMlKemSecretKey) {
|
|
520
521
|
mlKemSecretKeyBytes = await MajikKey._decryptMlKemSecretKey(this._encryptedMlKemSecretKey, currentPassphrase, salt);
|
|
@@ -559,6 +560,11 @@ export class MajikKey {
|
|
|
559
560
|
throw err;
|
|
560
561
|
throw new MajikKeyError("Failed to update passphrase", err);
|
|
561
562
|
}
|
|
563
|
+
finally {
|
|
564
|
+
// Clear the temporary unencrypted buffer
|
|
565
|
+
secureFill.call(new Uint8Array(privateKeyBuffer), 0);
|
|
566
|
+
secureFill.call(salt, 0);
|
|
567
|
+
}
|
|
562
568
|
}
|
|
563
569
|
/**
|
|
564
570
|
* Migrate KDF from PBKDF2 to Argon2id without changing passphrase.
|
|
@@ -588,6 +594,24 @@ export class MajikKey {
|
|
|
588
594
|
// ── LOCK / UNLOCK ────────────────────────────────────────────────────────────
|
|
589
595
|
// required
|
|
590
596
|
lock() {
|
|
597
|
+
// 1. Zeroize raw bytes of all active keys
|
|
598
|
+
// Apply the secure fill using .call(targetArray, value)
|
|
599
|
+
if (this._privateKey &&
|
|
600
|
+
"raw" in this._privateKey &&
|
|
601
|
+
this._privateKey.raw instanceof Uint8Array) {
|
|
602
|
+
secureFill.call(this._privateKey.raw, 0);
|
|
603
|
+
}
|
|
604
|
+
if (this._mlKemSecretKey)
|
|
605
|
+
secureFill.call(this._mlKemSecretKey, 0);
|
|
606
|
+
if (this._edSecretKey)
|
|
607
|
+
secureFill.call(this._edSecretKey, 0);
|
|
608
|
+
if (this._mlDsaSecretKey)
|
|
609
|
+
secureFill.call(this._mlDsaSecretKey, 0);
|
|
610
|
+
if (this._btcSecretKey)
|
|
611
|
+
secureFill.call(this._btcSecretKey, 0);
|
|
612
|
+
if (this._solanaKeypairMaterial) {
|
|
613
|
+
secureFill.call(this._solanaKeypairMaterial.secretKey, 0);
|
|
614
|
+
}
|
|
591
615
|
this._privateKey = undefined;
|
|
592
616
|
this._privateKeyBase64 = undefined;
|
|
593
617
|
this._mlKemSecretKey = undefined;
|
|
@@ -1210,3 +1234,7 @@ export class MajikKey {
|
|
|
1210
1234
|
return solanaAddressFromPublicKey(this.getSolanaKeypairMaterial(options).publicKey);
|
|
1211
1235
|
}
|
|
1212
1236
|
}
|
|
1237
|
+
// Freeze static methods (e.g., MajikKey.create, MajikKey.fromJSON)
|
|
1238
|
+
Object.freeze(MajikKey);
|
|
1239
|
+
// Freeze instance methods (e.g., this.lock, this.unlock)
|
|
1240
|
+
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.2",
|
|
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",
|