use anyhow::{bail, Result};
use num_bigint::BigUint;
use num_traits::One;
use zeroize::Zeroizing;
mod words;
mod capi;
#[derive(Clone)]
pub struct SeedEnc {
bytes: Zeroizing<[u8; Self::BYTES_LEN]>,
}
fn nums_for_words(w: &str) -> Result<[u16; SeedEnc::WORD_COUNT]> {
let splitwords = w.split(" ").collect::<Vec<_>>();
if splitwords.len() != SeedEnc::WORD_COUNT {
bail!(
"Expected a {} word seed, got {}",
SeedEnc::WORD_COUNT,
splitwords.len()
);
}
let mut nums = [0_u16; SeedEnc::WORD_COUNT];
let mut offending_word = None;
for lang in words::LANGUAGES {
for (word, i) in splitwords.iter().zip(0..) {
if let Some(n) = lang.num_for_word(word) {
nums[i] = n;
if i == SeedEnc::WORD_COUNT - 1 {
return Ok(nums);
}
} else {
offending_word = Some(word);
break;
}
}
}
bail!(
"No language could be found which matched: {:?} in languages: {:?}",
offending_word,
words::LANGUAGES.iter().map(|l|l.name).collect::<Vec<_>>()
);
}
fn date_from_ts(ts: u64) -> std::time::SystemTime {
use std::ops::Add;
std::time::SystemTime::UNIX_EPOCH.add(std::time::Duration::from_secs(ts))
}
impl SeedEnc {
const BEGINNING_OF_TIME: u64 = 1586276691;
pub fn words(&self, lang_name: &str) -> Result<String> {
if let Some(lang) = words::language(lang_name) {
let mut words = [""; Self::WORD_COUNT];
for (n, i) in self.nums().iter().zip(0..) {
words[i] = lang.word_for_num(*n).unwrap();
}
Ok(words.join(" "))
} else {
bail!("Language {} not found", lang_name);
}
}
pub fn from_words(w: &str) -> Result<Self> {
let nums = nums_for_words(w)?;
Self::from_nums(nums)
}
fn get_unused(&self) -> u8 {
self.bytes[0] >> 5
}
fn get_ver(&self) -> u8 {
(self.bytes[0] >> 1) & 0x0f
}
pub fn is_encrypted(&self) -> bool {
self.bytes[0] & 0x01 == 0x01
}
pub fn decrypt(&self, passphrase: Option<&[u8]>, force: bool) -> Result<Seed> {
let mut copy = self.clone();
if passphrase.is_some() && self.is_encrypted() {
cipher(&mut copy.bytes[2..], passphrase);
} else if self.is_encrypted() {
bail!("This seed is encrypted and requires a passphrase to decrypt");
}
let out = Seed::new_raw(©.bytes[2..]);
if !force {
if out.get_bday() < Self::BEGINNING_OF_TIME {
bail!(concat!(
"This seed has a declared birthday of {:?} which is older than the ",
"time when this seed algorithm was first created, the password or ",
"seed words are probably incorrect, to override this message set force ",
"to true."), date_from_ts(out.get_bday()));
} else if out.get_bday() > now_sec() {
bail!(concat!(
"This seed has a declated birthday of {:?} which is in the future ",
"the seed or password protecting it are probably incorrect. To override ",
"this message set force to true."), date_from_ts(out.get_bday()));
}
}
Ok(Seed::new_raw(©.bytes[2..]))
}
const BYTES_LEN: usize = 21;
const VER: u8 = 0;
const EXPECT_UNUSED: u8 = 1;
const WORD_COUNT: usize = 15;
fn from_nums(nums: [u16; Self::WORD_COUNT]) -> Result<Self> {
let mut b = BigUint::one();
for n in nums.iter().rev() {
b <<= 11;
b += *n;
}
let bytes = b.to_bytes_be();
if bytes.len() != Self::BYTES_LEN {
bail!("invalid seed: unexpected byte length");
}
let mut out = SeedEnc {
bytes: Zeroizing::new([0_u8; Self::BYTES_LEN]),
};
out.bytes.copy_from_slice(&bytes[..]);
if out.get_unused() != Self::EXPECT_UNUSED {
bail!("Invalid seed: Wrong bit pattern");
}
out.put_unused(0);
if out.get_ver() != Self::VER {
bail!("Invalid seed: Unknown version [{}]", out.get_ver())
}
if out.get_csum() != out.compute_csum() {
bail!(
"Invalid seed: Checksum mismatch: Declared: [{}], Computed: [{}]",
out.get_csum(),
out.compute_csum()
)
}
Ok(out)
}
fn nums(&self) -> [u16; Self::WORD_COUNT] {
let mut copy = self.clone();
copy.put_unused(Self::EXPECT_UNUSED);
let mut b = BigUint::from_bytes_be(©.bytes[..]);
let mut out = [0_u16; Self::WORD_COUNT];
for i in 0..Self::WORD_COUNT {
out[i] = (b.iter_u32_digits().next().unwrap() & 2047) as u16;
b >>= 11;
}
assert!(b.is_one());
out
}
fn put_unused(&mut self, u: u8) {
self.bytes[0] &= 31;
self.bytes[0] |= u << 5;
}
fn put_ver(&mut self, v: u8) {
self.bytes[0] = (self.bytes[0] & 0x01) | ((v & 0x0f) << 1);
}
fn put_encrypted(&mut self, e: bool) {
self.bytes[0] &= 0x1e;
if e {
self.bytes[0] |= 0x01
}
}
fn get_csum(&self) -> u8 {
self.bytes[1]
}
fn put_csum(&mut self, csum: u8) {
self.bytes[1] = csum;
}
fn compute_csum(&self) -> u8 {
let mut b2b = blake2b_simd::Params::new().hash_length(32).to_state();
let mut copy = self.clone();
copy.put_csum(0);
b2b.update(©.bytes[..]);
let res = b2b.finalize();
res.as_bytes()[0]
}
}
fn now_sec() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
let start = SystemTime::now();
start.duration_since(UNIX_EPOCH).unwrap().as_secs()
}
const ARGON_SALT: &[u8] = b"pktwallet seed 0";
const ARGON_ITERATIONS: u32 = 32;
const ARGON_THREADS: u32 = 8;
const ARGON_MEMORY: u32 = 256 * 1024; const ARGON_HASH_LEN: u32 = 19;
fn cipher(data: &mut [u8], passphrase: Option<&[u8]>) {
if let Some(passphrase) = passphrase {
let hash = argon2::hash_raw(passphrase, ARGON_SALT, &argon2::Config{
ad: &[],
hash_length: ARGON_HASH_LEN,
lanes: ARGON_THREADS,
mem_cost: ARGON_MEMORY,
secret: &[],
thread_mode: argon2::ThreadMode::Parallel,
time_cost: ARGON_ITERATIONS,
variant: argon2::Variant::Argon2id,
version: argon2::Version::Version13,
}).unwrap();
for i in 0..data.len() {
data[i] ^= hash[i];
}
}
}
#[derive(Clone)]
pub struct Seed {
pub bytes: Zeroizing<[u8; Self::BYTES_LEN]>,
}
impl Seed {
pub const BYTES_LEN: usize = 19;
pub fn new(bytes: &[u8]) -> Self {
Self::new_bday(bytes, now_sec())
}
pub fn new_bday(bytes: &[u8], birthday: u64) -> Self {
let mut out = Self {
bytes: Zeroizing::new([0_u8; Self::BYTES_LEN]),
};
out.bytes[2..].copy_from_slice(bytes);
out.put_bday(birthday);
out
}
pub fn new_raw(bytes: &[u8]) -> Self {
let mut out = Self {
bytes: Zeroizing::new([0_u8; Self::BYTES_LEN]),
};
out.bytes.copy_from_slice(bytes);
out
}
pub fn get_bday(&self) -> u64 {
let mut bday_bytes = [0_u8; 2];
bday_bytes.copy_from_slice(&self.bytes[0..2]);
let day = u16::from_be_bytes(bday_bytes);
(day as u64) * 60 * 60 * 24
}
pub fn put_bday(&mut self, unix: u64) {
let day_bytes = ((unix / (60 * 60 * 24)) as u16).to_be_bytes();
self.bytes[0..2].copy_from_slice(&day_bytes[..]);
}
pub fn encrypt(&self, passphrase: Option<&[u8]>) -> SeedEnc {
let mut out = SeedEnc {
bytes: Zeroizing::new([0_u8; SeedEnc::BYTES_LEN]),
};
out.bytes[2..].copy_from_slice(&self.bytes[..]);
cipher(&mut out.bytes[2..], passphrase);
out.put_ver(SeedEnc::VER);
out.put_encrypted(passphrase.is_some());
out.put_csum(out.compute_csum());
out
}
}
#[cfg(test)]
mod tests {
use super::Seed;
use super::SeedEnc;
use anyhow::Result;
const WORDS: &str =
"mom blanket bulk draw clip wolf bread erupt merry skin cable infant word exchange animal";
const BDAY: u64 = 1629936000;
const SECRET_HEX: &str = "49b1ad8001b3c4813d50c087c5a4e206aeb111";
const SEED_PASS: &[u8] = b"password";
#[test]
fn test_vec() -> Result<()> {
let se = SeedEnc::from_words(WORDS)?;
let seed = se.decrypt(Some(SEED_PASS), false)?;
let seed_hex = hex::encode(&seed.bytes[..]);
assert_eq!(seed.get_bday(), BDAY);
assert_eq!(seed_hex, SECRET_HEX);
Ok(())
}
#[test]
fn test_nums_from_words() -> Result<()> {
let expect_nums = [
1142_u16, 186, 240, 531, 346, 2022, 219, 615, 1117, 1620, 255, 922, 2027, 629, 72,
];
let nums = super::nums_for_words(WORDS)?;
assert_eq!(nums, expect_nums);
Ok(())
}
#[test]
fn test_enc_from_words() -> Result<()> {
let se = SeedEnc::from_words(WORDS)?;
let seed_hex = hex::encode(&se.bytes[..]);
println!("seed_enc_hex = {}", seed_hex);
assert_eq!(seed_hex, "01213afeb7343ff2a45d4ce36ff315a4263c05d476");
Ok(())
}
#[test]
fn test_dec_to_words() {
let seed = Seed::new_raw(&hex::decode(SECRET_HEX).unwrap()[..]);
let se = seed.encrypt(Some(SEED_PASS));
assert_eq!(se.words("english").unwrap(), WORDS);
}
}