Smash out polyseed

This commit is contained in:
Luke Parker
2024-06-23 05:34:41 -04:00
parent df095f027f
commit 1e2e3bd5ce
24 changed files with 210 additions and 115 deletions

19
Cargo.lock generated
View File

@@ -4867,7 +4867,6 @@ name = "monero-serai"
version = "0.1.4-alpha"
dependencies = [
"curve25519-dalek",
"hex",
"hex-literal",
"monero-borromean",
"monero-bulletproofs",
@@ -4877,8 +4876,6 @@ dependencies = [
"monero-mlsag",
"monero-primitives",
"rand_core",
"serde",
"serde_json",
"std-shims",
"zeroize",
]
@@ -4911,16 +4908,13 @@ dependencies = [
"monero-rpc",
"monero-serai",
"monero-simple-request-rpc",
"pbkdf2 0.12.2",
"rand",
"rand_chacha",
"rand_core",
"rand_distr",
"serde",
"serde_json",
"sha3",
"std-shims",
"subtle",
"thiserror",
"tokio",
"zeroize",
@@ -5809,6 +5803,19 @@ dependencies = [
"universal-hash",
]
[[package]]
name = "polyseed"
version = "0.1.0"
dependencies = [
"pbkdf2 0.12.2",
"rand_core",
"sha3",
"std-shims",
"subtle",
"thiserror",
"zeroize",
]
[[package]]
name = "polyval"
version = "0.6.2"

View File

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

View File

@@ -32,9 +32,6 @@ monero-borromean = { path = "ringct/borromean", version = "0.1", default-feature
monero-bulletproofs = { path = "ringct/bulletproofs", version = "0.1", default-features = false }
hex-literal = "0.4"
hex = { version = "0.4", default-features = false, features = ["alloc"] }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
serde_json = { version = "1", default-features = false, features = ["alloc"] }
[features]
std = [
@@ -50,10 +47,6 @@ std = [
"monero-mlsag/std",
"monero-clsag/std",
"monero-bulletproofs/std",
"hex/std",
"serde/std",
"serde_json/std",
]
compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-bulletproofs/compile-time-generators"]

View File

@@ -3,7 +3,15 @@
A modern Monero transaction library. It provides a modern, Rust-friendly view of
the Monero protocol.
### Purpose and support
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Wallet Functionality
monero-serai originally included wallet functionality. That has been moved to
monero-wallet.
### Purpose and Support
monero-serai was written for Serai, a decentralized exchange aiming to support
Monero. Despite this, monero-serai is intended to be a widely usable library,

View File

@@ -22,7 +22,6 @@ async-trait = { version = "0.1", default-features = false }
thiserror = { version = "1", default-features = false, optional = true }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
subtle = { version = "^2.4", default-features = false }
rand_core = { version = "0.6", default-features = false }
# Used to send transactions
@@ -31,9 +30,6 @@ rand_chacha = { version = "0.3", default-features = false }
# Used to select decoys
rand_distr = { version = "0.4", default-features = false }
sha3 = { version = "0.10", default-features = false }
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
group = { version = "0.13", default-features = false }
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "group"] }
@@ -66,16 +62,12 @@ std = [
"thiserror",
"zeroize/std",
"subtle/std",
"rand_core/std",
"rand/std",
"rand_chacha/std",
"rand_distr/std",
"sha3/std",
"pbkdf2/std",
"hex/std",
"base58-monero/std",
"serde/std",

View File

@@ -1,6 +1,45 @@
# Monero Wallet
Wallet functionality for the Monero protocol, built around monero-serai.
Wallet functionality for the Monero protocol, built around monero-serai. This
library prides itself on resolving common pit falls developers may face.
monero-wallet also offers a FROST-inspired multisignature protocol orders of
magnitude more performant than Monero's own.
This library is usable under no-std when the `std` feature (on by default) is
disabled.
### Features
- Scanning Monero transactions
- Sending Monero transactions
- Sending Monero transactions with a FROST-inspired threshold multisignature
protocol, orders of magnitude more performant than Monero's own.
### Caveats
This library DOES attempt to do the following:
- Create on-chain transactions identical to how wallet2 would (unless told not
to)
- Not be detectable as monero-serai when scanning outputs
- Not reveal spent outputs to the connected RPC node
This library DOES NOT attempt to do the following:
- Have identical RPC behavior when creating transactions
- Be a wallet
This means that monero-serai shouldn't be fingerprintable on-chain. It also
shouldn't be fingerprintable if a targeted attack occurs to detect if the
receiving wallet is monero-serai or wallet2. It also should be generally safe
for usage with remote nodes.
It won't hide from remote nodes it's monero-serai however, potentially
allowing a remote node to profile you. The implications of this are left to the
user to consider.
It also won't act as a wallet, just as a wallet functionality library. wallet2
has several *non-transaction-level* policies, such as always attempting to use
two inputs to create transactions. These are considered out of scope to
monero-serai.

View File

@@ -1,49 +0,0 @@
# monero-serai
A modern Monero transaction library intended for usage in wallets. It prides
itself on accuracy, correctness, and removing common pit falls developers may
face.
monero-serai also offers the following features:
- Featured Addresses
- A FROST-based multisig orders of magnitude more performant than Monero's
### Purpose and support
monero-serai was written for Serai, a decentralized exchange aiming to support
Monero. Despite this, monero-serai is intended to be a widely usable library,
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
yet will not deprive functionality from other users.
Various legacy transaction formats are not currently implemented, yet we are
willing to add support for them. There aren't active development efforts around
them however.
### Caveats
This library DOES attempt to do the following:
- Create on-chain transactions identical to how wallet2 would (unless told not
to)
- Not be detectable as monero-serai when scanning outputs
- Not reveal spent outputs to the connected RPC node
This library DOES NOT attempt to do the following:
- Have identical RPC behavior when creating transactions
- Be a wallet
This means that monero-serai shouldn't be fingerprintable on-chain. It also
shouldn't be fingerprintable if a targeted attack occurs to detect if the
receiving wallet is monero-serai or wallet2. It also should be generally safe
for usage with remote nodes.
It won't hide from remote nodes it's monero-serai however, potentially
allowing a remote node to profile you. The implications of this are left to the
user to consider.
It also won't act as a wallet, just as a transaction library. wallet2 has
several *non-transaction-level* policies, such as always attempting to use two
inputs to create transactions. These are considered out of scope to
monero-serai.

View File

@@ -0,0 +1,44 @@
[package]
name = "polyseed"
version = "0.1.0"
description = "Rust implementation of Polyseed"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/wallet.polyseed"
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 }
subtle = { version = "^2.4", default-features = false }
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
rand_core = { version = "0.6", default-features = false }
sha3 = { version = "0.10", default-features = false }
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
[features]
std = [
"std-shims/std",
"thiserror",
"subtle/std",
"zeroize/std",
"rand_core/std",
"sha3/std",
"pbkdf2/std",
]
default = ["std"]

View File

@@ -0,0 +1,6 @@
# Polyseed
Rust implementation of [Polyseed](https://github.com/tevador/polyseed).
This library is usable under no-std when the `std` feature (on by default) is
disabled.

View File

@@ -1,5 +1,10 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
#![deny(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use core::fmt;
use std_shims::{sync::OnceLock, vec::Vec, string::String, collections::HashMap};
use std_shims::{sync::OnceLock, string::String, collections::HashMap};
#[cfg(feature = "std")]
use std::time::{SystemTime, UNIX_EPOCH};
@@ -10,8 +15,6 @@ use rand_core::{RngCore, CryptoRng};
use sha3::Sha3_256;
use pbkdf2::pbkdf2_hmac;
use super::SeedError;
// Features
const FEATURE_BITS: u8 = 5;
#[allow(dead_code)]
@@ -34,7 +37,7 @@ fn polyseed_features_supported(features: u8) -> bool {
const DATE_BITS: u8 = 10;
const DATE_MASK: u16 = (1u16 << DATE_BITS) - 1;
const POLYSEED_EPOCH: u64 = 1635768000; // 1st November 2021 12:00 UTC
pub(crate) const TIME_STEP: u64 = 2629746; // 30.436875 days = 1/12 of the Gregorian year
const TIME_STEP: u64 = 2629746; // 30.436875 days = 1/12 of the Gregorian year
// After ~85 years, this will roll over.
fn birthday_encode(time: u64) -> u16 {
@@ -61,9 +64,9 @@ const LAST_BYTE_SECRET_BITS_MASK: u8 = ((1 << (BITS_PER_BYTE - CLEAR_BITS)) - 1)
const SECRET_BITS_PER_WORD: usize = 10;
// Amount of words in a seed
pub(crate) const POLYSEED_LENGTH: usize = 16;
const POLYSEED_LENGTH: usize = 16;
// Amount of characters each word must have if trimmed
pub(crate) const PREFIX_LEN: usize = 4;
const PREFIX_LEN: usize = 4;
const POLY_NUM_CHECK_DIGITS: usize = 1;
const DATA_WORDS: usize = POLYSEED_LENGTH - POLY_NUM_CHECK_DIGITS;
@@ -98,30 +101,58 @@ const POLYSEED_KEYGEN_ITERATIONS: u32 = 10000;
// See: https://github.com/tevador/polyseed/blob/master/include/polyseed.h#L58
const COIN: u16 = 0;
/// An error when working with a Polyseed.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "std", derive(thiserror::Error))]
pub enum PolyseedError {
/// Unsupported feature bits were set.
#[cfg_attr(feature = "std", error("unsupported features"))]
UnsupportedFeatures,
/// The entropy was invalid.
#[cfg_attr(feature = "std", error("invalid entropy"))]
InvalidEntropy,
#[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 for Polyseed.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize)]
pub enum Language {
/// English language option.
English,
/// Spanish language option.
Spanish,
/// French language option.
French,
/// Italian language option.
Italian,
/// Japanese language option.
Japanese,
/// Korean language option.
Korean,
/// Czech language option.
Czech,
/// Portuguese language option.
Portuguese,
/// Simplified Chinese language option.
ChineseSimplified,
/// Traditional Chinese language option.
ChineseTraditional,
}
struct WordList {
words: Vec<String>,
words: &'static [&'static str],
has_prefix: bool,
has_accent: bool,
}
impl WordList {
fn new(words: &str, has_prefix: bool, has_accent: bool) -> WordList {
let res = WordList { words: serde_json::from_str(words).unwrap(), has_prefix, has_accent };
fn new(words: &'static [&'static str], has_prefix: bool, has_accent: bool) -> WordList {
let res = WordList { words, has_prefix, has_accent };
// This is needed for a later unwrap to not fails
assert!(words.len() < usize::from(u16::MAX));
res
@@ -133,26 +164,27 @@ static LANGUAGES_CELL: OnceLock<HashMap<Language, WordList>> = OnceLock::new();
fn LANGUAGES() -> &'static HashMap<Language, WordList> {
LANGUAGES_CELL.get_or_init(|| {
HashMap::from([
(Language::Czech, WordList::new(include_str!("./polyseed/cs.json"), true, false)),
(Language::French, WordList::new(include_str!("./polyseed/fr.json"), true, true)),
(Language::Korean, WordList::new(include_str!("./polyseed/ko.json"), false, false)),
(Language::English, WordList::new(include_str!("./polyseed/en.json"), true, false)),
(Language::Italian, WordList::new(include_str!("./polyseed/it.json"), true, false)),
(Language::Spanish, WordList::new(include_str!("./polyseed/es.json"), true, true)),
(Language::Japanese, WordList::new(include_str!("./polyseed/ja.json"), false, false)),
(Language::Portuguese, WordList::new(include_str!("./polyseed/pt.json"), true, false)),
(Language::Czech, WordList::new(include!("./words/cs.rs"), true, false)),
(Language::French, WordList::new(include!("./words/fr.rs"), true, true)),
(Language::Korean, WordList::new(include!("./words/ko.rs"), false, false)),
(Language::English, WordList::new(include!("./words/en.rs"), true, false)),
(Language::Italian, WordList::new(include!("./words/it.rs"), true, false)),
(Language::Spanish, WordList::new(include!("./words/es.rs"), true, true)),
(Language::Japanese, WordList::new(include!("./words/ja.rs"), false, false)),
(Language::Portuguese, WordList::new(include!("./words/pt.rs"), true, false)),
(
Language::ChineseSimplified,
WordList::new(include_str!("./polyseed/zh_simplified.json"), false, false),
WordList::new(include!("./words/zh_simplified.rs"), false, false),
),
(
Language::ChineseTraditional,
WordList::new(include_str!("./polyseed/zh_traditional.json"), false, false),
WordList::new(include!("./words/zh_traditional.rs"), false, false),
),
])
})
}
/// A Polyseed.
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
pub struct Polyseed {
language: Language,
@@ -222,13 +254,13 @@ impl Polyseed {
masked_features: u8,
encoded_birthday: u16,
entropy: Zeroizing<[u8; 32]>,
) -> Result<Polyseed, SeedError> {
) -> Result<Polyseed, PolyseedError> {
if !polyseed_features_supported(masked_features) {
Err(SeedError::UnsupportedFeatures)?;
Err(PolyseedError::UnsupportedFeatures)?;
}
if !valid_entropy(&entropy) {
Err(SeedError::InvalidEntropy)?;
Err(PolyseedError::InvalidEntropy)?;
}
let mut res = Polyseed {
@@ -244,23 +276,24 @@ impl Polyseed {
/// Create a new `Polyseed` with specific internals.
///
/// `birthday` is defined in seconds since the Unix epoch.
pub fn from(
/// `birthday` is defined in seconds since the epoch.
fn from(
language: Language,
features: u8,
birthday: u64,
entropy: Zeroizing<[u8; 32]>,
) -> Result<Polyseed, SeedError> {
) -> Result<Polyseed, PolyseedError> {
Self::from_internal(language, user_features(features), birthday_encode(birthday), entropy)
}
/// Create a new `Polyseed`.
///
/// This uses the system's time for the birthday, if available.
/// This uses the system's time for the birthday, if available, else 0.
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, language: Language) -> Polyseed {
// Get the birthday
#[cfg(feature = "std")]
let birthday = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let birthday =
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(core::time::Duration::ZERO).as_secs();
#[cfg(not(feature = "std"))]
let birthday = 0;
@@ -275,7 +308,7 @@ impl Polyseed {
/// Create a new `Polyseed` from a String.
#[allow(clippy::needless_pass_by_value)]
pub fn from_string(lang: Language, seed: Zeroizing<String>) -> Result<Polyseed, SeedError> {
pub fn from_string(lang: Language, seed: Zeroizing<String>) -> Result<Polyseed, PolyseedError> {
// Decode the seed into its polynomial coefficients
let mut poly = [0; POLYSEED_LENGTH];
@@ -325,7 +358,7 @@ impl Polyseed {
} else {
check_if_matches(lang_word_list.has_prefix, lang_word_list.words.iter(), word)
}) else {
Err(SeedError::InvalidSeed)?
Err(PolyseedError::InvalidSeed)?
};
// WordList asserts the word list length is less than u16::MAX
@@ -337,7 +370,7 @@ impl Polyseed {
// Validate the checksum
if poly_eval(&poly) != 0 {
Err(SeedError::InvalidChecksum)?;
Err(PolyseedError::InvalidChecksum)?;
}
// Convert the polynomial into entropy
@@ -416,6 +449,7 @@ impl Polyseed {
key
}
/// The String representation of this seed.
pub fn to_string(&self) -> Zeroizing<String> {
// Encode the polynomial with the existing checksum
let mut poly = self.to_poly();
@@ -428,7 +462,7 @@ impl Polyseed {
let mut seed = Zeroizing::new(String::new());
let words = &LANGUAGES()[&self.language].words;
for i in 0 .. poly.len() {
seed.push_str(&words[usize::from(poly[i])]);
seed.push_str(words[usize::from(poly[i])]);
if i < poly.len() - 1 {
seed.push(' ');
}

View File

@@ -1,4 +1,4 @@
[
&[
"abdikace",
"abeceda",
"adresa",
@@ -2047,4 +2047,4 @@
"zvrat",
"zvukovod",
"zvyk"
]
]

View File

@@ -1,4 +1,4 @@
[
&[
"abandon",
"ability",
"able",

View File

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

View File

@@ -1,4 +1,4 @@
[
&[
"abaisser",
"abandon",
"abdiquer",

View File

@@ -1,4 +1,4 @@
[
&[
"abaco",
"abbaglio",
"abbinato",

View File

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

View File

@@ -1,4 +1,4 @@
[
&[
"가격",
"가끔",
"가난",

View File

@@ -1,4 +1,4 @@
[
&[
"abacate",
"abaixo",
"abalar",

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
# Monero Wallet Utilities
Additional utility functions for monero-wallet.
This library is isolated as it adds a notable amount of dependencies to the
tree, and to be a subject to a distinct versioning policy. This library may
more frequently undergo breaking API changes.
### Features
- Support for Monero's seed algorithm
- Support for Polyseed

View File

@@ -0,0 +1,2 @@
pub use monero_seed as seed;
pub use monero_polyseed as seed;