#[cfg(feature = "alloc")]
use alloc::{boxed::Box, string::String, vec::Vec};
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
pub struct Secret {
bytes: ByteStorage,
}
impl Secret {
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub const fn new(bytes: Box<[u8]>) -> Self {
Self {
bytes: ByteStorage::Heap(bytes),
}
}
pub(crate) const fn empty() -> Self {
Self {
bytes: ByteStorage::Empty,
}
}
pub const fn new_stack(bytes: [u8; 20]) -> Self {
Self {
bytes: ByteStorage::Stack(bytes),
}
}
pub const fn as_bytes(&self) -> &[u8] {
self.bytes.as_bytes()
}
#[cfg(feature = "gen_secret")]
#[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))]
pub fn generate() -> Self {
Self::from(generate_random_bytes())
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn try_from_base32(value: impl AsRef<str>) -> Result<Self, SecretParseError> {
match base32::decode(RFC4648_ALPHABET, value.as_ref()) {
Some(buf) => Ok(buf.into()),
None => Err(SecretParseError::ParseBase32),
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn to_base32(&self) -> String {
base32::encode(RFC4648_ALPHABET, self.bytes.as_bytes())
}
}
impl Clone for Secret {
fn clone(&self) -> Self {
Self {
bytes: self.bytes.clone(),
}
}
fn clone_from(&mut self, source: &Self) {
self.bytes.clone_from(&source.bytes);
}
}
impl PartialEq for Secret {
fn eq(&self, other: &Self) -> bool {
constant_time_eq::constant_time_eq(self, other)
}
}
impl Eq for Secret {}
#[cfg(feature = "gen_secret")]
#[cfg_attr(docsrs, doc(cfg(feature = "gen_secret")))]
impl Default for Secret {
fn default() -> Self {
Self::generate()
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl<'a> From<&'a [u8]> for Secret {
fn from(value: &'a [u8]) -> Self {
Self::new(value.into())
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl From<Box<[u8]>> for Secret {
fn from(value: Box<[u8]>) -> Self {
Self::new(value)
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl From<Vec<u8>> for Secret {
fn from(value: Vec<u8>) -> Self {
Self::new(value.into())
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl<const N: usize> From<[u8; N]> for Secret {
fn from(value: [u8; N]) -> Self {
if N == 20 {
let value = (&value as &[u8]).try_into().unwrap();
Self::new_stack(value)
} else {
Self::new(value.into())
}
}
}
#[cfg(not(feature = "alloc"))]
impl From<[u8; 20]> for Secret {
fn from(value: [u8; 20]) -> Self {
Self::new_stack(value)
}
}
impl AsRef<[u8]> for Secret {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl core::ops::Deref for Secret {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_bytes()
}
}
impl core::fmt::Debug for Secret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "REDACTED")
}
}
impl core::fmt::Display for Secret {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for b in self.as_bytes() {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for Secret {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
if s.is_human_readable() {
s.collect_str(&Base32Display(self.as_bytes()))
} else {
s.serialize_bytes(self.as_bytes())
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for Secret {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct SecretVisitor;
impl<'de> serde::de::Visitor<'de> for SecretVisitor {
type Value = Secret;
fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("a secret as a base32 string or a sequence of bytes")
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Secret, E> {
#[cfg(feature = "alloc")]
{
Secret::try_from_base32(v).map_err(E::custom)
}
#[cfg(not(feature = "alloc"))]
{
if v.len() > 32 {
return Err(E::custom(
"a secret must be at most 20 bytes without the `alloc` feature",
));
}
match decode_base32_stack(v) {
Some((buf, len)) => secret_from_bytes(&buf[..len]),
None => Err(E::custom("Could not decode base32 secret.")),
}
}
}
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<Secret, E> {
secret_from_bytes(v)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<Secret, A::Error> {
#[cfg(feature = "alloc")]
{
let mut buf = Vec::with_capacity(seq.size_hint().unwrap_or(20));
while let Some(byte) = seq.next_element::<u8>()? {
buf.push(byte);
}
secret_from_bytes(&buf)
}
#[cfg(not(feature = "alloc"))]
{
let mut buf = [0u8; 20];
let mut len = 0;
while let Some(byte) = seq.next_element::<u8>()? {
if len >= buf.len() {
return Err(serde::de::Error::custom(
"a secret must be at most 20 bytes without the `alloc` feature",
));
}
buf[len] = byte;
len += 1;
}
secret_from_bytes(&buf[..len])
}
}
}
if deserializer.is_human_readable() {
deserializer.deserialize_str(SecretVisitor)
} else {
deserializer.deserialize_bytes(SecretVisitor)
}
}
}
#[cfg(feature = "serde")]
struct Base32Display<'a>(&'a [u8]);
#[cfg(feature = "serde")]
impl core::fmt::Display for Base32Display<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use core::fmt::Write;
const ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
for chunk in self.0.chunks(5) {
let mut group = [0u8; 5];
group[..chunk.len()].copy_from_slice(chunk);
let bits =
u64::from_be_bytes([0, 0, 0, group[0], group[1], group[2], group[3], group[4]]);
let chars = [2, 4, 5, 7, 8][chunk.len() - 1];
for i in 0..chars {
let index = (bits >> (35 - 5 * i)) & 0x1f;
f.write_char(ALPHABET[index as usize] as char)?;
}
}
Ok(())
}
}
#[cfg(all(feature = "serde", any(not(feature = "alloc"), test)))]
fn decode_base32_stack(input: &str) -> Option<([u8; 20], usize)> {
if input.len() > 32 {
return None;
}
let mut out = [0u8; 20];
let mut acc = 0u32;
let mut bits = 0u8;
let mut len = 0;
for &c in input.as_bytes() {
let value = match c {
b'A'..=b'Z' => c - b'A',
b'2'..=b'7' => c - b'2' + 26,
_ => return None,
};
acc = (acc << 5) | u32::from(value);
bits += 5;
if bits >= 8 {
bits -= 8;
out[len] = (acc >> bits) as u8;
len += 1;
}
}
Some((out, len))
}
#[cfg(feature = "serde")]
fn secret_from_bytes<E: serde::de::Error>(bytes: &[u8]) -> Result<Secret, E> {
if bytes.len() == 20 {
let array: [u8; 20] = bytes.try_into().unwrap();
return Ok(Secret::new_stack(array));
}
#[cfg(feature = "alloc")]
{
Ok(Secret::new(bytes.into()))
}
#[cfg(not(feature = "alloc"))]
{
if bytes.is_empty() {
Ok(Secret::empty())
} else {
Err(E::custom(
"a secret must be 20 bytes (or empty) without the `alloc` feature",
))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SecretParseError {
ParseBase32,
}
impl core::error::Error for SecretParseError {}
impl core::fmt::Display for SecretParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
SecretParseError::ParseBase32 => write!(f, "Could not decode base32 secret."),
}
}
}
#[cfg(feature = "gen_secret")]
pub(crate) fn generate_random_bytes() -> [u8; 20] {
fn generate_inner<const N: usize, T: rand::RngExt>(mut rng: T) -> [u8; N] {
let mut secret = [0u8; N];
rng.fill(&mut secret[..]);
secret
}
#[cfg(feature = "std")]
return generate_inner(rand::rng());
#[allow(
unreachable_code,
reason = "allowing an unreachable statement here ensures this codepath is valid even if no_std isn't properly tested."
)]
return generate_inner(rand::make_rng::<rand::rngs::ChaCha12Rng>());
}
#[cfg(feature = "alloc")]
const RFC4648_ALPHABET: base32::Alphabet = base32::Alphabet::Rfc4648 { padding: false };
#[cfg_attr(feature = "zeroize", derive(zeroize::Zeroize, zeroize::ZeroizeOnDrop))]
#[non_exhaustive]
enum ByteStorage {
Empty,
#[cfg(feature = "alloc")]
Heap(Box<[u8]>),
Stack([u8; 20]),
}
impl ByteStorage {
const fn as_bytes(&self) -> &[u8] {
match self {
Self::Empty => &[],
#[cfg(feature = "alloc")]
Self::Heap(heap) => heap,
Self::Stack(stack) => stack,
}
}
}
impl Clone for ByteStorage {
fn clone(&self) -> Self {
match self {
Self::Empty => Self::Empty,
#[cfg(feature = "alloc")]
Self::Heap(heap) => Self::Heap(heap.clone()),
Self::Stack(stack) => Self::Stack(*stack),
}
}
}
#[cfg(feature = "alloc")]
#[cfg(test)]
mod tests {
use super::{ByteStorage, Secret, SecretParseError};
const BASE32: &str = "OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG";
const BYTES: [u8; 23] = [
0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
];
const BYTES_DISPLAY: &str = "706c61696e2d737472696e672d7365637265742d313233";
#[test]
fn secret_display_and_debug() {
let base32_str = String::from(BASE32);
let secret_raw = Secret::from(BYTES);
let secret_base32 = Secret::try_from_base32(base32_str).unwrap();
println!("{}", secret_raw);
assert_eq!(&secret_raw.to_string(), BYTES_DISPLAY);
assert_eq!(&secret_base32.to_string(), BYTES_DISPLAY);
assert_eq!(format!("{:?}", secret_base32), "REDACTED");
}
#[test]
fn secret_convert_base32_raw() {
let secret_raw = Secret::from(BYTES);
let secret_base32 = Secret::try_from_base32(BASE32);
assert_eq!(&Ok(secret_raw), &secret_base32);
}
#[test]
fn secret_as_bytes() {
assert_eq!(Secret::from(BYTES).as_bytes(), BYTES);
assert_eq!(
Secret::try_from_base32(BASE32).as_deref(),
Ok(BYTES.as_slice())
);
}
#[test]
fn secret_cloning_equality() {
let a = Secret::from(BYTES);
let b = Secret::clone(&a);
assert_eq!(a, b);
}
#[test]
fn secret_clone_from_equality() {
let a = Secret::from(BYTES);
let mut b = Secret::new_stack([0; 20]);
assert_ne!(a, b);
b.clone_from(&a);
assert_eq!(a, b);
}
#[test]
fn secret_from_box_equivalent_to_new() {
let heap: Box<[u8]> = Box::new(BYTES);
let a = Secret::new(heap.clone());
let b = Secret::from(heap);
assert_eq!(a, b);
}
#[test]
fn secret_from_string() {
let bytes = "TestSecretSuperSecret".as_bytes();
let base_32 = "KRSXG5CTMVRXEZLUKN2XAZLSKNSWG4TFOQ";
let raw = Secret::from(bytes);
let encoded = Secret::try_from_base32(base_32).unwrap();
assert_eq!(&raw.to_base32(), base_32);
assert_eq!(bytes, encoded.as_bytes());
}
#[test]
fn secret_from_string_failure() {
let base_32 = "1";
let secret = Secret::try_from_base32(base_32);
assert!(matches!(secret, Err(SecretParseError::ParseBase32)));
let error = secret.unwrap_err();
assert_eq!(&error.to_string(), "Could not decode base32 secret.");
}
#[test]
#[cfg(feature = "gen_secret")]
fn secret_gen_secret() {
let sec = Secret::generate();
assert_eq!(sec.len(), 20);
}
#[test]
#[cfg(feature = "gen_secret")]
fn secret_gen_default() {
let sec = Secret::default();
assert_eq!(sec.len(), 20);
}
#[test]
#[cfg(feature = "gen_secret")]
fn secret_empty() {
let non_ascii = vec![240, 159, 146, 150];
let sec = Secret::try_from_base32(core::str::from_utf8(&non_ascii).unwrap());
assert!(sec.is_err());
}
#[test]
fn bytestorage_cloning_consistency() {
use ByteStorage::{Empty, Heap, Stack};
assert!(matches!(Empty.clone(), Empty));
assert!(matches!(Heap(Box::new([])).clone(), Heap(..)));
assert!(matches!(Stack([0; 20]).clone(), Stack(..)));
}
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use super::{ByteStorage, Secret};
use serde_test::{Configure, Token, assert_de_tokens, assert_de_tokens_error, assert_tokens};
const STACK_20: [u8; 20] = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
];
const STACK_20_BASE32: &str = "AEBAGBAFAYDQQCIKBMGA2DQPCAIREEYU";
#[cfg(feature = "alloc")]
const BYTES: [u8; 23] = [
0x70, 0x6c, 0x61, 0x69, 0x6e, 0x2d, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2d, 0x73, 0x65,
0x63, 0x72, 0x65, 0x74, 0x2d, 0x31, 0x32, 0x33,
];
#[test]
fn serde_roundtrip_stack_20() {
let secret = Secret::new_stack(STACK_20);
assert!(matches!(secret.bytes, ByteStorage::Stack(_)));
assert_tokens(&secret.clone().compact(), &[Token::Bytes(&STACK_20)]);
assert_tokens(&secret.readable(), &[Token::Str(STACK_20_BASE32)]);
}
#[test]
#[cfg(feature = "alloc")]
fn serde_roundtrip_heap_non_20() {
let secret = Secret::from(BYTES); assert!(matches!(secret.bytes, ByteStorage::Heap(_)));
assert_tokens(&secret.clone().compact(), &[Token::Bytes(&BYTES)]);
assert_tokens(
&secret.readable(),
&[Token::Str("OBWGC2LOFVZXI4TJNZTS243FMNZGK5BNGEZDG")],
);
}
#[test]
fn serde_deserialize_rejects_invalid_base32() {
for input in ["obwgc2lo", "OBWGC2LOFU======", "0189", "O!"] {
assert_de_tokens_error::<serde_test::Readable<Secret>>(
&[Token::Str(input)],
"Could not decode base32 secret.",
);
}
}
#[test]
#[cfg(not(feature = "alloc"))]
fn serde_deserialize_rejects_over_32_chars_without_alloc() {
assert_de_tokens_error::<serde_test::Readable<Secret>>(
&[Token::Str("AEBAGBAFAYDQQCIKBMGA2DQPCAIREEYUA")], "a secret must be at most 20 bytes without the `alloc` feature",
);
}
#[test]
#[cfg(feature = "alloc")]
fn base32_display_matches_base32_crate() {
use alloc::format;
use alloc::vec::Vec;
for len in 0..=41usize {
let bytes: Vec<u8> = (0..len).map(|i| (i as u8).wrapping_mul(7)).collect();
let secret = Secret::from(bytes.as_slice());
assert_eq!(
format!("{}", super::Base32Display(&bytes)),
secret.to_base32(),
"encoder mismatch at length {len}",
);
}
}
#[test]
#[cfg(feature = "alloc")]
fn decode_base32_stack_matches_base32_crate() {
use super::{RFC4648_ALPHABET, decode_base32_stack};
use alloc::vec::Vec;
for len in 0..=20usize {
let bytes: Vec<u8> = (0..len)
.map(|i| (i as u8).wrapping_mul(13).wrapping_add(3))
.collect();
let encoded = Secret::from(bytes.as_slice()).to_base32();
let (buf, decoded_len) = decode_base32_stack(&encoded).unwrap();
assert_eq!(
&buf[..decoded_len],
&bytes[..],
"decoder mismatch at length {len}"
);
assert_eq!(
base32::decode(RFC4648_ALPHABET, &encoded).as_deref(),
Some(&buf[..decoded_len]),
"reference decoder disagrees at length {len}",
);
}
for input in [
"AEBAGBAFAYDQQCIKBMGA2DQPCAIREEYUA", "obwgc2lo",
"OBWGC2LOFU======",
"0189",
] {
assert!(
decode_base32_stack(input).is_none(),
"decoder accepted malformed input {input:?}",
);
}
}
#[test]
fn serde_deserialize_from_integer_sequence() {
assert_de_tokens(
&Secret::new_stack(STACK_20).readable(),
&seq_tokens(&STACK_20),
);
#[cfg(feature = "alloc")]
assert_de_tokens(&Secret::from(BYTES).readable(), &seq_tokens(&BYTES));
}
fn seq_tokens(bytes: &'static [u8]) -> Vec<Token> {
let mut tokens = vec![Token::Seq {
len: Some(bytes.len()),
}];
tokens.extend(bytes.iter().map(|&b| Token::U8(b)));
tokens.push(Token::SeqEnd);
tokens
}
}