Skip to content

Commit 107a104

Browse files
fix: default to argon2id KDF, fall back to scrypt if hash-wasm unavailable
The intended behavior is to always use Argon2id for password-based encryption. The KDF selection now works as: 1. If no KDF specified: try to load hash-wasm dynamically - If available: use argon2id (strongest option) - If not installed: fall back to scrypt (no extra deps needed) 2. If KDF explicitly specified: use that KDF - argon2id: requires hash-wasm, throws clear error if missing - scrypt/pbkdf2: always available, no extra deps This keeps hash-wasm as an optional peer dependency (zero bundle impact when not installed) while defaulting to the strongest KDF available. Co-authored-by: Greg Nazario <greg@gnazar.io>
1 parent 02ad9fd commit 107a104

File tree

3 files changed

+69
-70
lines changed

3 files changed

+69
-70
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ 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-
- scrypt KDF by default; Argon2id available via optional `hash-wasm` peer dependency
12+
- Argon2id KDF by default when optional `hash-wasm` peer dependency is installed, falls back to scrypt
1313
- PBKDF2-HMAC-SHA256 also supported as an alternative KDF
1414
- Password-based or key-file-based encryption
1515
- Portable JSON format designed for cross-SDK compatibility (TypeScript, Rust, Python, Go, etc.)

src/core/crypto/keystore.ts

Lines changed: 27 additions & 13 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: scrypt (default), Argon2id (requires optional `hash-wasm` peer dependency), or PBKDF2-HMAC-SHA256
11+
* - KDF: Argon2id (default when `hash-wasm` is installed), scrypt (fallback), 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,
@@ -105,8 +105,8 @@ export type KeystoreKdfParams = Argon2idKdfParams | ScryptKdfParams | Pbkdf2KdfP
105105
* "cipher": "aes-256-gcm",
106106
* "cipherparams": { "iv": "...", "tag": "..." },
107107
* "ciphertext": "...",
108-
* "kdf": "scrypt",
109-
* "kdfparams": { "n": 131072, "r": 8, "p": 1, "dklen": 32, "salt": "..." }
108+
* "kdf": "argon2id",
109+
* "kdfparams": { "iterations": 3, "parallelism": 4, "memorySize": 65536, "dklen": 32, "salt": "..." }
110110
* }
111111
* }
112112
* ```
@@ -144,7 +144,7 @@ export interface AptosKeyStore {
144144
* Options for customizing keystore encryption.
145145
*/
146146
export interface KeystoreEncryptOptions {
147-
/** KDF to use. Defaults to "scrypt". Use "argon2id" for stronger protection (requires `hash-wasm` peer dependency). */
147+
/** KDF to use. Defaults to "argon2id" if `hash-wasm` is installed, otherwise "scrypt". */
148148
kdf?: KeystoreKdf;
149149
/** Argon2id iterations (time cost). Defaults to 3. */
150150
argon2Iterations?: number;
@@ -180,13 +180,26 @@ function passwordToBytes(password: string | Uint8Array): Uint8Array {
180180
return password;
181181
}
182182

183-
async function loadArgon2id(): Promise<typeof import("hash-wasm").argon2id> {
183+
async function tryLoadArgon2id(): Promise<typeof import("hash-wasm")["argon2id"] | null> {
184184
try {
185185
const mod = await import("hash-wasm");
186186
return mod.argon2id;
187187
} catch {
188+
return null;
189+
}
190+
}
191+
192+
async function loadArgon2id(): Promise<typeof import("hash-wasm")["argon2id"]> {
193+
const fn = await tryLoadArgon2id();
194+
if (!fn) {
188195
throw new Error('Argon2id KDF requires the "hash-wasm" package. Install it with: npm install hash-wasm');
189196
}
197+
return fn;
198+
}
199+
200+
async function resolveDefaultKdf(): Promise<KeystoreKdf> {
201+
const argon2 = await tryLoadArgon2id();
202+
return argon2 ? "argon2id" : "scrypt";
190203
}
191204

192205
async function deriveKey(password: Uint8Array, kdf: KeystoreKdf, kdfparams: KeystoreKdfParams): Promise<Uint8Array> {
@@ -299,9 +312,9 @@ function createPrivateKey(bytes: Uint8Array, keyType: PrivateKeyVariants): Keyst
299312
* Supports all Aptos private key types (Ed25519, Secp256k1, Secp256r1).
300313
* The password can be a string (passphrase) or raw bytes (e.g., contents of a key file).
301314
*
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`).
315+
* Uses AES-256-GCM authenticated encryption. The default KDF is Argon2id when the
316+
* optional `hash-wasm` peer dependency is installed (`npm install hash-wasm`),
317+
* falling back to scrypt otherwise.
305318
*
306319
* @param args.privateKey - The private key to encrypt.
307320
* @param args.password - Password string or key-file bytes used to derive the encryption key.
@@ -312,18 +325,18 @@ function createPrivateKey(bytes: Uint8Array, keyType: PrivateKeyVariants): Keyst
312325
* ```typescript
313326
* import { Ed25519PrivateKey, encryptKeystore } from "@aptos-labs/ts-sdk";
314327
*
315-
* // Default (scrypt) — no extra dependencies
328+
* // Uses argon2id if hash-wasm is installed, scrypt otherwise
316329
* const privateKey = Ed25519PrivateKey.generate();
317330
* const keystore = await encryptKeystore({
318331
* privateKey,
319332
* password: "my-secure-password",
320333
* });
321334
*
322-
* // Argon2id — requires: npm install hash-wasm
323-
* const keystoreArgon2 = await encryptKeystore({
335+
* // Explicitly request a KDF
336+
* const keystoreScrypt = await encryptKeystore({
324337
* privateKey,
325338
* password: "my-secure-password",
326-
* options: { kdf: "argon2id" },
339+
* options: { kdf: "scrypt" },
327340
* });
328341
* ```
329342
*/
@@ -333,8 +346,9 @@ export async function encryptKeystore(args: {
333346
options?: KeystoreEncryptOptions;
334347
}): Promise<AptosKeyStore> {
335348
const { privateKey, password, options = {} } = args;
349+
const defaultKdf = options.kdf ?? (await resolveDefaultKdf());
336350
const {
337-
kdf = "scrypt",
351+
kdf = defaultKdf,
338352
argon2Iterations = 3,
339353
argon2Parallelism = 4,
340354
argon2MemorySize = 65536,

0 commit comments

Comments
 (0)