@majikah/majik-key 0.1.8 → 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/jedlsf/majik-key](https://github.com/jedlsf/majik-key)
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 argon2id(pw, salt, ARGON2_PARAMS.PASSPHRASE);
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 argon2id(m, salt, ARGON2_PARAMS.MNEMONIC);
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
@@ -542,7 +542,7 @@ export class MajikKey {
542
542
  }
543
543
  // ── PRIVATE: Encryption/Decryption ───────────────────────────────────────────
544
544
  static async _encryptPrivateKey(buffer, passphrase, salt) {
545
- const keyBytes = deriveKeyFromPassphraseArgon2(passphrase, salt);
545
+ const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
546
546
  const iv = generateRandomBytes(IV_LENGTH);
547
547
  const ciphertext = aesGcmEncrypt(keyBytes, iv, new Uint8Array(buffer));
548
548
  return {
@@ -556,14 +556,14 @@ export class MajikKey {
556
556
  * computation → two independently encrypted blobs.
557
557
  */
558
558
  static async _encryptMlKemSecretKey(mlKemSecretKey, passphrase, salt) {
559
- const keyBytes = deriveKeyFromPassphraseArgon2(passphrase, salt);
559
+ const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
560
560
  const iv = generateRandomBytes(IV_LENGTH); // different IV from X25519 blob
561
561
  const ciphertext = aesGcmEncrypt(keyBytes, iv, mlKemSecretKey);
562
562
  return concatUint8Arrays(iv, ciphertext).buffer;
563
563
  }
564
564
  static async _decryptPrivateKey(buffer, passphrase, salt, kdfVersion = KDF_VERSION.PBKDF2) {
565
565
  const keyBytes = kdfVersion === KDF_VERSION.ARGON2ID
566
- ? deriveKeyFromPassphraseArgon2(passphrase, salt)
566
+ ? await deriveKeyFromPassphraseArgon2(passphrase, salt)
567
567
  : deriveKeyFromPassphrase(passphrase, salt);
568
568
  const full = new Uint8Array(buffer);
569
569
  const iv = full.slice(0, IV_LENGTH);
@@ -575,7 +575,7 @@ export class MajikKey {
575
575
  }
576
576
  static async _decryptMlKemSecretKey(buffer, passphrase, salt) {
577
577
  // ML-KEM keys are only ever written by Argon2id (v2) code
578
- const keyBytes = deriveKeyFromPassphraseArgon2(passphrase, salt);
578
+ const keyBytes = await deriveKeyFromPassphraseArgon2(passphrase, salt);
579
579
  const full = new Uint8Array(buffer);
580
580
  const iv = full.slice(0, IV_LENGTH);
581
581
  const ciphertext = full.slice(IV_LENGTH);
@@ -590,7 +590,7 @@ export class MajikKey {
590
590
  const ciphertext = base64ToArrayBuffer(ciphertextBase64);
591
591
  const mnemonicSalt = new TextEncoder().encode(MAJIK_MNEMONIC_SALT);
592
592
  if (backupKdfVersion === KDF_VERSION.ARGON2ID) {
593
- const keyBytes = deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
593
+ const keyBytes = await deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
594
594
  const plain = aesGcmDecrypt(keyBytes, iv, new Uint8Array(ciphertext));
595
595
  if (!plain)
596
596
  throw new MajikKeyError("Failed to decrypt backup — invalid mnemonic or corrupted data");
@@ -629,7 +629,7 @@ export class MajikKey {
629
629
  throw new MajikKeyError("Cannot export public key");
630
630
  }
631
631
  const mnemonicSalt = new TextEncoder().encode(MAJIK_MNEMONIC_SALT);
632
- const keyBytes = deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
632
+ const keyBytes = await deriveKeyFromMnemonicArgon2(mnemonic, mnemonicSalt);
633
633
  const iv = generateRandomBytes(IV_LENGTH);
634
634
  const ciphertext = aesGcmEncrypt(keyBytes, iv, new Uint8Array(privRawBuf));
635
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.8",
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"