Encrypt secrets with libsodium, password required to send
All checks were successful
check / check (push) Successful in 14s
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:
@@ -8,6 +8,7 @@ const {
|
||||
parseEther,
|
||||
} = require("ethers");
|
||||
const { getTopTokenPrices } = require("../shared/tokens");
|
||||
const { encryptWithPassword, decryptWithPassword } = require("../shared/vault");
|
||||
const QRCode = require("qrcode");
|
||||
|
||||
const DEBUG = true;
|
||||
@@ -147,19 +148,20 @@ function formatUsd(amount) {
|
||||
);
|
||||
}
|
||||
|
||||
// Get an ethers Wallet (signer) for the currently selected address
|
||||
function getSignerForCurrentAddress() {
|
||||
// Get an ethers Wallet (signer) for the currently selected address.
|
||||
// Requires the decrypted secret (mnemonic or private key).
|
||||
function getSignerForCurrentAddress(decryptedSecret) {
|
||||
const wallet = state.wallets[state.selectedWallet];
|
||||
const addrIndex = state.selectedAddress;
|
||||
if (wallet.type === "hd") {
|
||||
const node = HDNodeWallet.fromPhrase(
|
||||
wallet.mnemonic,
|
||||
decryptedSecret,
|
||||
"",
|
||||
BIP44_ETH_BASE,
|
||||
);
|
||||
return node.deriveChild(addrIndex);
|
||||
} else {
|
||||
return new Wallet(wallet.privateKey);
|
||||
return new Wallet(decryptedSecret);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +400,7 @@ async function init() {
|
||||
$("add-wallet-phrase-warning").classList.remove("hidden");
|
||||
});
|
||||
|
||||
$("btn-add-wallet-confirm").addEventListener("click", () => {
|
||||
$("btn-add-wallet-confirm").addEventListener("click", async () => {
|
||||
const mnemonic = $("wallet-mnemonic").value.trim();
|
||||
if (!mnemonic) {
|
||||
showError(
|
||||
@@ -417,7 +419,6 @@ async function init() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Validate the mnemonic is real BIP-39
|
||||
if (!Mnemonic.isValidMnemonic(mnemonic)) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
@@ -425,15 +426,33 @@ async function init() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const pw = $("add-wallet-password").value;
|
||||
const pw2 = $("add-wallet-password-confirm").value;
|
||||
if (!pw) {
|
||||
showError("add-wallet-error", "Please choose a password.");
|
||||
return;
|
||||
}
|
||||
if (pw.length < 8) {
|
||||
showError(
|
||||
"add-wallet-error",
|
||||
"Password must be at least 8 characters.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (pw !== pw2) {
|
||||
showError("add-wallet-error", "Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
hideError("add-wallet-error");
|
||||
|
||||
const encrypted = await encryptWithPassword(mnemonic, pw);
|
||||
const { xpub, firstAddress } = hdWalletFromMnemonic(mnemonic);
|
||||
const walletNum = state.wallets.length + 1;
|
||||
addWalletAndGoToMain({
|
||||
type: "hd",
|
||||
name: "Wallet " + walletNum,
|
||||
xpub: xpub,
|
||||
mnemonic: mnemonic,
|
||||
encryptedSecret: encrypted,
|
||||
nextIndex: 1,
|
||||
addresses: [
|
||||
{ address: firstAddress, balance: "0.0000", tokens: [] },
|
||||
@@ -445,7 +464,7 @@ async function init() {
|
||||
$("btn-add-wallet-import-key").addEventListener("click", showImportKeyView);
|
||||
|
||||
// -- Import private key --
|
||||
$("btn-import-key-confirm").addEventListener("click", () => {
|
||||
$("btn-import-key-confirm").addEventListener("click", async () => {
|
||||
const key = $("import-private-key").value.trim();
|
||||
if (!key) {
|
||||
showError("import-key-error", "Please enter your private key.");
|
||||
@@ -458,12 +477,30 @@ async function init() {
|
||||
showError("import-key-error", "Invalid private key.");
|
||||
return;
|
||||
}
|
||||
const pw = $("import-key-password").value;
|
||||
const pw2 = $("import-key-password-confirm").value;
|
||||
if (!pw) {
|
||||
showError("import-key-error", "Please choose a password.");
|
||||
return;
|
||||
}
|
||||
if (pw.length < 8) {
|
||||
showError(
|
||||
"import-key-error",
|
||||
"Password must be at least 8 characters.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (pw !== pw2) {
|
||||
showError("import-key-error", "Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
hideError("import-key-error");
|
||||
const encrypted = await encryptWithPassword(key, pw);
|
||||
const walletNum = state.wallets.length + 1;
|
||||
addWalletAndGoToMain({
|
||||
type: "key",
|
||||
name: "Wallet " + walletNum,
|
||||
privateKey: key,
|
||||
encryptedSecret: encrypted,
|
||||
addresses: [{ address: addr, balance: "0.0000", tokens: [] }],
|
||||
});
|
||||
});
|
||||
@@ -498,6 +535,7 @@ async function init() {
|
||||
$("btn-send").addEventListener("click", () => {
|
||||
$("send-to").value = "";
|
||||
$("send-amount").value = "";
|
||||
$("send-password").value = "";
|
||||
$("send-fee-estimate").classList.add("hidden");
|
||||
$("send-status").classList.add("hidden");
|
||||
showView("send");
|
||||
@@ -558,11 +596,29 @@ async function init() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const statusEl = $("send-status") || $("send-status");
|
||||
statusEl.textContent = "Sending...";
|
||||
const password = $("send-password").value;
|
||||
if (!password) {
|
||||
showError("send-status", "Please enter your password.");
|
||||
$("send-status").classList.remove("hidden");
|
||||
return;
|
||||
}
|
||||
const wallet = state.wallets[state.selectedWallet];
|
||||
let decryptedSecret;
|
||||
const statusEl = $("send-status");
|
||||
statusEl.textContent = "Decrypting...";
|
||||
statusEl.classList.remove("hidden");
|
||||
try {
|
||||
const signer = getSignerForCurrentAddress();
|
||||
decryptedSecret = await decryptWithPassword(
|
||||
wallet.encryptedSecret,
|
||||
password,
|
||||
);
|
||||
} catch (e) {
|
||||
showError("send-status", "Wrong password.");
|
||||
return;
|
||||
}
|
||||
statusEl.textContent = "Sending...";
|
||||
try {
|
||||
const signer = getSignerForCurrentAddress(decryptedSecret);
|
||||
const provider = getProvider();
|
||||
const connectedSigner = signer.connect(provider);
|
||||
const tx = await connectedSigner.sendTransaction({
|
||||
|
||||
Reference in New Issue
Block a user