mod bip39_words;
mod mnemonic;
use alloc::string::ToString;
use argon2::{Algorithm as Argon2Algorithm, Argon2, Params, Version};
use rand_core::{OsRng, RngCore};
use zeroize::Zeroizing;
use crate::core::matter::builder::MatterBuilder;
use crate::core::matter::code::NoncerCode;
use crate::core::matter::error::MatterBuildError;
use crate::core::primitives::Noncer;
use crate::crypto::algo::Ed25519;
use crate::crypto::error::SaltError;
use crate::crypto::keypair::KeyPair;
pub const SALT_LEN: usize = 16;
pub const SEED_LEN: usize = 32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tier {
Low,
Medium,
High,
}
impl Tier {
const fn costs(self) -> (u32, u32) {
match self {
Self::Low => (2, 65_536),
Self::Medium => (3, 262_144),
Self::High => (4, 1_048_576),
}
}
}
pub struct Salt {
raw: Zeroizing<[u8; SALT_LEN]>,
}
impl core::fmt::Debug for Salt {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Salt").finish_non_exhaustive()
}
}
impl Salt {
pub fn from_raw(raw: &[u8]) -> Result<Self, SaltError> {
let bytes: [u8; SALT_LEN] = raw
.try_into()
.map_err(|_| SaltError::InvalidLength { actual: raw.len() })?;
Ok(Self {
raw: Zeroizing::new(bytes),
})
}
pub fn from_qb64(qb64: &str) -> Result<Self, SaltError> {
let untyped = MatterBuilder::new().from_qualified_base64(qb64.as_bytes().to_vec())?;
let noncer: Noncer<'_> = untyped.narrow::<NoncerCode>()?;
if *noncer.code() != NoncerCode::Salt128 {
return Err(SaltError::InvalidCode {
actual: alloc::format!("{:?}", noncer.code()),
});
}
Self::from_raw(noncer.raw())
}
pub fn generate() -> Result<Self, SaltError> {
let mut bytes = Zeroizing::new([0u8; SALT_LEN]);
OsRng
.try_fill_bytes(&mut bytes[..])
.map_err(|e| SaltError::Rng(e.to_string()))?;
Ok(Self { raw: bytes })
}
pub fn primitive(&self) -> Result<Noncer<'static>, SaltError> {
Ok(MatterBuilder::new()
.with_code(NoncerCode::Salt128)
.with_raw(self.raw.to_vec())
.map_err(MatterBuildError::from)?
.build()?)
}
pub fn stretch(&self, path: &str, tier: Tier) -> Result<Zeroizing<[u8; SEED_LEN]>, SaltError> {
let (t_cost, m_cost) = tier.costs();
self.stretch_with(path, t_cost, m_cost)
}
#[cfg(feature = "test-utils")]
pub fn stretch_temp(&self, path: &str) -> Result<Zeroizing<[u8; SEED_LEN]>, SaltError> {
self.stretch_with(path, 1, 8)
}
fn stretch_with(
&self,
path: &str,
t_cost: u32,
m_cost: u32,
) -> Result<Zeroizing<[u8; SEED_LEN]>, SaltError> {
let params = Params::new(m_cost, t_cost, 1, Some(SEED_LEN)).map_err(SaltError::Stretch)?;
let ctx = Argon2::new(Argon2Algorithm::Argon2id, Version::V0x13, params);
let mut seed = Zeroizing::new([0u8; SEED_LEN]);
ctx.hash_password_into(path.as_bytes(), &self.raw[..], &mut seed[..])
.map_err(SaltError::Stretch)?;
Ok(seed)
}
pub fn key_pair(&self, path: &str, tier: Tier) -> Result<KeyPair<Ed25519>, SaltError> {
let seed = self.stretch(path, tier)?;
Ok(KeyPair::from_seed_bytes(&seed))
}
#[cfg(feature = "test-utils")]
pub fn key_pair_temp(&self, path: &str) -> Result<KeyPair<Ed25519>, SaltError> {
let seed = self.stretch_temp(path)?;
Ok(KeyPair::from_seed_bytes(&seed))
}
}
#[cfg(test)]
#[allow(
clippy::panic,
clippy::disallowed_methods,
reason = "test assertions use unwrap and panic for clarity"
)]
mod tests {
use super::*;
const RAW: [u8; SALT_LEN] = *b"0123456789abcdef";
#[test]
fn from_raw_accepts_exact_length() {
let salt = Salt::from_raw(&RAW).unwrap();
let qb64 = salt.primitive().unwrap().to_qb64();
assert_eq!(qb64, "0AAwMTIzNDU2Nzg5YWJjZGVm");
}
#[test]
fn from_raw_rejects_wrong_length() {
let err = Salt::from_raw(&RAW[..15]).unwrap_err();
assert!(matches!(err, SaltError::InvalidLength { actual: 15 }));
}
#[test]
fn qb64_round_trips() {
let salt = Salt::from_raw(&RAW).unwrap();
let qb64 = salt.primitive().unwrap().to_qb64();
let back = Salt::from_qb64(&qb64).unwrap();
assert_eq!(back.primitive().unwrap().to_qb64(), qb64);
}
#[test]
fn from_qb64_rejects_non_salt_code() {
let err = Salt::from_qb64("EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA").unwrap_err();
assert!(matches!(
err,
SaltError::Validation(_) | SaltError::InvalidCode { .. }
));
}
#[test]
fn from_qb64_truncated_is_typed_error_not_panic() {
assert!(Salt::from_qb64("0AAwMTIz").is_err());
}
#[test]
fn generate_produces_distinct_salts() {
let a = Salt::generate().unwrap();
let b = Salt::generate().unwrap();
assert_ne!(
a.primitive().unwrap().to_qb64(),
b.primitive().unwrap().to_qb64()
);
}
#[test]
fn stretch_is_deterministic_and_path_sensitive() {
let salt = Salt::from_raw(&RAW).unwrap();
let a = salt.stretch_temp("00").unwrap();
let b = salt.stretch_temp("00").unwrap();
let c = salt.stretch_temp("01").unwrap();
assert_eq!(*a, *b);
assert_ne!(*a, *c);
}
#[test]
fn different_salts_stretch_differently() {
let a = Salt::from_raw(&RAW).unwrap().stretch_temp("00").unwrap();
let b = Salt::from_raw(b"fedcba9876543210")
.unwrap()
.stretch_temp("00")
.unwrap();
assert_ne!(*a, *b);
}
#[test]
fn key_pair_from_same_path_verifies_its_own_signature() {
let salt = Salt::from_raw(&RAW).unwrap();
let kp = salt.key_pair_temp("00").unwrap();
let sig = kp.sign(b"data").unwrap();
kp.verify(b"data", &sig).unwrap();
}
}