Skip to main content

pdfrum_crypt/
key.rs

1//! The file encryption key: a short byte string that must never be logged.
2
3use std::fmt;
4
5use zeroize::Zeroize;
6
7/// A file encryption key of at most 32 bytes.
8///
9/// Inline storage, because every key in this crate is between 5 and 32 bytes
10/// and heap-allocating one only adds a place for it to be copied. The
11/// invariant is `len <= 32`.
12///
13/// [`fmt::Debug`] is implemented by hand and prints only the length: key
14/// material must never reach a log line or a diagnostic entry, and the derived
15/// implementation would put it in both.
16#[derive(Clone, PartialEq, Eq)]
17pub struct SmallKey {
18    bytes: [u8; 32],
19    len: u8,
20}
21
22impl SmallKey {
23    /// The longest key any revision uses.
24    pub const MAX_LEN: usize = 32;
25
26    /// Take the first `len` bytes of `source` as a key, zero-filling if
27    /// `source` is shorter and truncating at 32 bytes.
28    #[must_use]
29    pub(crate) fn from_prefix(source: &[u8], len: usize) -> Self {
30        let len = len.min(Self::MAX_LEN);
31        let mut bytes = [0u8; Self::MAX_LEN];
32        let copied = len.min(source.len());
33        if let (Some(head), Some(from)) = (bytes.get_mut(..copied), source.get(..copied)) {
34            head.copy_from_slice(from);
35        }
36        #[expect(
37            clippy::cast_possible_truncation,
38            reason = "len <= 32 by the line above"
39        )]
40        Self {
41            bytes,
42            len: len as u8,
43        }
44    }
45
46    /// A full 32-byte key, as revision 5 and 6 produce.
47    #[must_use]
48    pub(crate) const fn from_full(bytes: [u8; 32]) -> Self {
49        #[expect(clippy::cast_possible_truncation, reason = "32 fits in a u8")]
50        Self {
51            bytes,
52            len: Self::MAX_LEN as u8,
53        }
54    }
55
56    /// The key bytes.
57    #[must_use]
58    pub fn bytes(&self) -> &[u8] {
59        self.bytes.get(..self.len()).unwrap_or_default()
60    }
61
62    /// The key length in bytes.
63    #[must_use]
64    pub fn len(&self) -> usize {
65        usize::from(self.len)
66    }
67
68    /// Whether the key is empty, which no valid handler produces.
69    #[must_use]
70    pub fn is_empty(&self) -> bool {
71        self.len == 0
72    }
73}
74
75impl Drop for SmallKey {
76    fn drop(&mut self) {
77        self.bytes.zeroize();
78        self.len = 0;
79    }
80}
81
82impl fmt::Debug for SmallKey {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "SmallKey(<{} bytes redacted>)", self.len())
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::SmallKey;
91
92    #[test]
93    fn a_prefix_shorter_than_requested_is_zero_filled() {
94        let key = SmallKey::from_prefix(b"abc", 5);
95        assert_eq!(key.bytes(), b"abc\0\0");
96        assert_eq!(key.len(), 5);
97    }
98
99    #[test]
100    fn a_key_never_exceeds_thirty_two_bytes() {
101        let key = SmallKey::from_prefix(&[0xFFu8; 64], 64);
102        assert_eq!(key.len(), SmallKey::MAX_LEN);
103        assert_eq!(key.bytes().len(), SmallKey::MAX_LEN);
104    }
105
106    #[test]
107    fn debug_redacts_the_material() {
108        let key = SmallKey::from_full([0xAB; 32]);
109        let dump = format!("{key:?}");
110        assert_eq!(dump, "SmallKey(<32 bytes redacted>)");
111        assert!(!dump.contains("ab"), "{dump}");
112        assert!(!dump.contains("171"), "{dump}");
113    }
114
115    #[test]
116    fn an_empty_key_is_representable_and_says_so() {
117        let key = SmallKey::from_prefix(b"", 0);
118        assert!(key.is_empty());
119        assert_eq!(key.bytes(), b"");
120    }
121}