240 lines
5.9 KiB
Rust
240 lines
5.9 KiB
Rust
use std::{borrow::Borrow, fmt::Display, str::FromStr};
|
|
|
|
use chacha20::cipher::{generic_array::GenericArray, ArrayLength};
|
|
use rand::{distributions, Rng};
|
|
use sha3::{Digest, Sha3_256};
|
|
|
|
use crate::{error::Error, PHRASE_LENGTH, SALT_LENGTH};
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub(crate) struct Phrase(String);
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
pub(crate) struct Id(String);
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub(crate) struct IdSalt(Vec<u8>);
|
|
#[derive(Debug, PartialEq)]
|
|
pub(crate) struct Key(Vec<u8>);
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub(crate) struct KeySalt(Vec<u8>);
|
|
#[derive(Debug, PartialEq)]
|
|
pub(crate) struct Nonce(Vec<u8>);
|
|
|
|
impl Phrase {
|
|
pub(crate) fn random() -> Self {
|
|
let phrase = rand::thread_rng()
|
|
.sample_iter(distributions::Alphanumeric)
|
|
.take(PHRASE_LENGTH)
|
|
.map(char::from)
|
|
.collect::<String>();
|
|
Self(phrase)
|
|
}
|
|
}
|
|
impl FromStr for Phrase {
|
|
type Err = Error;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
if s.chars().any(|x| !x.is_ascii_alphanumeric() && x != '.') {
|
|
Err(Error::PhraseInvalid)
|
|
} else {
|
|
Ok(Self(
|
|
s.chars().take(s.find('.').unwrap_or(s.len())).collect(),
|
|
))
|
|
}
|
|
}
|
|
}
|
|
impl Display for Phrase {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
|
|
impl Id {
|
|
pub(crate) fn from_phrase(phrase: &Phrase, salt: &IdSalt) -> Self {
|
|
let mut hasher = Sha3_256::new();
|
|
hasher.update(&phrase.0);
|
|
hasher.update(&salt.0);
|
|
|
|
let id = hex::encode(hasher.finalize());
|
|
Self(id)
|
|
}
|
|
pub(crate) fn from_str(s: &str) -> Self {
|
|
Self(s.to_string())
|
|
}
|
|
pub(crate) fn raw(&self) -> &str {
|
|
&self.0
|
|
}
|
|
}
|
|
impl Display for Id {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.0.fmt(f)
|
|
}
|
|
}
|
|
|
|
impl IdSalt {
|
|
pub(crate) fn random() -> Self {
|
|
let salt = rand::thread_rng()
|
|
.sample_iter(distributions::Standard)
|
|
.take(SALT_LENGTH)
|
|
.collect();
|
|
Self(salt)
|
|
}
|
|
pub(crate) fn raw(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
}
|
|
impl FromStr for IdSalt {
|
|
type Err = hex::FromHexError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(Self(hex::decode(s)?))
|
|
}
|
|
}
|
|
|
|
impl Key {
|
|
pub(crate) fn from_phrase(phrase: &Phrase, salt: &KeySalt) -> Self {
|
|
let mut hasher = Sha3_256::new();
|
|
hasher.update(&phrase.0);
|
|
hasher.update(&salt.0);
|
|
Self(hasher.finalize().to_vec())
|
|
}
|
|
}
|
|
|
|
impl<L: ArrayLength<u8>> Borrow<GenericArray<u8, L>> for Key {
|
|
fn borrow(&self) -> &GenericArray<u8, L> {
|
|
GenericArray::<u8, L>::from_slice(&self.0)
|
|
}
|
|
}
|
|
|
|
impl KeySalt {
|
|
pub(crate) fn random() -> Self {
|
|
let salt = rand::thread_rng()
|
|
.sample_iter(distributions::Standard)
|
|
.take(SALT_LENGTH)
|
|
.collect();
|
|
Self(salt)
|
|
}
|
|
pub(crate) fn raw(&self) -> &[u8] {
|
|
&self.0
|
|
}
|
|
}
|
|
impl FromStr for KeySalt {
|
|
type Err = hex::FromHexError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
Ok(Self(hex::decode(s)?))
|
|
}
|
|
}
|
|
|
|
impl Nonce {
|
|
pub(crate) fn random() -> Self {
|
|
// generate a 12 byte / 96 bit nonce for chacha20 as defined in rfc7539
|
|
let nonce = rand::thread_rng()
|
|
.sample_iter(distributions::Standard)
|
|
.take(12)
|
|
.collect();
|
|
Self(nonce)
|
|
}
|
|
pub(crate) fn from_hex(hex_value: &str) -> Result<Self, Error> {
|
|
Ok(Self(hex::decode(hex_value)?))
|
|
}
|
|
pub(crate) fn to_hex(&self) -> String {
|
|
hex::encode(&self.0)
|
|
}
|
|
}
|
|
|
|
impl<L: ArrayLength<u8>> Borrow<GenericArray<u8, L>> for Nonce {
|
|
fn borrow(&self) -> &GenericArray<u8, L> {
|
|
GenericArray::from_slice(&self.0)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use crate::{
|
|
util::{Id, IdSalt, Key, KeySalt, Nonce, Phrase},
|
|
PHRASE_LENGTH, SALT_LENGTH,
|
|
};
|
|
|
|
#[test]
|
|
fn phrase() {
|
|
assert_eq!(PHRASE_LENGTH, Phrase::random().0.len());
|
|
assert_ne!(Phrase::random(), Phrase::random());
|
|
}
|
|
|
|
#[test]
|
|
fn id() {
|
|
let phrase = Phrase::random();
|
|
let salt = IdSalt::random();
|
|
let phrase2 = Phrase::random();
|
|
let salt2 = IdSalt::random();
|
|
|
|
assert_eq!(
|
|
Id::from_phrase(&phrase, &salt),
|
|
Id::from_phrase(&phrase, &salt)
|
|
);
|
|
|
|
assert_ne!(
|
|
Id::from_phrase(&phrase, &salt),
|
|
Id::from_phrase(&phrase, &salt2)
|
|
);
|
|
|
|
assert_ne!(
|
|
Id::from_phrase(&phrase, &salt),
|
|
Id::from_phrase(&phrase2, &salt)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn key() {
|
|
let phrase = Phrase::random();
|
|
let salt = KeySalt::random();
|
|
let phrase2 = Phrase::random();
|
|
let salt2 = KeySalt::random();
|
|
|
|
assert_eq!(
|
|
Key::from_phrase(&phrase, &salt),
|
|
Key::from_phrase(&phrase, &salt)
|
|
);
|
|
|
|
assert_ne!(
|
|
Key::from_phrase(&phrase, &salt),
|
|
Key::from_phrase(&phrase, &salt2)
|
|
);
|
|
|
|
assert_ne!(
|
|
Key::from_phrase(&phrase, &salt),
|
|
Key::from_phrase(&phrase2, &salt)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[allow(clippy::unwrap_used)]
|
|
fn key_id_collision() {
|
|
let phrase = Phrase::random();
|
|
let id_salt = IdSalt::random();
|
|
let key_salt = KeySalt::random();
|
|
|
|
assert_ne!(
|
|
hex::decode(Id::from_phrase(&phrase, &id_salt).0).unwrap(),
|
|
Key::from_phrase(&phrase, &key_salt).0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn id_salt() {
|
|
assert_eq!(SALT_LENGTH, IdSalt::random().0.len());
|
|
assert_ne!(IdSalt::random(), IdSalt::random());
|
|
}
|
|
|
|
#[test]
|
|
fn key_salt() {
|
|
assert_eq!(SALT_LENGTH, KeySalt::random().0.len());
|
|
assert_ne!(KeySalt::random(), KeySalt::random());
|
|
}
|
|
|
|
#[test]
|
|
fn nonce() {
|
|
assert_eq!(12, Nonce::random().0.len());
|
|
assert_ne!(Nonce::random(), Nonce::random());
|
|
}
|
|
}
|