#![cfg_attr(not(feature = "std"), no_std)]
#![doc(html_root_url = "https://docs.rs/secret-tree/0.6.0")]
#![warn(missing_docs, missing_debug_implementations)]
#![warn(clippy::all, clippy::pedantic)]
#![allow(
clippy::missing_errors_doc,
clippy::must_use_candidate,
clippy::module_name_repetitions
)]
#[cfg(all(not(feature = "std"), test))]
extern crate std;
use core::{
array::TryFromSliceError,
convert::TryInto,
fmt,
str::{self, FromStr},
};
use rand_chacha::ChaChaRng;
use rand_core::{CryptoRng, RngCore, SeedableRng};
use secrecy::{zeroize::Zeroize, CloneableSecret, ExposeSecret, SecretBox};
use crate::kdf::{derive_key, try_derive_key, Index, CONTEXT_LEN, SALT_LEN};
pub use crate::{byte_slice::AsByteSliceMut, kdf::SEED_LEN};
mod byte_slice;
mod kdf;
pub const MAX_NAME_LEN: usize = SALT_LEN;
#[derive(Debug, Clone, Default)]
struct SeedBytes([u8; SEED_LEN]);
impl Zeroize for SeedBytes {
fn zeroize(&mut self) {
self.0.zeroize();
}
}
impl CloneableSecret for SeedBytes {}
#[derive(Debug, Clone)]
pub struct Seed(SecretBox<SeedBytes>);
impl Seed {
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
Self(SecretBox::<SeedBytes>::init_with_mut(|seed| {
rng.fill_bytes(&mut seed.0);
}))
}
fn init_with(init_fn: impl FnOnce(&mut [u8; SEED_LEN])) -> Self {
Self(SecretBox::<SeedBytes>::init_with_mut(|seed_bytes| {
init_fn(&mut seed_bytes.0);
}))
}
pub fn expose_secret(&self) -> &[u8; SEED_LEN] {
&self.0.expose_secret().0
}
}
impl From<&[u8; SEED_LEN]> for Seed {
fn from(bytes: &[u8; SEED_LEN]) -> Self {
Self::init_with(|seed_bytes| {
*seed_bytes = *bytes;
})
}
}
#[derive(Debug)]
#[must_use = "A tree should generate a secret or child tree"]
pub struct SecretTree {
seed: Seed,
}
impl SecretTree {
const FILL_BYTES_CONTEXT: [u8; CONTEXT_LEN] = *b"bytes\0\0\0";
const RNG_CONTEXT: [u8; CONTEXT_LEN] = *b"rng\0\0\0\0\0";
const NAME_CONTEXT: [u8; CONTEXT_LEN] = *b"name\0\0\0\0";
const INDEX_CONTEXT: [u8; CONTEXT_LEN] = *b"index\0\0\0";
const DIGEST_START_CONTEXT: [u8; CONTEXT_LEN] = *b"digest0\0";
const DIGEST_END_CONTEXT: [u8; CONTEXT_LEN] = *b"digest1\0";
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
Self {
seed: Seed::new(rng),
}
}
pub fn from_seed(seed: Seed) -> Self {
Self { seed }
}
pub fn from_slice(bytes: &[u8]) -> Result<Self, TryFromSliceError> {
let seed_ref: &[u8; 32] = bytes.try_into()?;
Ok(Self {
seed: seed_ref.into(),
})
}
pub fn seed(&self) -> &Seed {
&self.seed
}
pub fn rng(self) -> ChaChaRng {
let mut seed = <ChaChaRng as SeedableRng>::Seed::default();
derive_key(
seed.as_mut(),
Index::None,
Self::RNG_CONTEXT,
self.seed.expose_secret(),
);
ChaChaRng::from_seed(seed)
}
pub fn try_fill<T: AsByteSliceMut + ?Sized>(self, dest: &mut T) -> Result<(), FillError> {
try_derive_key(
dest.as_byte_slice_mut(),
Index::None,
Self::FILL_BYTES_CONTEXT,
self.seed.expose_secret(),
)?;
dest.convert_to_le();
Ok(())
}
pub fn fill<T: AsByteSliceMut + ?Sized>(self, dest: &mut T) {
self.try_fill(dest).unwrap_or_else(|err| {
panic!("Failed filling a buffer from `SecretTree`: {err}");
});
}
pub fn try_create_secret<T>(self) -> Result<SecretBox<T>, FillError>
where
T: AsByteSliceMut + Default + Zeroize,
{
let mut result = Ok(());
let secret = SecretBox::init_with_mut(|secret_value| {
result = self.try_fill(secret_value);
});
result?;
Ok(secret)
}
pub fn create_secret<T>(self) -> SecretBox<T>
where
T: AsByteSliceMut + Default + Zeroize,
{
self.try_create_secret().unwrap_or_else(|err| {
panic!("Failed creating a secret from `SecretTree`: {err}");
})
}
pub fn child(&self, name: Name) -> Self {
Self::from_seed(Seed::init_with(|child_seed| {
derive_key(
child_seed,
Index::Bytes(name.0),
Self::NAME_CONTEXT,
self.seed.expose_secret(),
);
}))
}
pub fn index(&self, index: u64) -> Self {
Self::from_seed(Seed::init_with(|child_seed| {
derive_key(
child_seed,
Index::Number(index),
Self::INDEX_CONTEXT,
self.seed.expose_secret(),
);
}))
}
pub fn digest(&self, digest: &[u8; 32]) -> Self {
let mut first_half_of_digest = [0_u8; SALT_LEN];
first_half_of_digest.copy_from_slice(&digest[0..SALT_LEN]);
let mut second_half_of_digest = [0_u8; SALT_LEN];
second_half_of_digest.copy_from_slice(&digest[SALT_LEN..]);
let intermediate_seed = Seed::init_with(|intermediate_seed| {
derive_key(
intermediate_seed,
Index::Bytes(first_half_of_digest),
Self::DIGEST_START_CONTEXT,
self.seed.expose_secret(),
);
});
Self::from_seed(Seed::init_with(|child_seed| {
derive_key(
child_seed,
Index::Bytes(second_half_of_digest),
Self::DIGEST_END_CONTEXT,
intermediate_seed.expose_secret(),
);
}))
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum FillError {
BufferTooSmall {
size: usize,
min_supported_size: usize,
},
BufferTooLarge {
size: usize,
max_supported_size: usize,
},
}
impl fmt::Display for FillError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BufferTooSmall {
size,
min_supported_size,
} => {
write!(
formatter,
"supplied buffer ({size} bytes) is too small to be filled; \
min supported size is {min_supported_size} bytes"
)
}
Self::BufferTooLarge {
size,
max_supported_size,
} => {
write!(
formatter,
"supplied buffer ({size} bytes) is too large to be filled; \
max supported size is {max_supported_size} bytes"
)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for FillError {}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Name([u8; SALT_LEN]);
impl Name {
pub const fn new(name: &str) -> Self {
let bytes = name.as_bytes();
assert!(
bytes.len() <= SALT_LEN,
"name is too long (should be <=16 bytes)"
);
let mut i = 0;
let mut buffer = [0_u8; SALT_LEN];
while i < name.len() {
assert!(bytes[i] != 0, "name contains a null char");
buffer[i] = bytes[i];
i += 1;
}
Name(buffer)
}
}
impl FromStr for Name {
type Err = NameError;
fn from_str(name: &str) -> Result<Self, Self::Err> {
let byte_len = name.len();
if byte_len > SALT_LEN {
return Err(NameError::TooLong);
}
if name.as_bytes().contains(&0) {
return Err(NameError::NullChar);
}
let mut bytes = [0; SALT_LEN];
bytes[..byte_len].copy_from_slice(name.as_bytes());
Ok(Self(bytes))
}
}
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
let str_len = self.0.iter().position(|&ch| ch == 0).unwrap_or(SALT_LEN);
unsafe {
str::from_utf8_unchecked(&self.0[..str_len])
}
}
}
impl fmt::Debug for Name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_tuple("Name").field(&self.as_ref()).finish()
}
}
impl fmt::Display for Name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_ref())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum NameError {
TooLong,
NullChar,
}
impl fmt::Display for NameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::TooLong => "name is too long, 0..=16 bytes expected",
Self::NullChar => "name contains a null char",
})
}
}
#[cfg(feature = "std")]
impl std::error::Error for NameError {}
#[cfg(doctest)]
doc_comment::doctest!("../README.md");
#[cfg(test)]
mod tests {
use rand::{Rng, SeedableRng};
use super::*;
#[test]
fn children_with_same_bytes_in_key() {
let name = Name::new("A");
let index = u64::from(b'A');
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let named_child = tree.child(name);
let indexed_child = tree.index(index);
assert_ne!(
named_child.seed.expose_secret(),
indexed_child.seed.expose_secret()
);
}
#[test]
fn fill_and_rng_result_in_different_data() {
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let mut buffer = [0_u64; 8];
tree.child(Name::new("foo")).fill(&mut buffer);
let other_buffer: [u64; 8] = tree.child(Name::new("foo")).rng().random();
assert_ne!(buffer, other_buffer);
}
#[test]
#[should_panic(expected = "supplied buffer (12 bytes) is too small to be filled")]
fn filling_undersized_key() {
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let mut buffer = [0_u8; 12];
tree.fill(&mut buffer);
}
#[test]
fn error_filling_undersized_key() {
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let mut buffer = [0_u8; 12];
let err = tree.try_fill(&mut buffer).unwrap_err();
assert!(matches!(
err,
FillError::BufferTooSmall {
size: 12,
min_supported_size: 16,
}
));
let err = err.to_string();
assert!(
err.contains("supplied buffer (12 bytes) is too small to be filled"),
"{err}"
);
assert!(err.contains("min supported size is 16 bytes"), "{err}");
}
#[test]
#[should_panic(expected = "supplied buffer (80 bytes) is too large to be filled")]
fn filling_oversized_key() {
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let mut buffer = [0_u64; 10];
tree.fill(&mut buffer);
}
#[test]
fn error_filling_oversized_key() {
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let mut buffer = [0_u64; 10];
let err = tree.try_fill(&mut buffer).unwrap_err();
assert!(matches!(
err,
FillError::BufferTooLarge {
size: 80,
max_supported_size: 64,
}
));
let err = err.to_string();
assert!(
err.contains("supplied buffer (80 bytes) is too large to be filled"),
"{err}"
);
assert!(err.contains("max supported size is 64 bytes"), "{err}");
}
#[test]
fn filling_acceptable_buffers() {
let mut u8_buffer = [0_u8; 40];
let mut i32_buffer = [0_i32; 16];
let mut u128_buffer = [0_u128];
let mut vec_buffer = [0_u16; 24];
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
tree.child(Name::new("u8")).fill(&mut u8_buffer[..]);
tree.child(Name::new("i32")).fill(&mut i32_buffer);
tree.child(Name::new("u128")).fill(&mut u128_buffer);
tree.child(Name::new("vec")).fill(&mut vec_buffer[..]);
}
#[test]
#[should_panic(expected = "name contains a null char")]
fn name_with_null_chars_cannot_be_created() {
let _name = Name::new("some\0name");
}
#[test]
fn name_with_null_chars_error() {
let err = Name::from_str("some\0name").unwrap_err();
assert!(matches!(err, NameError::NullChar));
}
#[test]
#[should_panic(expected = "name is too long")]
fn overly_long_name_cannot_be_created() {
let _name = Name::new("Overly long name?");
}
#[test]
fn overly_long_name_error() {
let err = Name::from_str("Overly long name?").unwrap_err();
assert!(matches!(err, NameError::TooLong));
}
#[test]
fn name_new_pads_input_with_zeros() {
const SAMPLES: &[(Name, &[u8; MAX_NAME_LEN])] = &[
(Name::new(""), b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"),
(Name::new("O"), b"O\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"),
(Name::new("Ov"), b"Ov\0\0\0\0\0\0\0\0\0\0\0\0\0\0"),
(Name::new("Ove"), b"Ove\0\0\0\0\0\0\0\0\0\0\0\0\0"),
(Name::new("Over"), b"Over\0\0\0\0\0\0\0\0\0\0\0\0"),
(Name::new("Overl"), b"Overl\0\0\0\0\0\0\0\0\0\0\0"),
(Name::new("Overly"), b"Overly\0\0\0\0\0\0\0\0\0\0"),
(Name::new("Overly "), b"Overly \0\0\0\0\0\0\0\0\0"),
(Name::new("Overly l"), b"Overly l\0\0\0\0\0\0\0\0"),
(Name::new("Overly lo"), b"Overly lo\0\0\0\0\0\0\0"),
(Name::new("Overly lon"), b"Overly lon\0\0\0\0\0\0"),
(Name::new("Overly long"), b"Overly long\0\0\0\0\0"),
(Name::new("Overly long "), b"Overly long \0\0\0\0"),
(Name::new("Overly long n"), b"Overly long n\0\0\0"),
(Name::new("Overly long na"), b"Overly long na\0\0"),
(Name::new("Overly long nam"), b"Overly long nam\0"),
(Name::new("Overly long name"), b"Overly long name"),
];
for (i, &(name, expected_bytes)) in SAMPLES.iter().enumerate() {
assert_eq!(name.0, *expected_bytes);
let expected_str = &"Overly long name"[..i];
assert_eq!(name.to_string(), expected_str);
assert_eq!(name.as_ref(), expected_str);
assert!(format!("{name:?}").contains(expected_str));
}
}
#[test]
fn buffers_with_different_size_should_be_unrelated() {
let tree = SecretTree::new(&mut ChaChaRng::seed_from_u64(123));
let mut bytes = [0_u8; 16];
tree.child(Name::new("foo")).fill(&mut bytes);
let mut other_bytes = [0_u8; 32];
tree.child(Name::new("foo")).fill(&mut other_bytes);
assert!(bytes.iter().zip(&other_bytes).any(|(&x, &y)| x != y));
}
#[test]
fn digest_derivation_depends_on_all_bits_of_digest() {
const RNG_SEED: u64 = 12345;
let mut rng = ChaChaRng::seed_from_u64(RNG_SEED);
let tree = SecretTree::new(&mut rng);
let mut digest = [0_u8; 32];
rng.fill_bytes(&mut digest);
let child_seed = tree.digest(&digest).seed;
for byte_idx in 0..32 {
for bit_idx in 0..8 {
let mut mutated_digest = digest;
mutated_digest[byte_idx] ^= 1 << bit_idx;
assert_ne!(mutated_digest, digest);
let mutated_child_seed = tree.digest(&mutated_digest).seed;
assert_ne!(
child_seed.expose_secret(),
mutated_child_seed.expose_secret()
);
}
}
}
}