Skip to content

Commit 02ad9fd

Browse files
refactor: make hash-wasm optional, default KDF to scrypt
Move hash-wasm from dependencies to an optional peerDependency to avoid bloating the bundle (~2.1 MB on disk, ~29 KB minified for argon2 alone) for users who don't need Argon2id. Changes: - hash-wasm is now an optional peer dependency, not a direct dependency - Default KDF changed from argon2id to scrypt (zero extra deps needed) - Argon2id loaded via dynamic import() only when explicitly requested - Clear error message if argon2id is requested without hash-wasm installed - Users who want argon2id: npm install hash-wasm, then { kdf: 'argon2id' } Bundle impact: zero for users who don't use argon2id. Co-authored-by: Greg Nazario <greg@gnazar.io>
1 parent 0907691 commit 02ad9fd

File tree

4 files changed

+97
-66
lines changed

4 files changed

+97
-66
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T
99
- Add Aptos Keystore: encrypted private key storage standard based on Ethereum's Web3 Secret Storage Definition
1010
- Supports all Aptos key types: Ed25519, Secp256k1, Secp256r1
1111
- AES-256-GCM authenticated encryption (no separate MAC needed)
12-
- Argon2id KDF by default (via `hash-wasm`), with scrypt and PBKDF2-HMAC-SHA256 as alternatives
12+
- scrypt KDF by default; Argon2id available via optional `hash-wasm` peer dependency
13+
- PBKDF2-HMAC-SHA256 also supported as an alternative KDF
1314
- Password-based or key-file-based encryption
1415
- Portable JSON format designed for cross-SDK compatibility (TypeScript, Rust, Python, Go, etc.)
1516
- New exports: `encryptKeystore`, `decryptKeystore`, `AptosKeyStore`, `KeystorePrivateKey`, `KeystoreEncryptOptions`

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@
5858
"@scure/bip32": "^1.4.0",
5959
"@scure/bip39": "^1.3.0",
6060
"eventemitter3": "^5.0.4",
61-
"hash-wasm": "^4.12.0",
6261
"js-base64": "^3.7.7",
6362
"jwt-decode": "^4.0.0",
6463
"poseidon-lite": "^0.2.0"
@@ -84,6 +83,14 @@
8483
"typescript": "^5.9.3",
8584
"vitest": "^4.0.18"
8685
},
86+
"peerDependencies": {
87+
"hash-wasm": "^4.12.0"
88+
},
89+
"peerDependenciesMeta": {
90+
"hash-wasm": {
91+
"optional": true
92+
}
93+
},
8794
"version": "6.2.0",
8895
"pnpm": {
8996
"overrides": {

src/core/crypto/keystore.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* adapted for Aptos key types. Supports encrypting Ed25519, Secp256k1, and Secp256r1
99
* private keys with a password or key file using modern cryptography:
1010
*
11-
* - KDF: Argon2id (default), scrypt, or PBKDF2-HMAC-SHA256
11+
* - KDF: scrypt (default), Argon2id (requires optional `hash-wasm` peer dependency), or PBKDF2-HMAC-SHA256
1212
* - Cipher: AES-256-GCM (authenticated encryption)
1313
*
1414
* AES-256-GCM provides both confidentiality and integrity in a single operation,
@@ -24,7 +24,6 @@ import { sha256 } from "@noble/hashes/sha256";
2424
import { scrypt as nobleScrypt } from "@noble/hashes/scrypt";
2525
import { pbkdf2 as noblePbkdf2 } from "@noble/hashes/pbkdf2";
2626
import { randomBytes, bytesToHex, hexToBytes } from "@noble/hashes/utils";
27-
import { argon2id as hashWasmArgon2id } from "hash-wasm";
2827
import { Ed25519PrivateKey } from "./ed25519";
2928
import { Secp256k1PrivateKey } from "./secp256k1";
3029
import { Secp256r1PrivateKey } from "./secp256r1";
@@ -106,8 +105,8 @@ export type KeystoreKdfParams = Argon2idKdfParams | ScryptKdfParams | Pbkdf2KdfP
106105
* "cipher": "aes-256-gcm",
107106
* "cipherparams": { "iv": "...", "tag": "..." },
108107
* "ciphertext": "...",
109-
* "kdf": "argon2id",
110-
* "kdfparams": { "iterations": 3, "parallelism": 4, "memorySize": 65536, "dklen": 32, "salt": "..." }
108+
* "kdf": "scrypt",
109+
* "kdfparams": { "n": 131072, "r": 8, "p": 1, "dklen": 32, "salt": "..." }
111110
* }
112111
* }
113112
* ```
@@ -145,7 +144,7 @@ export interface AptosKeyStore {
145144
* Options for customizing keystore encryption.
146145
*/
147146
export interface KeystoreEncryptOptions {
148-
/** KDF to use. Defaults to "argon2id". */
147+
/** KDF to use. Defaults to "scrypt". Use "argon2id" for stronger protection (requires `hash-wasm` peer dependency). */
149148
kdf?: KeystoreKdf;
150149
/** Argon2id iterations (time cost). Defaults to 3. */
151150
argon2Iterations?: number;
@@ -181,12 +180,22 @@ function passwordToBytes(password: string | Uint8Array): Uint8Array {
181180
return password;
182181
}
183182

183+
async function loadArgon2id(): Promise<typeof import("hash-wasm").argon2id> {
184+
try {
185+
const mod = await import("hash-wasm");
186+
return mod.argon2id;
187+
} catch {
188+
throw new Error('Argon2id KDF requires the "hash-wasm" package. Install it with: npm install hash-wasm');
189+
}
190+
}
191+
184192
async function deriveKey(password: Uint8Array, kdf: KeystoreKdf, kdfparams: KeystoreKdfParams): Promise<Uint8Array> {
185193
const salt = hexToBytes(kdfparams.salt);
186194

187195
if (kdf === "argon2id") {
196+
const argon2id = await loadArgon2id();
188197
const params = kdfparams as Argon2idKdfParams;
189-
return hashWasmArgon2id({
198+
return argon2id({
190199
password,
191200
salt,
192201
iterations: params.iterations,
@@ -290,7 +299,9 @@ function createPrivateKey(bytes: Uint8Array, keyType: PrivateKeyVariants): Keyst
290299
* Supports all Aptos private key types (Ed25519, Secp256k1, Secp256r1).
291300
* The password can be a string (passphrase) or raw bytes (e.g., contents of a key file).
292301
*
293-
* Uses AES-256-GCM authenticated encryption with Argon2id key derivation by default.
302+
* Uses AES-256-GCM authenticated encryption with scrypt key derivation by default.
303+
* For stronger password protection, use `kdf: "argon2id"` (requires the optional
304+
* `hash-wasm` peer dependency: `npm install hash-wasm`).
294305
*
295306
* @param args.privateKey - The private key to encrypt.
296307
* @param args.password - Password string or key-file bytes used to derive the encryption key.
@@ -301,12 +312,19 @@ function createPrivateKey(bytes: Uint8Array, keyType: PrivateKeyVariants): Keyst
301312
* ```typescript
302313
* import { Ed25519PrivateKey, encryptKeystore } from "@aptos-labs/ts-sdk";
303314
*
315+
* // Default (scrypt) — no extra dependencies
304316
* const privateKey = Ed25519PrivateKey.generate();
305317
* const keystore = await encryptKeystore({
306318
* privateKey,
307319
* password: "my-secure-password",
308320
* });
309-
* console.log(JSON.stringify(keystore, null, 2));
321+
*
322+
* // Argon2id — requires: npm install hash-wasm
323+
* const keystoreArgon2 = await encryptKeystore({
324+
* privateKey,
325+
* password: "my-secure-password",
326+
* options: { kdf: "argon2id" },
327+
* });
310328
* ```
311329
*/
312330
export async function encryptKeystore(args: {
@@ -316,7 +334,7 @@ export async function encryptKeystore(args: {
316334
}): Promise<AptosKeyStore> {
317335
const { privateKey, password, options = {} } = args;
318336
const {
319-
kdf = "argon2id",
337+
kdf = "scrypt",
320338
argon2Iterations = 3,
321339
argon2Parallelism = 4,
322340
argon2MemorySize = 65536,

0 commit comments

Comments
 (0)