@majikah/majik-key 0.1.7 → 0.1.9
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
|
@@ -926,7 +926,7 @@ Made with 💙 by [@thezelijah](https://github.com/jedlsf)
|
|
|
926
926
|
|
|
927
927
|
- **Developer**: Josef Elijah Fabian
|
|
928
928
|
- **GitHub**: [https://github.com/jedlsf](https://github.com/jedlsf)
|
|
929
|
-
- **Project Repository**: [https://github.com/
|
|
929
|
+
- **Project Repository**: [https://github.com/Majikah/majik-key](https://github.com/Majikah/majik-key)
|
|
930
930
|
|
|
931
931
|
---
|
|
932
932
|
|
|
@@ -17,27 +17,22 @@ export declare function aesGcmEncrypt(keyBytes: Uint8Array, iv: Uint8Array, plai
|
|
|
17
17
|
export declare function aesGcmDecrypt(keyBytes: Uint8Array, iv: Uint8Array, ciphertext: Uint8Array): Uint8Array | null;
|
|
18
18
|
/**
|
|
19
19
|
* Derive a 32-byte AES key from a user passphrase using Argon2id.
|
|
20
|
-
*
|
|
21
|
-
* Use this for all NEW account creation and any re-encryption operations.
|
|
22
|
-
* Works in browser, Node.js, Electron, and Chrome Extension environments.
|
|
20
|
+
* WASM-accelerated via hash-wasm when available, falls back to @noble/hashes.
|
|
23
21
|
*
|
|
24
22
|
* @param passphrase - The user's passphrase (plaintext string)
|
|
25
23
|
* @param salt - Per-identity random salt (32 bytes recommended)
|
|
26
24
|
* @returns - 32-byte key suitable for AES-256-GCM
|
|
27
25
|
*/
|
|
28
|
-
export declare function deriveKeyFromPassphraseArgon2(passphrase: string, salt: Uint8Array): Uint8Array
|
|
26
|
+
export declare function deriveKeyFromPassphraseArgon2(passphrase: string, salt: Uint8Array): Promise<Uint8Array>;
|
|
29
27
|
/**
|
|
30
28
|
* Derive a 32-byte AES key from a BIP-39 mnemonic using Argon2id.
|
|
31
|
-
*
|
|
32
|
-
* Used for encrypting and decrypting mnemonic backup exports.
|
|
33
|
-
* Lower memory parameters than the passphrase KDF because the mnemonic
|
|
34
|
-
* itself provides 128-bit entropy — brute-force is infeasible regardless.
|
|
29
|
+
* WASM-accelerated via hash-wasm when available, falls back to @noble/hashes.
|
|
35
30
|
*
|
|
36
31
|
* @param mnemonic - The 12-word BIP-39 mnemonic (plaintext string)
|
|
37
32
|
* @param salt - Domain-separator salt (can be a fixed constant)
|
|
38
33
|
* @returns - 32-byte key suitable for AES-256-GCM
|
|
39
34
|
*/
|
|
40
|
-
export declare function deriveKeyFromMnemonicArgon2(mnemonic: string, salt: Uint8Array): Uint8Array
|
|
35
|
+
export declare function deriveKeyFromMnemonicArgon2(mnemonic: string, salt: Uint8Array): Promise<Uint8Array>;
|
|
41
36
|
/**
|
|
42
37
|
* @deprecated KDF v1. Kept for reading existing accounts created before the
|
|
43
38
|
* Argon2id migration. Do NOT use this for new key derivation or re-encryption.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// crypto-provider.ts from @majikah/majik-key
|
|
1
2
|
import * as ed25519 from "@stablelib/ed25519";
|
|
2
3
|
import ed2curve from "ed2curve";
|
|
3
4
|
import { AES } from "@stablelib/aes";
|
|
@@ -6,7 +7,7 @@ import { deriveKey } from "@stablelib/pbkdf2";
|
|
|
6
7
|
import { hash, SHA256 } from "@stablelib/sha256";
|
|
7
8
|
import * as x25519 from "@stablelib/x25519";
|
|
8
9
|
import { arrayToBase64 } from "../utils";
|
|
9
|
-
import { argon2id } from "@noble/hashes/argon2.js";
|
|
10
|
+
import { argon2id as nobleArgon2id } from "@noble/hashes/argon2.js";
|
|
10
11
|
import { ARGON2_PARAMS } from "./constants";
|
|
11
12
|
import { ml_kem768 } from "@noble/post-quantum/ml-kem.js";
|
|
12
13
|
export const IV_LENGTH = 12;
|
|
@@ -51,35 +52,97 @@ export function aesGcmDecrypt(keyBytes, iv, ciphertext) {
|
|
|
51
52
|
const gcm = new GCM(aes);
|
|
52
53
|
return gcm.open(iv, ciphertext);
|
|
53
54
|
}
|
|
55
|
+
// ─── WASM-first Argon2id with @noble/hashes fallback ─────────────────────────
|
|
56
|
+
/**
|
|
57
|
+
* null = not yet probed
|
|
58
|
+
* true = hash-wasm loaded and verified
|
|
59
|
+
* false = hash-wasm unavailable or broken — use noble fallback
|
|
60
|
+
*/
|
|
61
|
+
let _wasmAvailable = null;
|
|
62
|
+
async function _probeWasm() {
|
|
63
|
+
try {
|
|
64
|
+
const { argon2id } = await import("hash-wasm");
|
|
65
|
+
// Cheap smoke-test: tiny params, just proves the WASM module loads & runs
|
|
66
|
+
await argon2id({
|
|
67
|
+
password: new Uint8Array(4),
|
|
68
|
+
salt: new Uint8Array(8),
|
|
69
|
+
memorySize: 8,
|
|
70
|
+
iterations: 1,
|
|
71
|
+
parallelism: 1,
|
|
72
|
+
hashLength: 4,
|
|
73
|
+
outputType: "binary",
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function _argon2idWasm(input, salt, params) {
|
|
82
|
+
const { argon2id } = await import("hash-wasm");
|
|
83
|
+
return argon2id({
|
|
84
|
+
password: input,
|
|
85
|
+
salt,
|
|
86
|
+
memorySize: params.m, // KB — same unit as noble's m ✓
|
|
87
|
+
iterations: params.t, // hash-wasm calls it iterations, not t
|
|
88
|
+
parallelism: params.p, // hash-wasm calls it parallelism, not p
|
|
89
|
+
hashLength: params.dkLen, // hash-wasm calls it hashLength, not dkLen
|
|
90
|
+
outputType: "binary",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function _argon2idNoble(input, salt, params) {
|
|
94
|
+
return nobleArgon2id(input, salt, params);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Internal: WASM-first argon2id with automatic noble fallback.
|
|
98
|
+
* Probes WASM once per session and caches the result.
|
|
99
|
+
* Falls back silently on any failure — output is always identical.
|
|
100
|
+
*/
|
|
101
|
+
async function _argon2id(input, salt, params) {
|
|
102
|
+
// First call: probe WASM availability
|
|
103
|
+
if (_wasmAvailable === null) {
|
|
104
|
+
_wasmAvailable = await _probeWasm();
|
|
105
|
+
if (!_wasmAvailable) {
|
|
106
|
+
console.warn("[majikah/crypto] hash-wasm unavailable, using @noble/hashes argon2id fallback");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (_wasmAvailable) {
|
|
110
|
+
try {
|
|
111
|
+
return await _argon2idWasm(input, salt, params);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
// WASM loaded but failed at runtime (e.g. OOM, corrupted module)
|
|
115
|
+
// Flip flag so we stop trying for the rest of this session
|
|
116
|
+
_wasmAvailable = false;
|
|
117
|
+
console.warn("[majikah/crypto] hash-wasm runtime failure, falling back to @noble/hashes", err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return _argon2idNoble(input, salt, params);
|
|
121
|
+
}
|
|
54
122
|
// ─── KDF v2: Argon2id (current) ───────────────────────────────────────────────
|
|
55
123
|
/**
|
|
56
124
|
* Derive a 32-byte AES key from a user passphrase using Argon2id.
|
|
57
|
-
*
|
|
58
|
-
* Use this for all NEW account creation and any re-encryption operations.
|
|
59
|
-
* Works in browser, Node.js, Electron, and Chrome Extension environments.
|
|
125
|
+
* WASM-accelerated via hash-wasm when available, falls back to @noble/hashes.
|
|
60
126
|
*
|
|
61
127
|
* @param passphrase - The user's passphrase (plaintext string)
|
|
62
128
|
* @param salt - Per-identity random salt (32 bytes recommended)
|
|
63
129
|
* @returns - 32-byte key suitable for AES-256-GCM
|
|
64
130
|
*/
|
|
65
|
-
export function deriveKeyFromPassphraseArgon2(passphrase, salt) {
|
|
131
|
+
export async function deriveKeyFromPassphraseArgon2(passphrase, salt) {
|
|
66
132
|
const pw = new TextEncoder().encode(passphrase);
|
|
67
|
-
return
|
|
133
|
+
return _argon2id(pw, salt, ARGON2_PARAMS.PASSPHRASE);
|
|
68
134
|
}
|
|
69
135
|
/**
|
|
70
136
|
* Derive a 32-byte AES key from a BIP-39 mnemonic using Argon2id.
|
|
71
|
-
*
|
|
72
|
-
* Used for encrypting and decrypting mnemonic backup exports.
|
|
73
|
-
* Lower memory parameters than the passphrase KDF because the mnemonic
|
|
74
|
-
* itself provides 128-bit entropy — brute-force is infeasible regardless.
|
|
137
|
+
* WASM-accelerated via hash-wasm when available, falls back to @noble/hashes.
|
|
75
138
|
*
|
|
76
139
|
* @param mnemonic - The 12-word BIP-39 mnemonic (plaintext string)
|
|
77
140
|
* @param salt - Domain-separator salt (can be a fixed constant)
|
|
78
141
|
* @returns - 32-byte key suitable for AES-256-GCM
|
|
79
142
|
*/
|
|
80
|
-
export function deriveKeyFromMnemonicArgon2(mnemonic, salt) {
|
|
143
|
+
export async function deriveKeyFromMnemonicArgon2(mnemonic, salt) {
|
|
81
144
|
const m = new TextEncoder().encode(mnemonic);
|
|
82
|
-
return
|
|
145
|
+
return _argon2id(m, salt, ARGON2_PARAMS.MNEMONIC);
|
|
83
146
|
}
|
|
84
147
|
// ─── KDF v1: PBKDF2-SHA256 (legacy — do not use for new operations) ───────────
|
|
85
148
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// encryption-engine.ts from @majikah/majik-key
|
|
1
2
|
import { mnemonicToSeedSync } from "@scure/bip39";
|
|
2
3
|
import * as ed25519 from "@stablelib/ed25519";
|
|
3
4
|
import ed2curve from "ed2curve";
|
|
@@ -11,29 +12,6 @@ export class EncryptionEngine {
|
|
|
11
12
|
/* ================================
|
|
12
13
|
* Identity
|
|
13
14
|
* ================================ */
|
|
14
|
-
// /**
|
|
15
|
-
// * Generates a random long-term identity keypair (X25519 only).
|
|
16
|
-
// * ML-KEM keys are not generated here since random identities
|
|
17
|
-
// * cannot be deterministically recovered from a mnemonic.
|
|
18
|
-
// */
|
|
19
|
-
// static async generateIdentity(): Promise<EncryptionIdentity> {
|
|
20
|
-
// try {
|
|
21
|
-
// const ed = ed25519.generateKeyPair();
|
|
22
|
-
// const skCurve = ed2curve.convertSecretKey(ed.secretKey);
|
|
23
|
-
// const pkCurve = ed2curve.convertPublicKey(ed.publicKey);
|
|
24
|
-
// if (!skCurve || !pkCurve) {
|
|
25
|
-
// throw new CryptoError("Failed to convert Ed25519 keys to Curve25519");
|
|
26
|
-
// }
|
|
27
|
-
// const pkBytes = new Uint8Array(pkCurve as Uint8Array);
|
|
28
|
-
// const skBytes = new Uint8Array(skCurve as Uint8Array);
|
|
29
|
-
// const publicKey = { type: "public", raw: pkBytes } as any;
|
|
30
|
-
// const privateKey = { type: "private", raw: skBytes } as any;
|
|
31
|
-
// const fingerprint = fingerprintFromPublicRaw(pkBytes);
|
|
32
|
-
// return { publicKey, privateKey, fingerprint };
|
|
33
|
-
// } catch (err) {
|
|
34
|
-
// throw new CryptoError("Failed to generate identity", err);
|
|
35
|
-
// }
|
|
36
|
-
// }
|
|
37
15
|
/**
|
|
38
16
|
* Derive a complete identity from a BIP-39 mnemonic.
|
|
39
17
|
*
|
package/dist/majik-key.js
CHANGED
|
@@ -394,9 +394,7 @@ export class MajikKey {
|
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
396
|
toContact() {
|
|
397
|
-
console.log("toContact mlKemPublicKey:", this._mlKemPublicKey, typeof this._mlKemPublicKey);
|
|
398
397
|
const mlKeyBase64 = arrayToBase64(this.mlKemPublicKey);
|
|
399
|
-
console.log("toContact mlKeyBase64:", mlKeyBase64);
|
|
400
398
|
return new MajikContact({
|
|
401
399
|
id: this._id,
|
|
402
400
|
publicKey: this._publicKey,
|
|
@@ -544,7 +542,7 @@ export class MajikKey {
|
|
|
544
542
|
}
|
|
545
543
|
// ── PRIVATE: Encryption/Decryption ───────────────────────────────────────────
|
|
546
544
|
static async _encryptPrivateKey(buffer, passphrase, salt) {
|
|
547
|
-
const keyBytes = deriveKeyFromPassphraseArgon2(passphrase, salt);
|
|
545
|
+
const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
|
|
548
546
|
const iv = generateRandomBytes(IV_LENGTH);
|
|
549
547
|
const ciphertext = aesGcmEncrypt(keyBytes, iv, new Uint8Array(buffer));
|
|
550
548
|
return {
|
|
@@ -558,14 +556,14 @@ export class MajikKey {
|
|
|
558
556
|
* computation → two independently encrypted blobs.
|
|
559
557
|
*/
|
|
560
558
|
static async _encryptMlKemSecretKey(mlKemSecretKey, passphrase, salt) {
|
|
561
|
-
const keyBytes = deriveKeyFromPassphraseArgon2(passphrase, salt);
|
|
559
|
+
const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
|
|
562
560
|
const iv = generateRandomBytes(IV_LENGTH); // different IV from X25519 blob
|
|
563
561
|
const ciphertext = aesGcmEncrypt(keyBytes, iv, mlKemSecretKey);
|
|
564
562
|
return concatUint8Arrays(iv, ciphertext).buffer;
|
|
565
563
|
}
|
|
566
564
|
static async _decryptPrivateKey(buffer, passphrase, salt, kdfVersion = KDF_VERSION.PBKDF2) {
|
|
567
565
|
const keyBytes = kdfVersion === KDF_VERSION.ARGON2ID
|
|
568
|
-
? deriveKeyFromPassphraseArgon2(passphrase, salt)
|
|
566
|
+
? await deriveKeyFromPassphraseArgon2(passphrase, salt)
|
|
569
567
|
: deriveKeyFromPassphrase(passphrase, salt);
|
|
570
568
|
const full = new Uint8Array(buffer);
|
|
571
569
|
const iv = full.slice(0, IV_LENGTH);
|
|
@@ -577,7 +575,7 @@ export class MajikKey {
|
|
|
577
575
|
}
|
|
578
576
|
static async _decryptMlKemSecretKey(buffer, passphrase, salt) {
|
|
579
577
|
// ML-KEM keys are only ever written by Argon2id (v2) code
|
|
580
|
-
const keyBytes = deriveKeyFromPassphraseArgon2(passphrase, salt);
|
|
578
|
+
const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
|
|
581
579
|
const full = new Uint8Array(buffer);
|
|
582
580
|
const iv = full.slice(0, IV_LENGTH);
|
|
583
581
|
const ciphertext = full.slice(IV_LENGTH);
|
|
@@ -592,7 +590,7 @@ export class MajikKey {
|
|
|
592
590
|
const ciphertext = base64ToArrayBuffer(ciphertextBase64);
|
|
593
591
|
const mnemonicSalt = new TextEncoder().encode(MAJIK_MNEMONIC_SALT);
|
|
594
592
|
if (backupKdfVersion === KDF_VERSION.ARGON2ID) {
|
|
595
|
-
const keyBytes = deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
|
|
593
|
+
const keyBytes = await deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
|
|
596
594
|
const plain = aesGcmDecrypt(keyBytes, iv, new Uint8Array(ciphertext));
|
|
597
595
|
if (!plain)
|
|
598
596
|
throw new MajikKeyError("Failed to decrypt backup — invalid mnemonic or corrupted data");
|
|
@@ -631,7 +629,7 @@ export class MajikKey {
|
|
|
631
629
|
throw new MajikKeyError("Cannot export public key");
|
|
632
630
|
}
|
|
633
631
|
const mnemonicSalt = new TextEncoder().encode(MAJIK_MNEMONIC_SALT);
|
|
634
|
-
const keyBytes = deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
|
|
632
|
+
const keyBytes = await deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
|
|
635
633
|
const iv = generateRandomBytes(IV_LENGTH);
|
|
636
634
|
const ciphertext = aesGcmEncrypt(keyBytes, iv, new Uint8Array(privRawBuf));
|
|
637
635
|
return utf8ToBase64(JSON.stringify({
|
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.1.
|
|
5
|
+
"version": "0.1.9",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": "Zelijah",
|
|
8
8
|
"main": "./dist/index.js",
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"@stablelib/sha256": "^2.0.1",
|
|
58
58
|
"@stablelib/x25519": "^2.0.1",
|
|
59
59
|
"@thezelijah/majik-user": "^1.0.3",
|
|
60
|
-
"ed2curve": "^0.3.0"
|
|
60
|
+
"ed2curve": "^0.3.0",
|
|
61
|
+
"hash-wasm": "^4.12.0"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
63
64
|
"@types/ed2curve": "^0.2.4"
|