add support for spending and viewkeys

This commit is contained in:
DenioD
2020-09-26 14:41:54 +02:00
parent e5c063817e
commit cd9efe80e7
6 changed files with 1005 additions and 336 deletions

View File

@@ -1,130 +0,0 @@
///
/// In v1.0 of silentdragonlite-cli, there was a bug that incorrectly derived HD wallet keys after the first key. That is, the
/// first key, address was correct, but subsequent ones were not.
///
/// The issue was that the 32-byte seed was directly being used to derive then subsequent addresses instead of the
/// 64-byte pkdf2(seed). The issue affected both t and z addresses
///
/// To fix the bug, we need to:
/// 1. Check if the wallet has more than 1 address for t or z addresses
/// 2. Move any funds in these addresses to the first address
/// 3. Re-derive the addresses
use super::LightWallet;
use crate::lightclient::LightClient;
use json::object;
use bip39::{Mnemonic, Language};
pub struct BugBip39Derivation {}
impl BugBip39Derivation {
/// Check if this bug exists in the wallet
pub fn has_bug(client: &LightClient) -> bool {
let wallet = client.wallet.read().unwrap();
if wallet.zaddress.read().unwrap().len() <= 1 {
return false;
}
if wallet.is_encrypted() {
return false;
}
// The seed bytes is the raw entropy. To pass it to HD wallet generation,
// we need to get the 64 byte bip39 entropy
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&wallet.seed, Language::English).unwrap(), "");
// Check z addresses
for pos in 0..wallet.zaddress.read().unwrap().len() {
let (_, _, address) =
LightWallet::get_zaddr_from_bip39seed(&wallet.config, &bip39_seed.as_bytes(), pos as u32);
if address != wallet.zaddress.read().unwrap()[pos] {
return true;
}
}
// Check t addresses
for pos in 0..wallet.taddresses.read().unwrap().len() {
let sk = LightWallet::get_taddr_from_bip39seed(&wallet.config, &bip39_seed.as_bytes(), pos as u32);
let address = wallet.address_from_sk(&sk);
if address != wallet.taddresses.read().unwrap()[pos] {
return true;
}
}
false
}
/// Automatically fix the bug if it exists in the wallet
pub fn fix_bug(client: &LightClient) -> String {
use zcash_primitives::transaction::components::amount::DEFAULT_FEE;
use std::convert::TryInto;
if !BugBip39Derivation::has_bug(client) {
let r = object!{
"has_bug" => false
};
return r.pretty(2);
}
// Tranfer money
// 1. The desination is z address #0
let zaddr = client.do_address()["z_addresses"][0].as_str().unwrap().to_string();
let balance_json = client.do_balance();
let amount: u64 = balance_json["zbalance"].as_u64().unwrap()
+ balance_json["tbalance"].as_u64().unwrap();
let txid = if amount > 0 {
println!("Sending funds to ourself.");
let fee: u64 = DEFAULT_FEE.try_into().unwrap();
match client.do_send(vec![(&zaddr, amount-fee, None)]) {
Ok(txid) => txid,
Err(e) => {
let r = object!{
"has_bug" => true,
"fixed" => false,
"error" => e,
};
return r.pretty(2);
}
}
} else {
"".to_string()
};
// regen addresses
let wallet = client.wallet.read().unwrap();
let num_zaddrs = wallet.zaddress.read().unwrap().len();
let num_taddrs = wallet.taddresses.read().unwrap().len();
wallet.extsks.write().unwrap().truncate(1);
wallet.extfvks.write().unwrap().truncate(1);
wallet.zaddress.write().unwrap().truncate(1);
wallet.tkeys.write().unwrap().truncate(1);
wallet.taddresses.write().unwrap().truncate(1);
for _ in 1..num_zaddrs {
wallet.add_zaddr();
}
for _ in 1..num_taddrs {
wallet.add_taddr();
}
let r = object!{
"has_bug" => true,
"fixed" => true,
"txid" => txid,
};
return r.pretty(2);
}
}

View File

@@ -141,8 +141,7 @@ impl SaplingNoteData {
// Reading a note also needs the corresponding address to read from.
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
let version = reader.read_u64::<LittleEndian>()?;
assert_eq!(version, SaplingNoteData::serialized_version());
let _version = reader.read_u64::<LittleEndian>()?;
let account = reader.read_u64::<LittleEndian>()? as usize;
@@ -488,9 +487,9 @@ pub struct SpendableNote {
}
impl SpendableNote {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option<Self> {
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &Option<ExtendedSpendingKey>) -> Option<Self> {
// Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet.
if nd.spent.is_none() && nd.unconfirmed_spent.is_none() &&
if nd.spent.is_none() && nd.unconfirmed_spent.is_none() && extsk.is_some() &&
nd.witnesses.len() >= (anchor_offset + 1) {
let witness = nd.witnesses.get(nd.witnesses.len() - anchor_offset - 1);
@@ -500,7 +499,7 @@ impl SpendableNote {
diversifier: nd.diversifier,
note: nd.note.clone(),
witness: w.clone(),
extsk: extsk.clone(),
extsk: extsk.clone().unwrap(),
})
} else {
None

View File

@@ -0,0 +1,425 @@
use std::io::{self, Read, Write};
use std::io::{Error, ErrorKind};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use pairing::bls12_381::{Bls12};
use sodiumoxide::crypto::secretbox;
use zcash_primitives::{
serialize::{Vector, Optional},
zip32::{ExtendedFullViewingKey, ExtendedSpendingKey},
primitives::{PaymentAddress},
};
use crate::lightclient::{LightClientConfig};
use crate::lightwallet::LightWallet;
#[derive(PartialEq, Debug, Clone)]
pub enum WalletZKeyType {
HdKey = 0,
ImportedSpendingKey = 1,
ImportedViewKey = 2
}
// A struct that holds z-address private keys or view keys
#[derive(Clone, Debug, PartialEq)]
pub struct WalletZKey {
pub(super) keytype: WalletZKeyType,
locked: bool,
pub(super) extsk: Option<ExtendedSpendingKey>,
pub(super) extfvk: ExtendedFullViewingKey,
pub(super) zaddress: PaymentAddress<Bls12>,
// If this is a HD key, what is the key number
pub(super) hdkey_num: Option<u32>,
// If locked, the encrypted private key is stored here
enc_key: Option<Vec<u8>>,
nonce: Option<Vec<u8>>,
}
impl WalletZKey {
pub fn new_hdkey(hdkey_num: u32, extsk: ExtendedSpendingKey) -> Self {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::HdKey,
locked: false,
extsk: Some(extsk),
extfvk,
zaddress,
hdkey_num: Some(hdkey_num),
enc_key: None,
nonce: None,
}
}
pub fn new_locked_hdkey(hdkey_num: u32, extfvk: ExtendedFullViewingKey) -> Self {
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::HdKey,
locked: true,
extsk: None,
extfvk,
zaddress,
hdkey_num: Some(hdkey_num),
enc_key: None,
nonce: None
}
}
pub fn new_imported_sk(extsk: ExtendedSpendingKey) -> Self {
let extfvk = ExtendedFullViewingKey::from(&extsk);
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::ImportedSpendingKey,
locked: false,
extsk: Some(extsk),
extfvk,
zaddress,
hdkey_num: None,
enc_key: None,
nonce: None,
}
}
pub fn new_imported_viewkey(extfvk: ExtendedFullViewingKey) -> Self {
let zaddress = extfvk.default_address().unwrap().1;
WalletZKey {
keytype: WalletZKeyType::ImportedViewKey,
locked: false,
extsk: None,
extfvk,
zaddress,
hdkey_num: None,
enc_key: None,
nonce: None,
}
}
fn serialized_version() -> u8 {
return 1;
}
pub fn have_spending_key(&self) -> bool {
self.extsk.is_some() || self.enc_key.is_some() || self.hdkey_num.is_some()
}
pub fn read<R: Read>(mut inp: R) -> io::Result<Self> {
let version = inp.read_u8()?;
assert!(version <= Self::serialized_version());
let keytype: WalletZKeyType = match inp.read_u32::<LittleEndian>()? {
0 => Ok(WalletZKeyType::HdKey),
1 => Ok(WalletZKeyType::ImportedSpendingKey),
2 => Ok(WalletZKeyType::ImportedViewKey),
n => Err(io::Error::new(ErrorKind::InvalidInput, format!("Unknown zkey type {}", n)))
}?;
let locked = inp.read_u8()? > 0;
let extsk = Optional::read(&mut inp, |r| ExtendedSpendingKey::read(r))?;
let extfvk = ExtendedFullViewingKey::read(&mut inp)?;
let zaddress = extfvk.default_address().unwrap().1;
let hdkey_num = Optional::read(&mut inp, |r| r.read_u32::<LittleEndian>())?;
let enc_key = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
let nonce = Optional::read(&mut inp, |r|
Vector::read(r, |r| r.read_u8()))?;
Ok(WalletZKey {
keytype,
locked,
extsk,
extfvk,
zaddress,
hdkey_num,
enc_key,
nonce,
})
}
pub fn write<W: Write>(&self, mut out: W) -> io::Result<()> {
out.write_u8(Self::serialized_version())?;
out.write_u32::<LittleEndian>(self.keytype.clone() as u32)?;
out.write_u8(self.locked as u8)?;
Optional::write(&mut out, &self.extsk, |w, sk| ExtendedSpendingKey::write(sk, w))?;
ExtendedFullViewingKey::write(&self.extfvk, &mut out)?;
Optional::write(&mut out, &self.hdkey_num, |o, n| o.write_u32::<LittleEndian>(*n))?;
// Write enc_key
Optional::write(&mut out, &self.enc_key, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))?;
// Write nonce
Optional::write(&mut out, &self.nonce, |o, v|
Vector::write(o, v, |o,n| o.write_u8(*n)))
}
pub fn lock(&mut self) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, just empty out the keys, since they will be reconstructed from the hdkey_num
self.extsk = None;
self.locked = true;
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, encrypt the key into enckey
// assert that we have the encrypted key.
if self.enc_key.is_none() {
return Err(Error::new(ErrorKind::InvalidInput, "Can't lock when imported key is not encrypted"));
}
self.extsk = None;
self.locked = true;
},
WalletZKeyType::ImportedViewKey => {
// For viewing keys, there is nothing to lock, so just return true
self.locked = true;
}
}
Ok(())
}
pub fn unlock(&mut self, config: &LightClientConfig, bip39_seed: &[u8], key: &secretbox::Key) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
let (extsk, extfvk, address) =
LightWallet::get_zaddr_from_bip39seed(&config, &bip39_seed, self.hdkey_num.unwrap());
if address != self.zaddress {
return Err(io::Error::new(ErrorKind::InvalidData,
format!("zaddress mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), address, self.zaddress)));
}
if extfvk != self.extfvk {
return Err(io::Error::new(ErrorKind::InvalidData,
format!("fvk mismatch at {}. {:?} vs {:?}", self.hdkey_num.unwrap(), extfvk, self.extfvk)));
}
self.extsk = Some(extsk);
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, we need to decrypt from the encrypted key
let nonce = secretbox::Nonce::from_slice(&self.nonce.as_ref().unwrap()).unwrap();
let extsk_bytes = match secretbox::open(&self.enc_key.as_ref().unwrap(), &nonce, &key) {
Ok(s) => s,
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
};
self.extsk = Some(ExtendedSpendingKey::read(&extsk_bytes[..])?);
},
WalletZKeyType::ImportedViewKey => {
// Viewing key unlocking is basically a no op
}
};
self.locked = false;
Ok(())
}
pub fn encrypt(&mut self, key: &secretbox::Key) -> io::Result<()> {
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
},
WalletZKeyType::ImportedSpendingKey => {
// For imported keys, encrypt the key into enckey
let nonce = secretbox::gen_nonce();
let mut sk_bytes = vec![];
self.extsk.as_ref().unwrap().write(&mut sk_bytes)?;
self.enc_key = Some(secretbox::seal(&sk_bytes, &nonce, &key));
self.nonce = Some(nonce.as_ref().to_vec());
},
WalletZKeyType::ImportedViewKey => {
// Encrypting a viewing key is a no-op
}
}
// Also lock after encrypt
self.lock()
}
pub fn remove_encryption(&mut self) -> io::Result<()> {
if self.locked {
return Err(Error::new(ErrorKind::InvalidInput, "Can't remove encryption while locked"));
}
match self.keytype {
WalletZKeyType::HdKey => {
// For HD keys, we don't need to do anything, since the hdnum has all the info to recreate this key
Ok(())
},
WalletZKeyType::ImportedSpendingKey => {
self.enc_key = None;
self.nonce = None;
Ok(())
},
WalletZKeyType::ImportedViewKey => {
// Removing encryption is a no-op for viewing keys
Ok(())
}
}
}
}
#[cfg(test)]
pub mod tests {
use zcash_client_backend::{
encoding::{encode_payment_address, decode_extended_spending_key, decode_extended_full_viewing_key}
};
use sodiumoxide::crypto::secretbox;
use crate::lightclient::LightClientConfig;
use super::WalletZKey;
fn get_config() -> LightClientConfig {
LightClientConfig {
server: "0.0.0.0:0".parse().unwrap(),
chain_name: "main".to_string(),
sapling_activation_height: 0,
consensus_branch_id: "000000".to_string(),
anchor_offset: 0,
data_dir: None,
}
}
#[test]
fn test_serialize() {
let config = get_config();
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
let wzk = WalletZKey::new_imported_sk(esk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
let mut v: Vec<u8> = vec![];
// Serialize
wzk.write(&mut v).unwrap();
// Read it right back
let wzk2 = WalletZKey::read(&v[..]).unwrap();
{
assert_eq!(wzk, wzk2);
assert_eq!(wzk.extsk, wzk2.extsk);
assert_eq!(wzk.extfvk, wzk2.extfvk);
assert_eq!(wzk.zaddress, wzk2.zaddress);
}
}
#[test]
fn test_encrypt_decrypt_sk() {
let config = get_config();
// Priv Key's address is "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv"
let privkey = "secret-extended-key-main1q0p44m9zqqqqpqyxfvy5w2vq6ahvxyrwsk2w4h2zleun4cft4llmnsjlv77lhuuknv6x9jgu5g2clf3xq0wz9axxxq8klvv462r5pa32gjuj5uhxnvps6wsrdg6xll05unwks8qpgp4psmvy5e428uxaggn4l29duk82k3sv3njktaaj453fdmfmj2fup8rls4egqxqtj2p5a3yt4070khn99vzxj5ag5qjngc4v2kq0ctl9q2rpc2phu4p3e26egu9w88mchjf83sqgh3cev";
let esk = decode_extended_spending_key(config.hrp_sapling_private_key(), privkey).unwrap().unwrap();
let mut wzk = WalletZKey::new_imported_sk(esk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1fxgluwznkzm52ux7jkf4st5znwzqay8zyz4cydnyegt2rh9uhr9458z0nk62fdsssx0cqhy6lyv".to_string());
// Can't lock without encryption
assert!(wzk.lock().is_err());
// Encryption key
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
// Encrypt, but save the extsk first
let orig_extsk = wzk.extsk.clone().unwrap();
wzk.encrypt(&key).unwrap();
{
assert!(wzk.enc_key.is_some());
assert!(wzk.nonce.is_some());
}
// Now lock
assert!(wzk.lock().is_ok());
{
assert!(wzk.extsk.is_none());
assert_eq!(wzk.locked, true);
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
}
// Can't remove encryption without unlocking
assert!(wzk.remove_encryption().is_err());
// Unlock
assert!(wzk.unlock(&config, &[], &key).is_ok());
{
assert_eq!(wzk.extsk, Some(orig_extsk));
}
// Remove encryption
assert!(wzk.remove_encryption().is_ok());
{
assert_eq!(wzk.enc_key, None);
assert_eq!(wzk.nonce, None);
}
}
#[test]
fn test_encrypt_decrypt_vk() {
let config = get_config();
// Priv Key's address is "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc"
let viewkey = "zxviews1qvvx7cqdqyqqpqqte7292el2875kw2fgvnkmlmrufyszlcy8xgstwarnumqye3tr3d9rr3ydjm9zl9464majh4pa3ejkfy779dm38sfnkar67et7ykxkk0z9rfsmf9jclfj2k85xt2exkg4pu5xqyzyxzlqa6x3p9wrd7pwdq2uvyg0sal6zenqgfepsdp8shestvkzxuhm846r2h3m4jvsrpmxl8pfczxq87886k0wdasppffjnd2eh47nlmkdvrk6rgyyl0ekh3ycqtvvje";
let extfvk = decode_extended_full_viewing_key(config.hrp_sapling_viewing_key(), viewkey).unwrap().unwrap();
let mut wzk = WalletZKey::new_imported_viewkey(extfvk);
assert_eq!(encode_payment_address(config.hrp_sapling_address(), &wzk.zaddress), "zs1va5902apnzlhdu0pw9r9q7ca8s4vnsrp2alr6xndt69jnepn2v2qrj9vg3wfcnjyks5pg65g9dc".to_string());
// Encryption key
let key = secretbox::Key::from_slice(&[0; 32]).unwrap();
// Encrypt
wzk.encrypt(&key).unwrap();
{
assert!(wzk.enc_key.is_none());
assert!(wzk.nonce.is_none());
}
// Now lock
assert!(wzk.lock().is_ok());
{
assert!(wzk.extsk.is_none());
assert_eq!(wzk.locked, true);
assert_eq!(wzk.zaddress, wzk.extfvk.default_address().unwrap().1);
}
// Can't remove encryption without unlocking
assert!(wzk.remove_encryption().is_err());
// Unlock
assert!(wzk.unlock(&config, &[], &key).is_ok());
{
assert_eq!(wzk.extsk, None);
}
// Remove encryption
assert!(wzk.remove_encryption().is_ok());
{
assert_eq!(wzk.enc_key, None);
assert_eq!(wzk.nonce, None);
}
}
}