use std::fmt;
use crate::error::{Error, Result};
#[derive(Clone, PartialEq, Eq)]
pub struct Pin(Vec<u8>);
impl Pin {
pub const MAX_LEN: usize = 16;
pub fn new(value: impl AsRef<[u8]>) -> Result<Self> {
let value = value.as_ref();
if value.is_empty() {
return Err(Error::InvalidPin("must not be empty"));
}
if value.len() > Self::MAX_LEN {
return Err(Error::InvalidPin("must not exceed 16 bytes"));
}
if !value.iter().all(|b| (0x20..0x7F).contains(b)) {
return Err(Error::InvalidPin("must consist of printable ASCII"));
}
Ok(Pin(value.to_vec()))
}
pub fn numeric(value: impl AsRef<[u8]>) -> Result<Self> {
let pin = Pin::new(value)?;
if !pin.0.iter().all(u8::is_ascii_digit) {
return Err(Error::InvalidPin("must consist of decimal digits"));
}
Ok(pin)
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
false
}
}
impl Drop for Pin {
fn drop(&mut self) {
self.0.fill(0);
}
}
impl fmt::Debug for Pin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Pin(<{} bytes redacted>)", self.0.len())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_digits_and_alphanumerics() {
assert_eq!(Pin::new("1234").unwrap().as_bytes(), b"1234");
assert_eq!(Pin::new("PASSWORD1234").unwrap().len(), 12);
assert_eq!(Pin::numeric("0000").unwrap().as_bytes(), b"0000");
}
#[test]
fn rejects_unusable_values() {
assert!(Pin::new("").is_err());
assert!(Pin::new("1234\n").is_err());
assert!(Pin::new("1234").is_err());
assert!(Pin::numeric("PASSWORD").is_err());
assert!(Pin::new(vec![b'0'; 16]).is_ok());
assert!(Pin::new(vec![b'0'; 17]).is_err());
}
#[test]
fn debug_does_not_leak_the_secret() {
let rendered = format!("{:?}", Pin::new("1234").unwrap());
assert!(!rendered.contains("1234"), "{rendered}");
}
}