use std::fmt;
use rand::rngs::SysRng;
use rand::TryRng;
use secrecy::{ExposeSecret, SecretBox};
use zeroize::Zeroize;
use crate::error::SecretError;
pub const MASTER_KEY_LEN: usize = 32;
pub struct MasterKey {
inner: SecretBox<[u8; MASTER_KEY_LEN]>,
}
impl fmt::Debug for MasterKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MasterKey").finish_non_exhaustive()
}
}
impl MasterKey {
pub fn generate() -> Result<Self, SecretError> {
let mut bytes = [0_u8; MASTER_KEY_LEN];
SysRng
.try_fill_bytes(&mut bytes)
.map_err(|e| SecretError::Backend(format!("OS RNG: {e}")))?;
Ok(Self::from_bytes(bytes))
}
#[must_use]
pub fn from_bytes(bytes: [u8; MASTER_KEY_LEN]) -> Self {
Self {
inner: SecretBox::new(Box::new(bytes)),
}
}
#[must_use]
pub const fn bytes(&self) -> &SecretBox<[u8; MASTER_KEY_LEN]> {
&self.inner
}
#[must_use]
pub fn to_hex_secret(&self) -> crate::crypto::SecretString {
const HEX: &[u8; 16] = b"0123456789abcdef";
let bytes = self.inner.expose_secret();
let mut buf: Vec<u8> = vec![0_u8; MASTER_KEY_LEN * 2];
for (i, b) in bytes.iter().enumerate() {
buf[2 * i] = HEX[(b >> 4) as usize];
buf[2 * i + 1] = HEX[(b & 0x0F) as usize];
}
let boxed: Box<str> = {
#[allow(clippy::expect_used)]
let hex_str: &str = std::str::from_utf8(&buf).expect("hex characters are always ASCII");
Box::<str>::from(hex_str)
};
buf.zeroize();
crate::crypto::SecretString::from(boxed)
}
#[must_use]
pub fn ct_eq(&self, other: &Self) -> bool {
let a = self.inner.expose_secret();
let b = other.inner.expose_secret();
let mut diff: u8 = 0;
for (x, y) in a.iter().zip(b.iter()) {
diff |= x ^ y;
}
std::hint::black_box(diff) == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_produces_correct_length() {
let k = MasterKey::generate().expect("OS RNG");
assert_eq!(k.bytes().expose_secret().len(), MASTER_KEY_LEN);
}
#[test]
fn two_generated_keys_are_distinct() {
let a = MasterKey::generate().expect("OS RNG");
let b = MasterKey::generate().expect("OS RNG");
assert_ne!(a.bytes().expose_secret(), b.bytes().expose_secret());
}
#[test]
fn from_bytes_roundtrips() {
let bytes = [0xA5_u8; MASTER_KEY_LEN];
let k = MasterKey::from_bytes(bytes);
assert_eq!(k.bytes().expose_secret(), &bytes);
}
#[test]
fn to_hex_secret_has_double_length() {
let bytes = [0xAB_u8; MASTER_KEY_LEN];
let k = MasterKey::from_bytes(bytes);
let hex = k.to_hex_secret();
assert_eq!(hex.expose_secret().len(), MASTER_KEY_LEN * 2);
assert_eq!(hex.expose_secret(), &"ab".repeat(MASTER_KEY_LEN));
}
#[test]
fn to_hex_secret_uses_lowercase_alphabet() {
let bytes = [0xDE_u8; MASTER_KEY_LEN];
let hex = MasterKey::from_bytes(bytes).to_hex_secret();
assert!(hex
.expose_secret()
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit()));
}
#[test]
fn debug_redacts_key_bytes() {
let k = MasterKey::from_bytes([0xCD_u8; MASTER_KEY_LEN]);
let dbg = format!("{k:?}");
assert!(!dbg.contains("cd"));
assert!(!dbg.contains("CD"));
assert!(dbg.contains("MasterKey"));
assert!(dbg.contains(".."));
}
#[test]
fn ct_eq_is_true_for_equal_keys() {
let a = MasterKey::from_bytes([0x12; MASTER_KEY_LEN]);
let b = MasterKey::from_bytes([0x12; MASTER_KEY_LEN]);
assert!(a.ct_eq(&b));
}
#[test]
fn ct_eq_is_false_for_keys_that_differ_only_in_last_byte() {
let a = MasterKey::from_bytes([0x12; MASTER_KEY_LEN]);
let mut diff = [0x12_u8; MASTER_KEY_LEN];
diff[MASTER_KEY_LEN - 1] = 0x13;
let b = MasterKey::from_bytes(diff);
assert!(!a.ct_eq(&b));
}
#[test]
fn ct_eq_is_false_for_keys_that_differ_only_in_first_byte() {
let a = MasterKey::from_bytes([0x12; MASTER_KEY_LEN]);
let mut diff = [0x12_u8; MASTER_KEY_LEN];
diff[0] = 0x13;
let b = MasterKey::from_bytes(diff);
assert!(!a.ct_eq(&b));
}
}