mirror of
https://github.com/Qortal/piratewallet-light-cli.git
synced 2025-02-12 02:05:47 +00:00
Add lock/unlock API
This commit is contained in:
parent
fc15de5687
commit
72548e077e
@ -34,6 +34,7 @@ webpki-roots = "0.16.0"
|
|||||||
tower-h2 = { git = "https://github.com/tower-rs/tower-h2" }
|
tower-h2 = { git = "https://github.com/tower-rs/tower-h2" }
|
||||||
rust-embed = { version = "5.1.0", features = ["debug-embed"] }
|
rust-embed = { version = "5.1.0", features = ["debug-embed"] }
|
||||||
rand = "0.7.2"
|
rand = "0.7.2"
|
||||||
|
sodiumoxide = "0.2.5"
|
||||||
|
|
||||||
[dependencies.bellman]
|
[dependencies.bellman]
|
||||||
git = "https://github.com/adityapk00/librustzcash.git"
|
git = "https://github.com/adityapk00/librustzcash.git"
|
||||||
|
@ -91,8 +91,10 @@ impl ToBase58Check for [u8] {
|
|||||||
|
|
||||||
pub struct LightWallet {
|
pub struct LightWallet {
|
||||||
locked: bool, // Is the wallet's spending keys locked?
|
locked: bool, // Is the wallet's spending keys locked?
|
||||||
|
enc_seed: [u8; 48], // If locked, this contains the encrypted seed
|
||||||
|
nonce: Vec<u8>, // Nonce used to encrypt the wallet.
|
||||||
|
|
||||||
seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is encrypted
|
seed: [u8; 32], // Seed phrase for this wallet. If wallet is locked, this is 0
|
||||||
|
|
||||||
// List of keys, actually in this wallet. If the wallet is locked, the `extsks` will be
|
// List of keys, actually in this wallet. If the wallet is locked, the `extsks` will be
|
||||||
// encrypted (but the fvks are not encrpyted)
|
// encrypted (but the fvks are not encrpyted)
|
||||||
@ -123,6 +125,8 @@ impl LightWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey {
|
fn get_taddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) -> SecretKey {
|
||||||
|
assert_eq!(bip39_seed.len(), 64);
|
||||||
|
|
||||||
let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap();
|
let ext_t_key = ExtendedPrivKey::with_seed(bip39_seed).unwrap();
|
||||||
ext_t_key
|
ext_t_key
|
||||||
.derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap()
|
.derive_private_key(KeyIndex::hardened_from_normalize_index(44).unwrap()).unwrap()
|
||||||
@ -134,10 +138,12 @@ impl LightWallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39seed: &[u8], pos: u32) ->
|
fn get_zaddr_from_bip39seed(config: &LightClientConfig, bip39_seed: &[u8], pos: u32) ->
|
||||||
(ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress<Bls12>) {
|
(ExtendedSpendingKey, ExtendedFullViewingKey, PaymentAddress<Bls12>) {
|
||||||
|
assert_eq!(bip39_seed.len(), 64);
|
||||||
|
|
||||||
let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path(
|
let extsk: ExtendedSpendingKey = ExtendedSpendingKey::from_path(
|
||||||
&ExtendedSpendingKey::master(bip39seed),
|
&ExtendedSpendingKey::master(bip39_seed),
|
||||||
&[
|
&[
|
||||||
ChildIndex::Hardened(32),
|
ChildIndex::Hardened(32),
|
||||||
ChildIndex::Hardened(config.get_coin_type()),
|
ChildIndex::Hardened(config.get_coin_type()),
|
||||||
@ -178,6 +184,8 @@ impl LightWallet {
|
|||||||
|
|
||||||
Ok(LightWallet {
|
Ok(LightWallet {
|
||||||
locked: false,
|
locked: false,
|
||||||
|
enc_seed: [0u8; 48],
|
||||||
|
nonce: vec![],
|
||||||
seed: seed_bytes,
|
seed: seed_bytes,
|
||||||
extsks: Arc::new(RwLock::new(vec![extsk])),
|
extsks: Arc::new(RwLock::new(vec![extsk])),
|
||||||
extfvks: Arc::new(RwLock::new(vec![extfvk])),
|
extfvks: Arc::new(RwLock::new(vec![extfvk])),
|
||||||
@ -202,6 +210,17 @@ impl LightWallet {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut enc_seed = [0u8; 48];
|
||||||
|
if version >= 4 {
|
||||||
|
reader.read_exact(&mut enc_seed)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let nonce = if version >= 4 {
|
||||||
|
Vector::read(&mut reader, |r| r.read_u8())?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
// Seed
|
// Seed
|
||||||
let mut seed_bytes = [0u8; 32];
|
let mut seed_bytes = [0u8; 32];
|
||||||
reader.read_exact(&mut seed_bytes)?;
|
reader.read_exact(&mut seed_bytes)?;
|
||||||
@ -257,6 +276,8 @@ impl LightWallet {
|
|||||||
|
|
||||||
Ok(LightWallet{
|
Ok(LightWallet{
|
||||||
locked: locked,
|
locked: locked,
|
||||||
|
enc_seed: enc_seed,
|
||||||
|
nonce: nonce,
|
||||||
seed: seed_bytes,
|
seed: seed_bytes,
|
||||||
extsks: Arc::new(RwLock::new(extsks)),
|
extsks: Arc::new(RwLock::new(extsks)),
|
||||||
extfvks: Arc::new(RwLock::new(extfvks)),
|
extfvks: Arc::new(RwLock::new(extfvks)),
|
||||||
@ -277,6 +298,12 @@ impl LightWallet {
|
|||||||
// Write if it is locked
|
// Write if it is locked
|
||||||
writer.write_u8(if self.locked {1} else {0})?;
|
writer.write_u8(if self.locked {1} else {0})?;
|
||||||
|
|
||||||
|
// Write the encrypted seed bytes
|
||||||
|
writer.write_all(&self.enc_seed)?;
|
||||||
|
|
||||||
|
// Write the nonce
|
||||||
|
Vector::write(&mut writer, &self.nonce, |w, b| w.write_u8(*b))?;
|
||||||
|
|
||||||
// Write the seed
|
// Write the seed
|
||||||
writer.write_all(&self.seed)?;
|
writer.write_all(&self.seed)?;
|
||||||
|
|
||||||
@ -368,8 +395,10 @@ impl LightWallet {
|
|||||||
/// NOTE: This does NOT rescan
|
/// NOTE: This does NOT rescan
|
||||||
pub fn add_zaddr(&self) -> String {
|
pub fn add_zaddr(&self) -> String {
|
||||||
let pos = self.extsks.read().unwrap().len() as u32;
|
let pos = self.extsks.read().unwrap().len() as u32;
|
||||||
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
|
||||||
|
|
||||||
let (extsk, extfvk, address) =
|
let (extsk, extfvk, address) =
|
||||||
LightWallet::get_zaddr_from_bip39seed(&self.config, &self.seed, pos);
|
LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
||||||
|
|
||||||
let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address);
|
let zaddr = encode_payment_address(self.config.hrp_sapling_address(), &address);
|
||||||
self.extsks.write().unwrap().push(extsk);
|
self.extsks.write().unwrap().push(extsk);
|
||||||
@ -384,8 +413,9 @@ impl LightWallet {
|
|||||||
/// NOTE: This is not rescan the wallet
|
/// NOTE: This is not rescan the wallet
|
||||||
pub fn add_taddr(&self) -> String {
|
pub fn add_taddr(&self) -> String {
|
||||||
let pos = self.tkeys.read().unwrap().len() as u32;
|
let pos = self.tkeys.read().unwrap().len() as u32;
|
||||||
|
let bip39_seed = bip39::Seed::new(&Mnemonic::from_entropy(&self.seed, Language::English).unwrap(), "");
|
||||||
|
|
||||||
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &self.seed, pos);
|
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos);
|
||||||
let address = self.address_from_sk(&sk);
|
let address = self.address_from_sk(&sk);
|
||||||
|
|
||||||
self.tkeys.write().unwrap().push(sk);
|
self.tkeys.write().unwrap().push(sk);
|
||||||
@ -531,6 +561,102 @@ impl LightWallet {
|
|||||||
).unwrap().phrase().to_string()
|
).unwrap().phrase().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lock(&mut self, passwd: String) -> io::Result<()> {
|
||||||
|
use sodiumoxide::crypto::secretbox;
|
||||||
|
|
||||||
|
if self.locked {
|
||||||
|
return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is already locked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the doublesha256 of the password, which is the right length
|
||||||
|
let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap();
|
||||||
|
let nonce = secretbox::gen_nonce();
|
||||||
|
|
||||||
|
let cipher = secretbox::seal(&self.seed, &nonce, &key);
|
||||||
|
|
||||||
|
self.enc_seed.copy_from_slice(&cipher);
|
||||||
|
self.nonce = vec![];
|
||||||
|
self.nonce.extend_from_slice(nonce.as_ref());
|
||||||
|
|
||||||
|
// Empty the seed and the secret keys
|
||||||
|
self.seed.copy_from_slice(&[0u8; 32]);
|
||||||
|
self.tkeys = Arc::new(RwLock::new(vec![]));
|
||||||
|
self.extsks = Arc::new(RwLock::new(vec![]));
|
||||||
|
|
||||||
|
self.locked = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(&mut self, passwd: String) -> io::Result<()> {
|
||||||
|
use sodiumoxide::crypto::secretbox;
|
||||||
|
|
||||||
|
if !self.locked {
|
||||||
|
return Err(io::Error::new(ErrorKind::AlreadyExists, "Wallet is not locked"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the doublesha256 of the password, which is the right length
|
||||||
|
let key = secretbox::Key::from_slice(&double_sha256(passwd.as_bytes())).unwrap();
|
||||||
|
let nonce = secretbox::Nonce::from_slice(&self.nonce).unwrap();
|
||||||
|
|
||||||
|
let seed = match secretbox::open(&self.enc_seed, &nonce, &key) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => {return Err(io::Error::new(ErrorKind::InvalidData, "Decryption failed. Is your password correct?"));}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now that we have the seed, we'll generate the extsks and tkeys, and verify the fvks and addresses
|
||||||
|
// respectively match
|
||||||
|
|
||||||
|
// 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(&seed, Language::English).unwrap(), "");
|
||||||
|
|
||||||
|
// Sapling keys
|
||||||
|
let mut extsks = vec![];
|
||||||
|
for pos in 0..self.zaddress.read().unwrap().len() {
|
||||||
|
let (extsk, extfvk, address) =
|
||||||
|
LightWallet::get_zaddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos as u32);
|
||||||
|
|
||||||
|
if address != self.zaddress.read().unwrap()[pos] {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidData,
|
||||||
|
format!("zaddress mismatch at {}. {:?} vs {:?}", pos, address, self.zaddress.read().unwrap()[pos])));
|
||||||
|
}
|
||||||
|
|
||||||
|
if extfvk != self.extfvks.read().unwrap()[pos] {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidData,
|
||||||
|
format!("fvk mismatch at {}. {:?} vs {:?}", pos, extfvk, self.extfvks.read().unwrap()[pos])));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Don't add it to self yet, we'll do that at the end when everything is verified
|
||||||
|
extsks.push(extsk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transparent keys
|
||||||
|
let mut tkeys = vec![];
|
||||||
|
for pos in 0..self.taddresses.read().unwrap().len() {
|
||||||
|
let sk = LightWallet::get_taddr_from_bip39seed(&self.config, &bip39_seed.as_bytes(), pos as u32);
|
||||||
|
let address = self.address_from_sk(&sk);
|
||||||
|
|
||||||
|
if address != self.taddresses.read().unwrap()[pos] {
|
||||||
|
return Err(io::Error::new(ErrorKind::InvalidData,
|
||||||
|
format!("taddress mismatch at {}. {} vs {}", pos, address, self.taddresses.read().unwrap()[pos])));
|
||||||
|
}
|
||||||
|
|
||||||
|
tkeys.push(sk);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Everything checks out, so we'll update our wallet with the unlocked values
|
||||||
|
self.extsks = Arc::new(RwLock::new(extsks));
|
||||||
|
self.tkeys = Arc::new(RwLock::new(tkeys));
|
||||||
|
self.seed.copy_from_slice(&seed);
|
||||||
|
|
||||||
|
self.nonce = vec![];
|
||||||
|
self.enc_seed.copy_from_slice(&[0u8; 48]);
|
||||||
|
self.locked = false;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn zbalance(&self, addr: Option<String>) -> u64 {
|
pub fn zbalance(&self, addr: Option<String>) -> u64 {
|
||||||
self.txs.read().unwrap()
|
self.txs.read().unwrap()
|
||||||
.values()
|
.values()
|
||||||
@ -2927,6 +3053,87 @@ pub mod tests {
|
|||||||
assert_eq!(seed_phrase, Some(wallet.get_seed_phrase()));
|
assert_eq!(seed_phrase, Some(wallet.get_seed_phrase()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_lock_unlock() {
|
||||||
|
const AMOUNT: u64 = 500000;
|
||||||
|
|
||||||
|
let (mut wallet, _, _) = get_test_wallet(AMOUNT);
|
||||||
|
let config = wallet.config.clone();
|
||||||
|
|
||||||
|
// Add some addresses
|
||||||
|
let zaddr0 = encode_payment_address(config.hrp_sapling_address(),
|
||||||
|
&wallet.extfvks.read().unwrap()[0].default_address().unwrap().1);
|
||||||
|
let zaddr1 = wallet.add_zaddr();
|
||||||
|
let zaddr2 = wallet.add_zaddr();
|
||||||
|
|
||||||
|
let taddr0 = wallet.address_from_sk(&wallet.tkeys.read().unwrap()[0]);
|
||||||
|
let taddr1 = wallet.add_taddr();
|
||||||
|
let taddr2 = wallet.add_taddr();
|
||||||
|
|
||||||
|
let seed = wallet.seed;
|
||||||
|
|
||||||
|
wallet.lock("somepassword".to_string()).unwrap();
|
||||||
|
|
||||||
|
// Locking a locked wallet should fail
|
||||||
|
assert!(wallet.lock("somepassword".to_string()).is_err());
|
||||||
|
|
||||||
|
// Serialize a locked wallet
|
||||||
|
let mut serialized_data = vec![];
|
||||||
|
wallet.write(&mut serialized_data).expect("Serialize wallet");
|
||||||
|
|
||||||
|
// Should fail when there's a wrong password
|
||||||
|
assert!(wallet.unlock("differentpassword".to_string()).is_err());
|
||||||
|
|
||||||
|
// Properly unlock
|
||||||
|
wallet.unlock("somepassword".to_string()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(seed, wallet.seed);
|
||||||
|
{
|
||||||
|
let extsks = wallet.extsks.read().unwrap();
|
||||||
|
let tkeys = wallet.tkeys.read().unwrap();
|
||||||
|
assert_eq!(extsks.len(), 3);
|
||||||
|
assert_eq!(tkeys.len(), 3);
|
||||||
|
|
||||||
|
assert_eq!(zaddr0, encode_payment_address(config.hrp_sapling_address(),
|
||||||
|
&ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1));
|
||||||
|
assert_eq!(zaddr1, encode_payment_address(config.hrp_sapling_address(),
|
||||||
|
&ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1));
|
||||||
|
assert_eq!(zaddr2, encode_payment_address(config.hrp_sapling_address(),
|
||||||
|
&ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1));
|
||||||
|
|
||||||
|
assert_eq!(taddr0, wallet.address_from_sk(&tkeys[0]));
|
||||||
|
assert_eq!(taddr1, wallet.address_from_sk(&tkeys[1]));
|
||||||
|
assert_eq!(taddr2, wallet.address_from_sk(&tkeys[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlocking an already unlocked wallet should fail
|
||||||
|
assert!(wallet.unlock("somepassword".to_string()).is_err());
|
||||||
|
|
||||||
|
|
||||||
|
// Try from a deserialized, locked wallet
|
||||||
|
let mut wallet2 = LightWallet::read(&serialized_data[..], &config).unwrap();
|
||||||
|
wallet2.unlock("somepassword".to_string()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(seed, wallet2.seed);
|
||||||
|
{
|
||||||
|
let extsks = wallet2.extsks.read().unwrap();
|
||||||
|
let tkeys = wallet2.tkeys.read().unwrap();
|
||||||
|
assert_eq!(extsks.len(), 3);
|
||||||
|
assert_eq!(tkeys.len(), 3);
|
||||||
|
|
||||||
|
assert_eq!(zaddr0, encode_payment_address(wallet2.config.hrp_sapling_address(),
|
||||||
|
&ExtendedFullViewingKey::from(&extsks[0]).default_address().unwrap().1));
|
||||||
|
assert_eq!(zaddr1, encode_payment_address(wallet2.config.hrp_sapling_address(),
|
||||||
|
&ExtendedFullViewingKey::from(&extsks[1]).default_address().unwrap().1));
|
||||||
|
assert_eq!(zaddr2, encode_payment_address(wallet2.config.hrp_sapling_address(),
|
||||||
|
&ExtendedFullViewingKey::from(&extsks[2]).default_address().unwrap().1));
|
||||||
|
|
||||||
|
assert_eq!(taddr0, wallet2.address_from_sk(&tkeys[0]));
|
||||||
|
assert_eq!(taddr1, wallet2.address_from_sk(&tkeys[1]));
|
||||||
|
assert_eq!(taddr2, wallet2.address_from_sk(&tkeys[2]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_scan_blocks() {
|
fn test_invalid_scan_blocks() {
|
||||||
const AMOUNT: u64 = 500000;
|
const AMOUNT: u64 = 500000;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user