fix(lite-backend): validate WIFs, refuse key import while encrypted
import_taddr decoded a WIF with bs58::decode(..).unwrap(), a [2..66] slice of its hex and SecretKey::from_slice(..).unwrap(), all under the wallet write guard that do_import_tk holds. A z-key, a typo or any short string panicked there and poisoned the wallet until restart, and the checksum was never verified, so a mistyped WIF that was still valid base58 imported a different key. A private key imported into an encrypted wallet was stored without an enc_key. The import then failed in do_save's lock(), which had already zeroed the seed while the wallet stayed "unlocked": seed export showed the all-zero-entropy phrase. - decode_taddr_wif replaces the old decode: Base58Check with the checksum verified, version 0xBC (the config prefix) or 0x80, and the 0x01 compressed flag. It decodes into a buffer it owns and wipes it with sodiumoxide::utils::memzero on every path, including a failed checksum. Errors are fixed strings that never contain the key. - import_taddr and add_imported_sk refuse while the wallet is encrypted, as add_taddr and add_zaddr already do. - do_import_tk and do_import_sk check is_encrypted() before the "locked" check, so a locked wallet does not send the user to unlock and retry. Flagged behaviour changes: - Base58Check, version 0xBC/0x80 and the compressed flag are required; - uncompressed and foreign-network WIFs are rejected with fixed messages (an uncompressed key used to import under the compressed address, where its funds are not); - no panic and no poisoned lock; - private-key import (t and z) is refused while the wallet is encrypted, with a reason that says to remove encryption, import, then encrypt again; viewing keys still import; - the key is never echoed. No ABI, symbol or wallet format change. Tests: garbage, z-keys, bad checksums, a transposed or flipped character, a zero scalar, a wrong flag, uncompressed, 0x05 and 0xEF WIFs are rejected with their message and leave tkeys usable; the wallet's own export, a 0x80 WIF and the published key-1 vector still import; the encrypted refusal in the wallet and the client, locked and unlocked, leaves lock, save and seed export intact; a viewing key still imports into an encrypted wallet; a bad timport leaves the wallet lock unpoisoned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+36
-12
@@ -675,19 +675,17 @@ impl LightWallet {
|
||||
return "Error: Can't add key while wallet is locked".to_string();
|
||||
}
|
||||
|
||||
//// Decode Wif to base58 to hex
|
||||
let sk_to_bs58 = bs58::decode(sk).into_vec().unwrap();
|
||||
// An imported key would be stored with no enc_key; lock() could then never succeed (see lock()).
|
||||
if self.encrypted {
|
||||
return "Error: Can't import a private key while the wallet is encrypted - remove encryption, import, then encrypt again".to_string();
|
||||
}
|
||||
|
||||
let bs58_to_hex = hex::encode(sk_to_bs58);
|
||||
|
||||
//// Manipulate string, to exclude last 4 bytes (checksum bytes), first 2 bytes (secretkey prefix) and the compressed flag (works only for compressed Wifs!)
|
||||
|
||||
let slice_sk = &bs58_to_hex[2..66];
|
||||
|
||||
//// Get the SecretKey from slice
|
||||
let secret_key = SecretKey::from_slice(&hex::decode(slice_sk).unwrap());
|
||||
|
||||
let sk_raw = secret_key.unwrap();
|
||||
//// Decode the WIF: Base58Check (checksum verified), version byte, 32-byte key, 0x01 compressed flag.
|
||||
//// Never panics, and never echoes the key in an error.
|
||||
let sk_raw = match LightWallet::decode_taddr_wif(&sk, self.config.base58_secretkey_prefix()[0]) {
|
||||
Ok(k) => k,
|
||||
Err(e) => return format!("Error: {}", e),
|
||||
};
|
||||
|
||||
//// Make sure the key doesn't already exist
|
||||
if self.tkeys.read().unwrap().iter().find(|&wk| wk.tkey.is_some() && wk.tkey.as_ref().unwrap() == &sk_raw.clone()).is_some() {
|
||||
@@ -714,6 +712,11 @@ impl LightWallet {
|
||||
return "Error: Can't add key while wallet is locked".to_string();
|
||||
}
|
||||
|
||||
// An imported key would be stored with no enc_key; lock() could then never succeed (see lock()).
|
||||
if self.encrypted {
|
||||
return "Error: Can't import a private key while the wallet is encrypted - remove encryption, import, then encrypt again".to_string();
|
||||
}
|
||||
|
||||
// First, try to interpret the key
|
||||
let extsk = match decode_extended_spending_key(self.config.hrp_sapling_private_key(), &sk) {
|
||||
Ok(Some(k)) => k,
|
||||
@@ -934,6 +937,27 @@ impl LightWallet {
|
||||
hash160.result().to_base58check(prefix, &[])
|
||||
}
|
||||
|
||||
/// WIF = version(1) || key(32) || 0x01 (compressed). `bs58 ... with_check(None)` verifies the double-SHA256
|
||||
/// checksum and strips it (bs58 0.2.5 decode.rs:284-310), keeping the version byte. Do NOT pass
|
||||
/// `with_check(Some(v))`: it would pin one version and reject the 0x80 (K/L) WIFs the help text advertises.
|
||||
/// Accepts the config prefix (0xBC on main) and 0x80. The decoded bytes are wiped on every path (s2-D5),
|
||||
/// and no error contains the key.
|
||||
pub(crate) fn decode_taddr_wif(wif: &str, network_prefix: u8) -> Result<SecretKey, String> {
|
||||
let wif = wif.trim();
|
||||
// Decode into a buffer we own (bs58's into_vec allocates the same size), so it is wiped even when the
|
||||
// checksum fails; into_vec would drop its buffer, which then holds the decoded bytes, unwiped.
|
||||
let mut buf = vec![0u8; wif.len()];
|
||||
let r = match bs58::decode(wif).with_check(None).into(&mut buf[..]) {
|
||||
Err(_) => Err("Couldn't decode the transparent private key (not valid Base58Check)".to_string()),
|
||||
Ok(33) => Err("Uncompressed transparent private keys are not supported; import the compressed form".to_string()),
|
||||
Ok(n) if n != 34 || buf[33] != 0x01 => Err("Couldn't decode the transparent private key (unrecognized format)".to_string()),
|
||||
Ok(_) if buf[0] != network_prefix && buf[0] != 0x80 => Err("The transparent private key is for a different network".to_string()),
|
||||
Ok(_) => SecretKey::from_slice(&buf[1..33]).map_err(|_| "Invalid transparent private key".to_string()),
|
||||
};
|
||||
sodiumoxide::utils::memzero(&mut buf[..]); // already a dependency; no new crate
|
||||
r
|
||||
}
|
||||
|
||||
pub fn address_from_sk(&self, sk: &secp256k1::SecretKey) -> String {
|
||||
LightWallet::address_from_prefix_sk(&self.config.base58_pubkey_address(), sk)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user