Encrypt secrets with libsodium, password required to send
All checks were successful
check / check (push) Successful in 14s

vault.js: Argon2id key derivation + XSalsa20-Poly1305 encryption
via libsodium-wrappers-sumo. No raw crypto primitives.

Wallet creation now requires a password. The mnemonic or private
key is encrypted before storage — only the ciphertext blob
(salt, nonce, ciphertext) is persisted. The plaintext secret
is never stored.

Sending requires the password to decrypt the secret, derive
the signing key, and construct the transaction. Wrong password
is caught and reported.
This commit is contained in:
2026-02-25 18:23:09 +07:00
parent bfecddf2f7
commit f2e22cadf2
4 changed files with 183 additions and 14 deletions

62
src/shared/vault.js Normal file
View File

@@ -0,0 +1,62 @@
// Vault: password-based encryption of secrets using libsodium.
// Uses Argon2id for key derivation and XSalsa20-Poly1305 for encryption.
// All crypto operations are delegated to libsodium — no raw primitives.
const sodium = require("libsodium-wrappers-sumo");
let ready = false;
async function ensureReady() {
if (!ready) {
await sodium.ready;
ready = true;
}
}
// Returns { salt, nonce, ciphertext } (all base64-encoded strings).
async function encryptWithPassword(plaintext, password) {
await ensureReady();
const salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
const key = sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
password,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_ARGON2ID13,
);
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const ciphertext = sodium.crypto_secretbox_easy(
sodium.from_string(plaintext),
nonce,
key,
);
return {
salt: sodium.to_base64(salt),
nonce: sodium.to_base64(nonce),
ciphertext: sodium.to_base64(ciphertext),
};
}
// Returns the plaintext string, or throws on wrong password.
async function decryptWithPassword(encrypted, password) {
await ensureReady();
const salt = sodium.from_base64(encrypted.salt);
const nonce = sodium.from_base64(encrypted.nonce);
const ciphertext = sodium.from_base64(encrypted.ciphertext);
const key = sodium.crypto_pwhash(
sodium.crypto_secretbox_KEYBYTES,
password,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_ARGON2ID13,
);
const plaintext = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
if (!plaintext) {
throw new Error("Decryption failed — wrong password.");
}
return sodium.to_string(plaintext);
}
module.exports = { encryptWithPassword, decryptWithPassword };