use crate::address::{Address, AddressType};
use crate::base::EpeeWrite;
use crate::crypto::{
Point, Scalar, chacha8, chacha8_generate_key, pub_from_priv, scalar_from_canonical,
scalar_from_wide,
};
use crate::error::{Error, Result};
use crate::finalized::FinalizedTx;
use crate::ftp::FinalizeTxParam;
use purecrypto::hash::keccak256;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct Wallet {
pub spend_priv_key: Option<Scalar>,
pub spend_pub_key: Point,
pub view_priv_key: Scalar,
pub view_pub_key: Point,
pub flags: u8,
}
impl Wallet {
pub fn load_spend_secret(pk: &[u8], flags: u8) -> Result<Wallet> {
let spend_priv = scalar_from_canonical(pk)?;
let view_priv = scalar_from_wide(&keccak256(pk));
Ok(Wallet {
spend_pub_key: pub_from_priv(&spend_priv),
spend_priv_key: Some(spend_priv),
view_pub_key: pub_from_priv(&view_priv),
view_priv_key: view_priv,
flags,
})
}
pub fn generate(rnd: &mut dyn crate::rng::RngCore, flags: u8) -> Result<Wallet> {
let mut seed = [0u8; 64];
rnd.fill_bytes(&mut seed);
let s = Scalar::from_bytes_mod_order(&seed);
Wallet::load_spend_secret(&s.to_bytes(), flags)
}
pub fn load_view_only(
view_secret_key: &[u8],
spend_public_key: &[u8],
flags: u8,
) -> Result<Wallet> {
if view_secret_key.len() != 32 {
return Err(crate::err!(
"view secret key must be 32 bytes, got {}",
view_secret_key.len()
));
}
if spend_public_key.len() != 32 {
return Err(crate::err!(
"spend public key must be 32 bytes, got {}",
spend_public_key.len()
));
}
let view_priv = scalar_from_canonical(view_secret_key)
.map_err(|_| Error::msg("invalid view secret key"))?;
let spend_pub = crate::crypto::point_from_bytes(spend_public_key)
.map_err(|_| Error::msg("invalid spend public key"))?;
Ok(Wallet {
spend_priv_key: None,
spend_pub_key: spend_pub,
view_pub_key: pub_from_priv(&view_priv),
view_priv_key: view_priv,
flags,
})
}
pub fn is_view_only(&self) -> bool {
self.spend_priv_key.is_none()
}
pub fn address(&self) -> Address {
let typ = if self.flags & 1 == 1 {
AddressType::Audit
} else {
AddressType::Public
};
Address {
typ,
flags: self.flags,
spend_key: self.spend_pub_key.compress().to_vec(),
view_key: self.view_pub_key.compress().to_vec(),
payment_id: Vec::new(),
}
}
pub fn parse_ftp(&self, buf: &[u8]) -> Result<FinalizeTxParam> {
FinalizeTxParam::parse(buf, &self.view_priv_key.to_bytes())
}
pub fn parse_finalized(&self, buf: &[u8]) -> Result<FinalizedTx> {
FinalizedTx::parse(buf, &self.view_priv_key.to_bytes())
}
pub fn encrypt<T: EpeeWrite>(&self, data: &T) -> Result<Vec<u8>> {
let plain = data.to_epee_bytes();
let code = chacha8_generate_key(&self.view_priv_key.to_bytes())?;
chacha8(&code, &[0u8; 8], &plain)
}
pub fn export_view(&self, start_height: u64) -> ViewWalletData {
ViewWalletData {
address: Some(self.address().to_string()),
spend_public_key: Some(hex::encode(self.spend_pub_key.compress())),
view_public_key: Some(hex::encode(self.view_pub_key.compress())),
view_secret_key: hex::encode(self.view_priv_key.to_bytes()),
flags: self.flags,
start_height,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ViewWalletData {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub spend_public_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub view_public_key: Option<String>,
pub view_secret_key: String,
#[serde(default, skip_serializing_if = "is_zero_u8")]
pub flags: u8,
pub start_height: u64,
}
fn is_zero_u8(v: &u8) -> bool {
*v == 0
}
impl ViewWalletData {
pub fn load_view_wallet(&self) -> Result<Wallet> {
let view_sec =
hex::decode(&self.view_secret_key).map_err(|e| crate::err!("view_secret_key: {e}"))?;
let (spend_pub, flags) = match (&self.spend_public_key, &self.address) {
(Some(s), _) if !s.is_empty() => (
hex::decode(s).map_err(|e| crate::err!("spend_public_key: {e}"))?,
self.flags,
),
(_, Some(a)) if !a.is_empty() => {
let addr = Address::parse(a).map_err(|e| crate::err!("address: {e}"))?;
(addr.spend_key.clone(), addr.flags)
}
_ => {
return Err(Error::msg(
"view wallet data needs either address or spend_public_key",
));
}
};
let w = Wallet::load_view_only(&view_sec, &spend_pub, flags)?;
if let Some(vpub) = self.view_public_key.as_deref().filter(|s| !s.is_empty()) {
let want = hex::decode(vpub).map_err(|e| crate::err!("view_public_key: {e}"))?;
if w.view_pub_key.compress()[..] != want[..] {
return Err(Error::msg("view_public_key does not match view_secret_key"));
}
}
if let Some(addr) = self.address.as_deref().filter(|s| !s.is_empty()) {
let got = w.address().to_string();
if got != addr {
return Err(crate::err!(
"address {addr:?} does not match the provided keys (derived {got:?})"
));
}
}
Ok(w)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rng::FixedRng;
fn spend_secret(first: u8) -> [u8; 32] {
let mut b = [0u8; 32];
b[0] = first;
b
}
#[test]
fn view_key_is_keccak_of_spend_secret() {
let spend = spend_secret(1);
let w = Wallet::load_spend_secret(&spend, 0).unwrap();
let want = scalar_from_wide(&keccak256(&spend));
assert_eq!(w.view_priv_key.to_bytes(), want.to_bytes());
assert_eq!(w.view_pub_key, Point::mul_base(&w.view_priv_key));
assert_eq!(
w.spend_pub_key,
Point::mul_base(w.spend_priv_key.as_ref().unwrap())
);
assert!(!w.is_view_only());
}
#[test]
fn wallet_is_deterministic() {
let a = Wallet::load_spend_secret(&spend_secret(7), 0).unwrap();
let b = Wallet::load_spend_secret(&spend_secret(7), 0).unwrap();
assert_eq!(a.address().to_string(), b.address().to_string());
}
#[test]
fn auditable_flag_changes_the_address_type() {
let w = Wallet::load_spend_secret(&spend_secret(9), 1).unwrap();
assert_eq!(w.address().typ, AddressType::Audit);
assert!(w.address().to_string().starts_with("aZx"));
}
#[test]
fn short_or_non_canonical_keys_are_rejected() {
assert!(Wallet::load_spend_secret(&[1, 2, 3], 0).is_err());
assert!(Wallet::load_spend_secret(&[0xff; 32], 0).is_err());
}
#[test]
fn view_only_round_trip() {
let mut rnd = FixedRng(0x42);
let full = Wallet::generate(&mut rnd, 0).unwrap();
let data = full.export_view(1234);
let view = data.load_view_wallet().unwrap();
assert!(view.is_view_only());
assert_eq!(view.address().to_string(), full.address().to_string());
assert_eq!(view.view_priv_key.to_bytes(), full.view_priv_key.to_bytes());
}
#[test]
fn view_wallet_data_json_round_trip() {
let full = Wallet::load_spend_secret(&spend_secret(3), 0).unwrap();
let json = serde_json::to_string(&full.export_view(10)).unwrap();
let back: ViewWalletData = serde_json::from_str(&json).unwrap();
assert_eq!(back.start_height, 10);
assert_eq!(
back.load_view_wallet().unwrap().address().to_string(),
full.address().to_string()
);
}
#[test]
fn mismatched_view_public_key_is_rejected() {
let full = Wallet::load_spend_secret(&spend_secret(3), 0).unwrap();
let mut data = full.export_view(0);
data.view_public_key = Some(hex::encode([0x02u8; 32]));
assert!(data.load_view_wallet().is_err());
}
#[test]
fn view_only_needs_a_key_source() {
let data = ViewWalletData {
view_secret_key: hex::encode([1u8; 32]),
..Default::default()
};
assert!(data.load_view_wallet().is_err());
}
}