@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/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
@@ -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.7",
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"