Smash out seed

This commit is contained in:
Luke Parker
2024-06-23 06:31:22 -04:00
parent 1e2e3bd5ce
commit 11dba9173f
22 changed files with 146 additions and 64 deletions

11
Cargo.lock generated
View File

@@ -4862,6 +4862,17 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "monero-seed"
version = "0.1.0"
dependencies = [
"curve25519-dalek",
"rand_core",
"std-shims",
"thiserror",
"zeroize",
]
[[package]] [[package]]
name = "monero-serai" name = "monero-serai"
version = "0.1.4-alpha" version = "0.1.4-alpha"

View File

@@ -54,6 +54,7 @@ members = [
"coins/monero/rpc", "coins/monero/rpc",
"coins/monero/rpc/simple-request", "coins/monero/rpc/simple-request",
"coins/monero/wallet", "coins/monero/wallet",
"coins/monero/wallet/seed",
"coins/monero/wallet/polyseed", "coins/monero/wallet/polyseed",
"message-queue", "message-queue",

View File

@@ -3,7 +3,7 @@ name = "polyseed"
version = "0.1.0" version = "0.1.0"
description = "Rust implementation of Polyseed" description = "Rust implementation of Polyseed"
license = "MIT" license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet.polyseed" repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet/polyseed"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021" edition = "2021"
rust-version = "1.79" rust-version = "1.79"
@@ -40,5 +40,4 @@ std = [
"sha3/std", "sha3/std",
"pbkdf2/std", "pbkdf2/std",
] ]
default = ["std"] default = ["std"]

View File

@@ -0,0 +1,37 @@
[package]
name = "monero-seed"
version = "0.1.0"
description = "Rust implementation of Monero's seed algorithm"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet/seed"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021"
rust-version = "1.79"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[lints]
workspace = true
[dependencies]
std-shims = { path = "../../../../common/std-shims", version = "^0.1.1", default-features = false }
thiserror = { version = "1", default-features = false, optional = true }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
rand_core = { version = "0.6", default-features = false }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] }
[features]
std = [
"std-shims/std",
"thiserror",
"zeroize/std",
"rand_core/std",
]
default = ["std"]

View File

@@ -1,6 +1,6 @@
# Monero Seeds # Monero Seeds
A Rust implementation of Monero's seed algorithm. Rust implementation of Monero's seed algorithm.
This library is usable under no-std when the `std` feature (on by default) is This library is usable under no-std when the `std` feature (on by default) is
disabled. disabled.

View File

@@ -11,26 +11,55 @@ use rand_core::{RngCore, CryptoRng};
use curve25519_dalek::scalar::Scalar; use curve25519_dalek::scalar::Scalar;
use crate::seed::SeedError; const CLASSIC_SEED_LENGTH: usize = 24;
const CLASSIC_SEED_LENGTH_WITH_CHECKSUM: usize = 25;
pub(crate) const CLASSIC_SEED_LENGTH: usize = 24; /// An error when working with a seed.
pub(crate) const CLASSIC_SEED_LENGTH_WITH_CHECKSUM: usize = 25; #[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum SeedError {
/// The deprecated English language option was used with a checksum.
///
/// The deprecated English language option did not include a checksum.
#[cfg_attr(feature = "std", error("deprecated English language option included a checksum"))]
DeprecatedEnglishWithChecksum,
#[cfg_attr(feature = "std", error("invalid seed"))]
/// The seed was invalid.
InvalidSeed,
/// The checksum did not match the data.
#[cfg_attr(feature = "std", error("invalid checksum"))]
InvalidChecksum,
}
/// Language options.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)]
pub enum Language { pub enum Language {
/// Chinese language option.
Chinese, Chinese,
/// English language option.
English, English,
/// Dutch language option.
Dutch, Dutch,
/// French language option.
French, French,
/// Spanish language option.
Spanish, Spanish,
/// German language option.
German, German,
/// Italian language option.
Italian, Italian,
/// Portuguese language option.
Portuguese, Portuguese,
/// Japanese language option.
Japanese, Japanese,
/// Russian language option.
Russian, Russian,
/// Esperanto language option.
Esperanto, Esperanto,
/// Lojban language option.
Lojban, Lojban,
EnglishOld, /// The original, and deprecated, English language.
DeprecatedEnglish,
} }
fn trim(word: &str, len: usize) -> Zeroizing<String> { fn trim(word: &str, len: usize) -> Zeroizing<String> {
@@ -38,14 +67,14 @@ fn trim(word: &str, len: usize) -> Zeroizing<String> {
} }
struct WordList { struct WordList {
word_list: Vec<&'static str>, word_list: &'static [&'static str],
word_map: HashMap<&'static str, usize>, word_map: HashMap<&'static str, usize>,
trimmed_word_map: HashMap<String, usize>, trimmed_word_map: HashMap<String, usize>,
unique_prefix_length: usize, unique_prefix_length: usize,
} }
impl WordList { impl WordList {
fn new(word_list: Vec<&'static str>, prefix_length: usize) -> WordList { fn new(word_list: &'static [&'static str], prefix_length: usize) -> WordList {
let mut lang = WordList { let mut lang = WordList {
word_list, word_list,
word_map: HashMap::new(), word_map: HashMap::new(),
@@ -67,32 +96,23 @@ static LANGUAGES_CELL: OnceLock<HashMap<Language, WordList>> = OnceLock::new();
fn LANGUAGES() -> &'static HashMap<Language, WordList> { fn LANGUAGES() -> &'static HashMap<Language, WordList> {
LANGUAGES_CELL.get_or_init(|| { LANGUAGES_CELL.get_or_init(|| {
HashMap::from([ HashMap::from([
(Language::Chinese, WordList::new(include!("./classic/zh.rs"), 1)), (Language::Chinese, WordList::new(include!("./words/zh.rs"), 1)),
(Language::English, WordList::new(include!("./classic/en.rs"), 3)), (Language::English, WordList::new(include!("./words/en.rs"), 3)),
(Language::Dutch, WordList::new(include!("./classic/nl.rs"), 4)), (Language::Dutch, WordList::new(include!("./words/nl.rs"), 4)),
(Language::French, WordList::new(include!("./classic/fr.rs"), 4)), (Language::French, WordList::new(include!("./words/fr.rs"), 4)),
(Language::Spanish, WordList::new(include!("./classic/es.rs"), 4)), (Language::Spanish, WordList::new(include!("./words/es.rs"), 4)),
(Language::German, WordList::new(include!("./classic/de.rs"), 4)), (Language::German, WordList::new(include!("./words/de.rs"), 4)),
(Language::Italian, WordList::new(include!("./classic/it.rs"), 4)), (Language::Italian, WordList::new(include!("./words/it.rs"), 4)),
(Language::Portuguese, WordList::new(include!("./classic/pt.rs"), 4)), (Language::Portuguese, WordList::new(include!("./words/pt.rs"), 4)),
(Language::Japanese, WordList::new(include!("./classic/ja.rs"), 3)), (Language::Japanese, WordList::new(include!("./words/ja.rs"), 3)),
(Language::Russian, WordList::new(include!("./classic/ru.rs"), 4)), (Language::Russian, WordList::new(include!("./words/ru.rs"), 4)),
(Language::Esperanto, WordList::new(include!("./classic/eo.rs"), 4)), (Language::Esperanto, WordList::new(include!("./words/eo.rs"), 4)),
(Language::Lojban, WordList::new(include!("./classic/jbo.rs"), 4)), (Language::Lojban, WordList::new(include!("./words/jbo.rs"), 4)),
(Language::EnglishOld, WordList::new(include!("./classic/ang.rs"), 4)), (Language::DeprecatedEnglish, WordList::new(include!("./words/ang.rs"), 4)),
]) ])
}) })
} }
#[cfg(test)]
pub(crate) fn trim_by_lang(word: &str, lang: Language) -> String {
if lang != Language::EnglishOld {
word.chars().take(LANGUAGES()[&lang].unique_prefix_length).collect()
} else {
word.to_string()
}
}
fn checksum_index(words: &[Zeroizing<String>], lang: &WordList) -> usize { fn checksum_index(words: &[Zeroizing<String>], lang: &WordList) -> usize {
let mut trimmed_words = Zeroizing::new(String::new()); let mut trimmed_words = Zeroizing::new(String::new());
for w in words { for w in words {
@@ -135,7 +155,7 @@ fn checksum_index(words: &[Zeroizing<String>], lang: &WordList) -> usize {
// Convert a private key to a seed // Convert a private key to a seed
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> ClassicSeed { fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> Seed {
let bytes = Zeroizing::new(key.to_bytes()); let bytes = Zeroizing::new(key.to_bytes());
// get the language words // get the language words
@@ -172,7 +192,7 @@ fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> ClassicSeed {
indices.zeroize(); indices.zeroize();
// create a checksum word for all languages except old english // create a checksum word for all languages except old english
if lang != Language::EnglishOld { if lang != Language::DeprecatedEnglish {
let checksum = seed[checksum_index(&seed, &LANGUAGES()[&lang])].clone(); let checksum = seed[checksum_index(&seed, &LANGUAGES()[&lang])].clone();
seed.push(checksum); seed.push(checksum);
} }
@@ -184,11 +204,11 @@ fn key_to_seed(lang: Language, key: Zeroizing<Scalar>) -> ClassicSeed {
} }
*res += word; *res += word;
} }
ClassicSeed(lang, res) Seed(lang, res)
} }
// Convert a seed to bytes // Convert a seed to bytes
pub(crate) fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> { fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8; 32]>, SeedError> {
// get seed words // get seed words
let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::<Vec<_>>(); 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) { if (words.len() != CLASSIC_SEED_LENGTH) && (words.len() != CLASSIC_SEED_LENGTH_WITH_CHECKSUM) {
@@ -196,8 +216,8 @@ pub(crate) fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8
} }
let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM; let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM;
if has_checksum && lang == Language::EnglishOld { if has_checksum && lang == Language::DeprecatedEnglish {
Err(SeedError::EnglishOldWithChecksum)?; Err(SeedError::DeprecatedEnglishWithChecksum)?;
} }
// Validate words are in the language word list // Validate words are in the language word list
@@ -272,15 +292,20 @@ pub(crate) fn seed_to_bytes(lang: Language, words: &str) -> Result<Zeroizing<[u8
Ok(res) Ok(res)
} }
/// A Monero seed.
#[derive(Clone, PartialEq, Eq, Zeroize)] #[derive(Clone, PartialEq, Eq, Zeroize)]
pub struct ClassicSeed(Language, Zeroizing<String>); pub struct Seed(Language, Zeroizing<String>);
impl ClassicSeed { impl Seed {
pub(crate) fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> ClassicSeed { /// Create a new seed.
key_to_seed(lang, Zeroizing::new(Scalar::random(rng))) pub fn new<R: RngCore + CryptoRng>(rng: &mut R, lang: Language) -> Seed {
let mut scalar_bytes = Zeroizing::new([0; 64]);
rng.fill_bytes(scalar_bytes.as_mut());
key_to_seed(lang, Zeroizing::new(Scalar::from_bytes_mod_order_wide(scalar_bytes.deref())))
} }
/// Parse a seed from a string.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn from_string(lang: Language, words: Zeroizing<String>) -> Result<ClassicSeed, SeedError> { pub fn from_string(lang: Language, words: Zeroizing<String>) -> Result<Seed, SeedError> {
let entropy = seed_to_bytes(lang, &words)?; let entropy = seed_to_bytes(lang, &words)?;
// Make sure this is a valid scalar // Make sure this is a valid scalar
@@ -295,17 +320,20 @@ impl ClassicSeed {
Ok(Self::from_entropy(lang, entropy).unwrap()) Ok(Self::from_entropy(lang, entropy).unwrap())
} }
/// Create a seed from entropy.
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option<ClassicSeed> { pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option<Seed> {
Option::from(Scalar::from_canonical_bytes(*entropy)) Option::from(Scalar::from_canonical_bytes(*entropy))
.map(|scalar| key_to_seed(lang, Zeroizing::new(scalar))) .map(|scalar| key_to_seed(lang, Zeroizing::new(scalar)))
} }
pub(crate) fn to_string(&self) -> Zeroizing<String> { /// Convert a seed to a string.
pub fn to_string(&self) -> Zeroizing<String> {
self.1.clone() self.1.clone()
} }
pub(crate) fn entropy(&self) -> Zeroizing<[u8; 32]> { /// Return the entropy underlying this seed.
pub fn entropy(&self) -> Zeroizing<[u8; 32]> {
seed_to_bytes(self.0, &self.1).unwrap() seed_to_bytes(self.0, &self.1).unwrap()
} }
} }

View File

@@ -1,4 +1,4 @@
vec![ &[
"like", "like",
"just", "just",
"love", "love",

View File

@@ -1,4 +1,4 @@
vec![ &[
"Abakus", "Abakus",
"Abart", "Abart",
"abbilden", "abbilden",

View File

@@ -1,4 +1,4 @@
vec![ &[
"abbey", "abbey",
"abducts", "abducts",
"ability", "ability",

View File

@@ -1,4 +1,4 @@
vec![ &[
"abako", "abako",
"abdiki", "abdiki",
"abelo", "abelo",

View File

@@ -1,4 +1,4 @@
vec![ &[
"ábaco", "ábaco",
"abdomen", "abdomen",
"abeja", "abeja",

View File

@@ -1,4 +1,4 @@
vec![ &[
"abandon", "abandon",
"abattre", "abattre",
"aboi", "aboi",

View File

@@ -1,4 +1,4 @@
vec![ &[
"abbinare", "abbinare",
"abbonato", "abbonato",
"abisso", "abisso",

View File

@@ -1,4 +1,4 @@
vec![ &[
"あいこくしん", "あいこくしん",
"あいさつ", "あいさつ",
"あいだ", "あいだ",

View File

@@ -1,4 +1,4 @@
vec![ &[
"backi", "backi",
"bacru", "bacru",
"badna", "badna",

View File

@@ -1,4 +1,4 @@
vec![ &[
"aalglad", "aalglad",
"aalscholver", "aalscholver",
"aambeeld", "aambeeld",

View File

@@ -1,4 +1,4 @@
vec![ &[
"abaular", "abaular",
"abdominal", "abdominal",
"abeto", "abeto",

View File

@@ -1,4 +1,4 @@
vec![ &[
"абажур", "абажур",
"абзац", "абзац",
"абонент", "абонент",

View File

@@ -1,4 +1,4 @@
vec![ &[
"", "",
"", "",
"", "",

View File

@@ -4,7 +4,7 @@ use std_shims::string::String;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
use rand_core::{RngCore, CryptoRng}; use rand_core::{RngCore, CryptoRng};
pub(crate) mod classic; pub(crate) use monero_seed as classic;
pub(crate) use polyseed; pub(crate) use polyseed;
use classic::{CLASSIC_SEED_LENGTH, CLASSIC_SEED_LENGTH_WITH_CHECKSUM, ClassicSeed}; use classic::{CLASSIC_SEED_LENGTH, CLASSIC_SEED_LENGTH_WITH_CHECKSUM, ClassicSeed};
use polyseed::{POLYSEED_LENGTH, Polyseed}; use polyseed::{POLYSEED_LENGTH, Polyseed};

View File

@@ -6,11 +6,7 @@ use curve25519_dalek::scalar::Scalar;
use monero_serai::primitives::keccak256; use monero_serai::primitives::keccak256;
use crate::seed::{ use crate::seed::{Seed, SeedType, SeedError, classic, polyseed};
Seed, SeedType, SeedError,
classic::{self, trim_by_lang},
polyseed,
};
#[test] #[test]
fn test_classic_seed() { fn test_classic_seed() {
@@ -186,6 +182,14 @@ fn test_classic_seed() {
]; ];
for vector in vectors { for vector in vectors {
fn trim_by_lang(word: &str, lang: Language) -> String {
if lang != Language::DeprecatedEnglish {
word.chars().take(LANGUAGES()[&lang].unique_prefix_length).collect()
} else {
word.to_string()
}
}
let trim_seed = |seed: &str| { let trim_seed = |seed: &str| {
seed seed
.split_whitespace() .split_whitespace()

View File

@@ -43,4 +43,6 @@ monero-clsag = { path = "../../coins/monero/ringct/clsag", default-features = fa
monero-bulletproofs = { path = "../../coins/monero/ringct/bulletproofs", default-features = false } monero-bulletproofs = { path = "../../coins/monero/ringct/bulletproofs", default-features = false }
monero-serai = { path = "../../coins/monero", default-features = false } monero-serai = { path = "../../coins/monero", default-features = false }
monero-rpc = { path = "../../coins/monero/rpc", default-features = false } monero-rpc = { path = "../../coins/monero/rpc", default-features = false }
monero-seed = { path = "../../coins/monero/wallet/seed", default-features = false }
monero-polyseed = { path = "../../coins/monero/wallet/polyseed", default-features = false }
monero-wallet = { path = "../../coins/monero/wallet", default-features = false } monero-wallet = { path = "../../coins/monero/wallet", default-features = false }