Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

monero: require seed lang when decoding seed #502

Merged
merged 2 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 93 additions & 29 deletions coins/monero/src/tests/seed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,53 @@ fn test_classic_seed() {
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
},
// The following seeds require the language specification in order to calculate
// a single valid checksum
Vector {
language: classic::Language::Spanish,
seed: "pluma laico atraer pintor peor cerca balde buscar \
lancha batir nulo reloj resto gemelo nevera poder columna gol \
oveja latir amplio bolero feliz fuerza nevera"
.into(),
spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(),
view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(),
},
Vector {
language: classic::Language::Spanish,
seed: "pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma \
pluma pluma pluma pluma pluma pluma pluma pluma pluma"
.into(),
spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(),
view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(),
},
Vector {
language: classic::Language::English,
seed: "plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus \
plus plus plus plus plus plus plus plus plus"
.into(),
spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(),
view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(),
},
Vector {
language: classic::Language::Spanish,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(),
view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(),
},
Vector {
language: classic::Language::English,
seed: "audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio \
audio audio audio audio audio audio audio audio audio"
.into(),
spend: "7900000079000000790000007900000079000000790000007900000079000000".into(),
view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(),
},
];

for vector in vectors {
Expand All @@ -150,15 +197,15 @@ fn test_classic_seed() {

// Test against Monero
{
let seed = Seed::from_string(Zeroizing::new(vector.seed.clone())).unwrap();
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed =
Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(vector.seed.clone()))
.unwrap();
let trim = trim_seed(&vector.seed);
println!(
"{}. seed: {}, entropy: {:?}, trim: {trim}",
line!(),
*seed.to_string(),
*seed.entropy()
assert_eq!(
seed,
Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(trim)).unwrap()
);
assert_eq!(seed, Seed::from_string(Zeroizing::new(trim)).unwrap());

let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
// For classical seeds, Monero directly uses the entropy as a spend key
Expand All @@ -184,19 +231,20 @@ fn test_classic_seed() {
// Test against ourselves
{
let seed = Seed::new(&mut OsRng, SeedType::Classic(vector.language));
println!("{}. seed: {}", line!(), *seed.to_string());
let trim = trim_seed(&seed.to_string());
println!(
"{}. seed: {}, entropy: {:?}, trim: {trim}",
line!(),
*seed.to_string(),
*seed.entropy()
assert_eq!(
seed,
Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(trim)).unwrap()
);
assert_eq!(seed, Seed::from_string(Zeroizing::new(trim)).unwrap());
assert_eq!(
seed,
Seed::from_entropy(SeedType::Classic(vector.language), seed.entropy(), None).unwrap()
);
assert_eq!(seed, Seed::from_string(seed.to_string()).unwrap());
assert_eq!(
seed,
Seed::from_string(SeedType::Classic(vector.language), seed.to_string()).unwrap()
);
}
}
}
Expand Down Expand Up @@ -309,6 +357,18 @@ fn test_polyseed() {
has_prefix: false,
has_accent: false,
},
// The following seed requires the language specification in order to calculate
// a single valid checksum
Vector {
language: polyseed::Language::Spanish,
seed: "impo sort usua cabi venu nobl oliv clim \
cont barr marc auto prod vaca torn fati"
.into(),
entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(),
birthday: 1701511650,
has_prefix: true,
has_accent: true,
},
];

for vector in vectors {
Expand Down Expand Up @@ -350,31 +410,32 @@ fn test_polyseed() {
};

// String -> Seed
let seed = Seed::from_string(Zeroizing::new(vector.seed.clone())).unwrap();
println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone());
let seed =
Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(vector.seed.clone()))
.unwrap();
let trim = trim_seed(&vector.seed);
let add_whitespace = add_whitespace(vector.seed.clone());
let seed_without_accents = seed_without_accents(&vector.seed);
println!(
"{}. seed: {}, entropy: {:?}, trim: {}, add_whitespace: {}, seed_without_accents: {}",
line!(),
*seed.to_string(),
*seed.entropy(),
trim,
add_whitespace,
seed_without_accents,
);

// Make sure a version with added whitespace still works
let whitespaced_seed = Seed::from_string(Zeroizing::new(add_whitespace)).unwrap();
let whitespaced_seed =
Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(add_whitespace))
.unwrap();
assert_eq!(seed, whitespaced_seed);
// Check trimmed versions works
if vector.has_prefix {
let trimmed_seed = Seed::from_string(Zeroizing::new(trim)).unwrap();
let trimmed_seed =
Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(trim)).unwrap();
assert_eq!(seed, trimmed_seed);
}
// Check versions without accents work
if vector.has_accent {
let seed_without_accents = Seed::from_string(Zeroizing::new(seed_without_accents)).unwrap();
let seed_without_accents = Seed::from_string(
SeedType::Polyseed(vector.language),
Zeroizing::new(seed_without_accents),
)
.unwrap();
assert_eq!(seed, seed_without_accents);
}

Expand All @@ -391,8 +452,11 @@ fn test_polyseed() {
// Check against ourselves
{
let seed = Seed::new(&mut OsRng, SeedType::Polyseed(vector.language));
println!("{}. seed: {}, key: {:?}", line!(), *seed.to_string(), *seed.key());
assert_eq!(seed, Seed::from_string(seed.to_string()).unwrap());
println!("{}. seed: {}", line!(), *seed.to_string());
assert_eq!(
seed,
Seed::from_string(SeedType::Polyseed(vector.language), seed.to_string()).unwrap()
);
assert_eq!(
seed,
Seed::from_entropy(
Expand Down
94 changes: 45 additions & 49 deletions coins/monero/src/wallet/seed/classic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::{random_scalar, wallet::seed::SeedError};
pub(crate) const CLASSIC_SEED_LENGTH: usize = 24;
pub(crate) const CLASSIC_SEED_LENGTH_WITH_CHECKSUM: usize = 25;

#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)]
pub enum Language {
Chinese,
English,
Expand Down Expand Up @@ -184,63 +184,58 @@ fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> ClassicSeed {
}
*res += word;
}
ClassicSeed(res)
ClassicSeed(lang, res)
}

// Convert a seed to bytes
pub(crate) fn seed_to_bytes(words: &str) -> Result<(Language, Zeroizing<[u8; 32]>), SeedError> {
pub(crate) fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> {
// get seed words
let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::<Vec<_>>();
if (words.len() != CLASSIC_SEED_LENGTH) && (words.len() != CLASSIC_SEED_LENGTH_WITH_CHECKSUM) {
panic!("invalid seed passed to seed_to_bytes");
}

// find the language
let (matched_indices, lang_name, lang) = (|| {
let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM;
if has_checksum && lang == Language::EnglishOld {
Err(SeedError::EnglishOldWithChecksum)?;
}

// Validate words are in the language word list
let lang_word_list: &WordList = &LANGUAGES()[&lang];
let matched_indices = (|| {
let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM;
let mut matched_indices = Zeroizing::new(vec![]);

// Iterate through all the languages
'language: for (lang_name, lang) in LANGUAGES() {
matched_indices.zeroize();
matched_indices.clear();

// Iterate through all the words and see if they're all present
for word in &words {
let trimmed = trim(word, lang.unique_prefix_length);
let word = if has_checksum { &trimmed } else { word };

if let Some(index) = if has_checksum {
lang.trimmed_word_map.get(word.deref())
} else {
lang.word_map.get(&word.as_str())
} {
matched_indices.push(*index);
} else {
continue 'language;
}
// Iterate through all the words and see if they're all present
for word in &words {
let trimmed = trim(word, lang_word_list.unique_prefix_length);
let word = if has_checksum { &trimmed } else { word };

if let Some(index) = if has_checksum {
lang_word_list.trimmed_word_map.get(word.deref())
} else {
lang_word_list.word_map.get(&word.as_str())
} {
matched_indices.push(*index);
} else {
Err(SeedError::InvalidSeed)?;
}
}

if has_checksum {
if lang_name == &Language::EnglishOld {
Err(SeedError::EnglishOldWithChecksum)?;
}

// exclude the last word when calculating a checksum.
let last_word = words.last().unwrap().clone();
let checksum = words[checksum_index(&words[.. words.len() - 1], lang)].clone();
if has_checksum {
// exclude the last word when calculating a checksum.
let last_word = words.last().unwrap().clone();
let checksum = words[checksum_index(&words[.. words.len() - 1], lang_word_list)].clone();

// check the trimmed checksum and trimmed last word line up
if trim(&checksum, lang.unique_prefix_length) != trim(&last_word, lang.unique_prefix_length)
{
Err(SeedError::InvalidChecksum)?;
}
// check the trimmed checksum and trimmed last word line up
if trim(&checksum, lang_word_list.unique_prefix_length) !=
trim(&last_word, lang_word_list.unique_prefix_length)
{
Err(SeedError::InvalidChecksum)?;
}

return Ok((matched_indices, lang_name, lang));
}

Err(SeedError::UnknownLanguage)?
Ok(matched_indices)
})()?;

// convert to bytes
Expand All @@ -254,16 +249,17 @@ pub(crate) fn seed_to_bytes(words: &str) -> Result<(Language, Zeroizing<[u8; 32]
indices[3] = matched_indices[i3 + 2];

let inner = |i| {
let mut base = (lang.word_list.len() - indices[i] + indices[i + 1]) % lang.word_list.len();
let mut base = (lang_word_list.word_list.len() - indices[i] + indices[i + 1]) %
lang_word_list.word_list.len();
// Shift the index over
for _ in 0 .. i {
base *= lang.word_list.len();
base *= lang_word_list.word_list.len();
}
base
};
// set the last index
indices[0] = indices[1] + inner(1) + inner(2);
if (indices[0] % lang.word_list.len()) != indices[1] {
if (indices[0] % lang_word_list.word_list.len()) != indices[1] {
Err(SeedError::InvalidSeed)?;
}

Expand All @@ -273,19 +269,19 @@ pub(crate) fn seed_to_bytes(words: &str) -> Result<(Language, Zeroizing<[u8; 32]
bytes.zeroize();
}

Ok((*lang_name, res))
Ok(res)
}

#[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct ClassicSeed(Zeroizing<String>);
pub struct ClassicSeed(Language, Zeroizing<String>);
impl ClassicSeed {
pub(crate) fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> ClassicSeed {
key_to_seed(lang, Zeroizing::new(random_scalar(rng)))
}

#[allow(clippy::needless_pass_by_value)]
pub fn from_string(words: Zeroizing<String>) -> Result<ClassicSeed, SeedError> {
let (lang, entropy) = seed_to_bytes(&words)?;
pub fn from_string(lang: Language, words: Zeroizing<String>) -> Result<ClassicSeed, SeedError> {
let entropy = seed_to_bytes(lang, &words)?;

// Make sure this is a valid scalar
let scalar = Scalar::from_canonical_bytes(*entropy);
Expand All @@ -306,10 +302,10 @@ impl ClassicSeed {
}

pub(crate) fn to_string(&self) -> Zeroizing<String> {
self.0.clone()
self.1.clone()
}

pub(crate) fn entropy(&self) -> Zeroizing<[u8; 32]> {
seed_to_bytes(&self.0).unwrap().1
seed_to_bytes(self.0, &self.1).unwrap()
}
}
22 changes: 16 additions & 6 deletions coins/monero/src/wallet/seed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,23 @@ impl Seed {
}

/// Parse a seed from a `String`.
pub fn from_string(words: Zeroizing<String>) -> Result<Seed, SeedError> {
match words.split_whitespace().count() {
CLASSIC_SEED_LENGTH | CLASSIC_SEED_LENGTH_WITH_CHECKSUM => {
ClassicSeed::from_string(words).map(Seed::Classic)
pub fn from_string(seed_type: SeedType, words: Zeroizing<String>) -> Result<Seed, SeedError> {
let word_count = words.split_whitespace().count();
match seed_type {
SeedType::Classic(lang) => {
if word_count != CLASSIC_SEED_LENGTH && word_count != CLASSIC_SEED_LENGTH_WITH_CHECKSUM {
Err(SeedError::InvalidSeedLength)?
} else {
ClassicSeed::from_string(lang, words).map(Seed::Classic)
}
}
SeedType::Polyseed(lang) => {
if word_count != POLYSEED_LENGTH {
Err(SeedError::InvalidSeedLength)?
} else {
Polyseed::from_string(lang, words).map(Seed::Polyseed)
}
}
POLYSEED_LENGTH => Polyseed::from_string(words).map(Seed::Polyseed),
_ => Err(SeedError::InvalidSeedLength)?,
}
}

Expand Down
Loading
Loading