use crate::constants;
use crate::error::{Error, Result};
use crate::primitives::sha3_256;
include!(concat!(env!("OUT_DIR"), "/eff_wordlist.rs"));
const _: () = assert!(
EFF_WORDLIST.len() == 7776,
"EFF_WORDLIST must contain exactly 7776 words"
);
#[must_use = "verification phrase must be displayed to the user"]
pub fn verification_phrase(pk_a: &[u8], pk_b: &[u8]) -> Result<String> {
if pk_a.len() != constants::LO_PUBLIC_KEY_SIZE {
return Err(Error::InvalidLength {
expected: constants::LO_PUBLIC_KEY_SIZE,
got: pk_a.len(),
});
}
if pk_b.len() != constants::LO_PUBLIC_KEY_SIZE {
return Err(Error::InvalidLength {
expected: constants::LO_PUBLIC_KEY_SIZE,
got: pk_b.len(),
});
}
if pk_a == pk_b {
return Err(Error::InvalidData);
}
let (first, second) = if pk_a <= pk_b {
(pk_a, pk_b)
} else {
(pk_b, pk_a)
};
let mut input =
Vec::with_capacity(constants::PHRASE_HASH_LABEL.len() + first.len() + second.len());
input.extend_from_slice(constants::PHRASE_HASH_LABEL);
input.extend_from_slice(first);
input.extend_from_slice(second);
let mut hash = sha3_256::hash(&input);
const LIMIT: u16 = 7776 * 8;
const _: () = assert!(7776 * 8 <= u16::MAX as u32);
let mut words = Vec::with_capacity(7);
let mut offset = 0;
let mut rehash_count = 0u32;
while words.len() < 7 {
if offset + 2 > 32 {
rehash_count += 1;
if rehash_count >= 20 {
return Err(Error::Internal);
}
let mut expand_input = Vec::with_capacity(19 + 1 + 32);
expand_input.extend_from_slice(constants::PHRASE_EXPAND_LABEL);
expand_input.push(u8::try_from(rehash_count).map_err(|_| Error::Internal)?);
expand_input.extend_from_slice(&hash);
hash = sha3_256::hash(&expand_input);
offset = 0;
}
let val = u16::from_be_bytes([hash[offset], hash[offset + 1]]);
offset += 2;
if val < LIMIT {
let index = (val as usize) % EFF_WORDLIST.len();
words.push(EFF_WORDLIST[index]);
}
}
Ok(words.join(" "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::identity::{GeneratedIdentity, generate_identity};
#[test]
fn deterministic() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk_b, ..
} = generate_identity().unwrap();
let phrase1 = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap();
let phrase2 = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap();
assert_eq!(phrase1, phrase2);
}
#[test]
fn order_independent() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk_b, ..
} = generate_identity().unwrap();
let ab = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap();
let ba = verification_phrase(pk_b.as_bytes(), pk_a.as_bytes()).unwrap();
assert_eq!(ab, ba);
}
#[test]
fn seven_words() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk_b, ..
} = generate_identity().unwrap();
let phrase = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap();
let words: Vec<&str> = phrase.split(' ').collect();
assert_eq!(words.len(), 7);
}
#[test]
fn all_words_in_wordlist() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk_b, ..
} = generate_identity().unwrap();
let phrase = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap();
for word in phrase.split(' ') {
assert!(
EFF_WORDLIST.contains(&word),
"word '{}' not in EFF_WORDLIST",
word
);
}
}
#[test]
fn different_keys_differ() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk_b, ..
} = generate_identity().unwrap();
let GeneratedIdentity {
public_key: pk_c, ..
} = generate_identity().unwrap();
let phrase_ab = verification_phrase(pk_a.as_bytes(), pk_b.as_bytes()).unwrap();
let phrase_ac = verification_phrase(pk_a.as_bytes(), pk_c.as_bytes()).unwrap();
assert_ne!(phrase_ab, phrase_ac);
}
#[test]
fn wrong_pk_a_size() {
let GeneratedIdentity {
public_key: pk_b, ..
} = generate_identity().unwrap();
assert!(matches!(
verification_phrase(&[0u8; 100], pk_b.as_bytes()),
Err(Error::InvalidLength {
expected: 3200,
got: 100
})
));
}
#[test]
fn wrong_pk_b_size() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
assert!(matches!(
verification_phrase(pk_a.as_bytes(), &[0u8; 100]),
Err(Error::InvalidLength {
expected: 3200,
got: 100
})
));
}
#[test]
fn self_pair_rejected() {
let GeneratedIdentity {
public_key: pk_a, ..
} = generate_identity().unwrap();
assert!(matches!(
verification_phrase(pk_a.as_bytes(), pk_a.as_bytes()),
Err(Error::InvalidData)
));
}
}