fix: enforce the base58 checksum and reject non-master extended keys (closes #210)
Some checks failed
check / check (push) Has been cancelled
Some checks failed
check / check (push) Has been cancelled
This commit was merged in pull request #232.
This commit is contained in:
@@ -136,7 +136,9 @@
|
||||
<div id="add-wallet-section-xprv" class="hidden">
|
||||
<p class="mb-2">
|
||||
Paste your extended private key (xprv) below. This will
|
||||
import the HD wallet and scan for used addresses.
|
||||
import the HD wallet and scan for used addresses. It
|
||||
must be the master key for the wallet; an account-level
|
||||
or child key is not supported.
|
||||
</p>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
|
||||
@@ -6,6 +6,7 @@ const {
|
||||
addressFromPrivateKey,
|
||||
hdWalletFromXprv,
|
||||
isValidXprv,
|
||||
isMasterExtendedKey,
|
||||
} = require("../../shared/wallet");
|
||||
const { encryptWithPassword } = require("../../shared/vault");
|
||||
const { state, saveState } = require("../../shared/state");
|
||||
@@ -213,14 +214,25 @@ async function importXprvKey(ctx) {
|
||||
return;
|
||||
}
|
||||
if (!isValidXprv(xprv)) {
|
||||
showFlash("Invalid extended private key.");
|
||||
showFlash(
|
||||
"That extended private key is not valid. Please check it and try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!isMasterExtendedKey(xprv)) {
|
||||
showFlash(
|
||||
"That is an account-level or child key, which cannot be imported. " +
|
||||
"Please paste the master extended private key for the wallet.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = hdWalletFromXprv(xprv);
|
||||
} catch (e) {
|
||||
showFlash("Invalid extended private key.");
|
||||
showFlash(
|
||||
"That extended private key is not valid. Please check it and try again.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { xpub, firstAddress } = result;
|
||||
|
||||
@@ -16,8 +16,60 @@ function generateMnemonic() {
|
||||
return m.phrase;
|
||||
}
|
||||
|
||||
// Every extended key (xprv or xpub) entering the app goes through this.
|
||||
//
|
||||
// ethers' HDNodeWallet.fromExtendedKey does NOT verify the base58 checksum
|
||||
// when the decoded payload is the usual 82 bytes, which is exactly the case
|
||||
// the checksum exists to catch: a key with a one-character typo parses into a
|
||||
// *different* wallet instead of being rejected. Re-encoding the parsed node
|
||||
// reproduces a well-formed key byte for byte, checksum included, so comparing
|
||||
// the round trip against the input rejects any altered character. Measured by
|
||||
// the sweep in tests/wallet.test.js over every single-character substitution
|
||||
// of the BIP-32 vector 1 master key: 199 parse without the round-trip
|
||||
// comparison, 0 with it.
|
||||
//
|
||||
// Returns the parsed node, or null if the key is not a well-formed extended
|
||||
// key. Callers turn null into a user-facing error; none of them may fall back
|
||||
// to fromExtendedKey directly.
|
||||
function parseExtendedKey(key) {
|
||||
if (typeof key !== "string") return null;
|
||||
try {
|
||||
const node = HDNodeWallet.fromExtendedKey(key);
|
||||
return node.extendedKey === key ? node : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// A master key is at depth 0. Only from there is BIP44_ETH_PATH the absolute
|
||||
// path it names; deriving it under an account-level or child key yields
|
||||
// addresses that correspond to nothing the user holds.
|
||||
const MASTER_DEPTH = 0;
|
||||
|
||||
// Parse an extended private key that the BIP-44 Ethereum account path can be
|
||||
// derived from, or throw. Both callers derive BIP44_ETH_PATH from the result.
|
||||
function masterXprvOrThrow(key) {
|
||||
const node = parseExtendedKey(key);
|
||||
if (!node) {
|
||||
throw new Error("Not a valid extended private key (xprv).");
|
||||
}
|
||||
if (!node.privateKey) {
|
||||
throw new Error("Not an extended private key (xprv).");
|
||||
}
|
||||
if (node.depth !== MASTER_DEPTH) {
|
||||
throw new Error(
|
||||
"Not a master extended private key (xprv): an account-level or " +
|
||||
"child key cannot be imported.",
|
||||
);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
function deriveAddressFromXpub(xpub, index) {
|
||||
const node = HDNodeWallet.fromExtendedKey(xpub);
|
||||
const node = parseExtendedKey(xpub);
|
||||
if (!node) {
|
||||
throw new Error("Not a valid extended key.");
|
||||
}
|
||||
return node.deriveChild(index).address;
|
||||
}
|
||||
|
||||
@@ -29,23 +81,28 @@ function hdWalletFromMnemonic(mnemonic) {
|
||||
}
|
||||
|
||||
function hdWalletFromXprv(xprv) {
|
||||
const root = HDNodeWallet.fromExtendedKey(xprv);
|
||||
if (!root.privateKey) {
|
||||
throw new Error("Not an extended private key (xprv).");
|
||||
}
|
||||
const node = root.derivePath("44'/60'/0'/0");
|
||||
// BIP44_ETH_PATH is absolute ("m/..."), which ethers will only derive from
|
||||
// a depth-0 node. The relative form this used to derive would have been
|
||||
// applied *beneath* an account-level key instead of being refused.
|
||||
const node = masterXprvOrThrow(xprv).derivePath(BIP44_ETH_PATH);
|
||||
const xpub = node.neuter().extendedKey;
|
||||
const firstAddress = node.deriveChild(0).address;
|
||||
return { xpub, firstAddress };
|
||||
}
|
||||
|
||||
// Well-formed extended private key. Says nothing about depth: the import view
|
||||
// reports a non-master key separately, since "check it for a typo" is the
|
||||
// wrong advice for a key the user copied correctly.
|
||||
function isValidXprv(key) {
|
||||
try {
|
||||
const node = HDNodeWallet.fromExtendedKey(key);
|
||||
return !!node.privateKey;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const node = parseExtendedKey(key);
|
||||
return !!(node && node.privateKey);
|
||||
}
|
||||
|
||||
// Whether an extended key is a master key, i.e. the one BIP44_ETH_PATH can be
|
||||
// derived from. False for anything parseExtendedKey rejects.
|
||||
function isMasterExtendedKey(key) {
|
||||
const node = parseExtendedKey(key);
|
||||
return !!node && node.depth === MASTER_DEPTH;
|
||||
}
|
||||
|
||||
function addressFromPrivateKey(key) {
|
||||
@@ -63,8 +120,8 @@ function getSignerForAddress(walletData, addrIndex, decryptedSecret) {
|
||||
return node.deriveChild(addrIndex);
|
||||
}
|
||||
if (walletData.type === "xprv") {
|
||||
const root = HDNodeWallet.fromExtendedKey(decryptedSecret);
|
||||
const node = root.derivePath("44'/60'/0'/0");
|
||||
const node =
|
||||
masterXprvOrThrow(decryptedSecret).derivePath(BIP44_ETH_PATH);
|
||||
return node.deriveChild(addrIndex);
|
||||
}
|
||||
return new Wallet(decryptedSecret);
|
||||
@@ -89,6 +146,7 @@ module.exports = {
|
||||
hdWalletFromMnemonic,
|
||||
hdWalletFromXprv,
|
||||
isValidXprv,
|
||||
isMasterExtendedKey,
|
||||
addressFromPrivateKey,
|
||||
getSignerForAddress,
|
||||
isValidMnemonic,
|
||||
|
||||
Reference in New Issue
Block a user