Skip to main content

btc_keygen/
secret.rs

1//! Fixed-size, erase-on-drop containers for secret ASCII text.
2//!
3//! Producers write their output *directly* into one of these buffers, so no
4//! secret-bearing `String`, `Vec`, or `format!` temporary is ever allocated on
5//! the way out. The buffer lives on the heap behind a `Box` so that moving the
6//! value moves a pointer, not the secret: the bytes are written once and
7//! zeroized once, at the address they were born.
8//!
9//! There is deliberately no `Display`, `Clone`, `Copy`, or `Deref`. Reading a
10//! secret is spelled `expose_bytes` or `expose_str`, and what a caller does
11//! with the borrow is beyond this crate's reach: printing it, or copying it
12//! into a `String`, creates copies that will not be erased.
13
14use core::fmt;
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17/// A compressed mainnet WIF: exactly 52 Base58 ASCII bytes.
18///
19/// Returned by [`encode_wif`](crate::encode_wif). See [`SecretAscii`].
20pub type SecretWif = SecretAscii<52>;
21
22/// A raw private key in hexadecimal: exactly 64 lowercase ASCII bytes.
23///
24/// Returned by [`PrivateKey::to_hex`](crate::PrivateKey::to_hex). See
25/// [`SecretAscii`].
26pub type SecretKeyHex = SecretAscii<64>;
27
28/// A fixed-length ASCII secret that zeroizes on drop and redacts its `Debug`.
29///
30/// Use it through the [`SecretWif`] and [`SecretKeyHex`] aliases; the length
31/// parameter is what keeps them distinct types, so a hex string cannot be
32/// passed where a WIF is expected.
33#[derive(Zeroize, ZeroizeOnDrop)]
34pub struct SecretAscii<const N: usize> {
35    // Boxed so that a move copies the pointer, never the secret.
36    bytes: Box<[u8; N]>,
37}
38
39impl<const N: usize> SecretAscii<N> {
40    /// A zeroed buffer for a producer to fill via [`Self::bytes_mut`].
41    pub(crate) fn zeroed() -> Self {
42        Self {
43            bytes: Box::new([0u8; N]),
44        }
45    }
46
47    /// Write access for producers inside this crate. Crate-private: it is the
48    /// only way to break the "every byte is printable ASCII" invariant that
49    /// [`Self::expose_str`] relies on.
50    pub(crate) fn bytes_mut(&mut self) -> &mut [u8; N] {
51        &mut self.bytes
52    }
53
54    /// Borrows the secret as raw ASCII bytes.
55    ///
56    /// Prefer handing this straight to
57    /// [`Write::write_all`](std::io::Write::write_all). Anything that copies
58    /// the bytes elsewhere creates a copy this crate cannot erase.
59    #[must_use]
60    pub fn expose_bytes(&self) -> &[u8; N] {
61        &self.bytes
62    }
63
64    /// Borrows the secret as a string.
65    ///
66    /// Convenient for comparisons, but note that a `&str` slips into
67    /// `format!`, `to_string`, and `String::from` without a second thought,
68    /// each of which leaves an unerased copy on the heap. Reach for
69    /// [`Self::expose_bytes`] at output boundaries.
70    #[must_use]
71    pub fn expose_str(&self) -> &str {
72        // Infallible: every byte is written from a fixed ASCII alphabet, and
73        // producers assert that the buffer was filled completely.
74        core::str::from_utf8(self.expose_bytes()).expect("SecretAscii holds ASCII")
75    }
76}
77
78/// Redacted, so that a stray `{:?}` cannot leak a key.
79///
80/// ```
81/// # let key = btc_keygen::PrivateKey::from_hex(
82/// #     "0000000000000000000000000000000000000000000000000000000000000001").unwrap();
83/// let wif = btc_keygen::encode_wif(&key);
84/// assert_eq!(format!("{:?}", wif), "Secret<52>([REDACTED])");
85/// ```
86///
87/// A secret cannot be cloned:
88///
89/// ```compile_fail
90/// # let key = btc_keygen::PrivateKey::from_hex(
91/// #     "0000000000000000000000000000000000000000000000000000000000000001").unwrap();
92/// let wif = btc_keygen::encode_wif(&key);
93/// let copy = wif.clone();
94/// ```
95///
96/// nor copied, so a move ends the original's life:
97///
98/// ```compile_fail
99/// # let key = btc_keygen::PrivateKey::from_hex(
100/// #     "0000000000000000000000000000000000000000000000000000000000000001").unwrap();
101/// let wif = btc_keygen::encode_wif(&key);
102/// let moved = wif;
103/// let _ = wif.expose_bytes();
104/// ```
105///
106/// nor formatted with `{}`:
107///
108/// ```compile_fail
109/// # let key = btc_keygen::PrivateKey::from_hex(
110/// #     "0000000000000000000000000000000000000000000000000000000000000001").unwrap();
111/// let wif = btc_keygen::encode_wif(&key);
112/// println!("{}", wif);
113/// ```
114impl<const N: usize> fmt::Debug for SecretAscii<N> {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        write!(f, "Secret<{N}>([REDACTED])")
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_debug_redacts_contents() {
126        let mut secret = SecretWif::zeroed();
127        secret
128            .bytes_mut()
129            .copy_from_slice(b"KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn");
130
131        let debug = format!("{:?}", secret);
132        assert_eq!(debug, "Secret<52>([REDACTED])");
133        assert!(!debug.contains("KwDi"), "Debug must not leak the WIF");
134    }
135
136    #[test]
137    fn test_expose_str_round_trips() {
138        let mut secret = SecretKeyHex::zeroed();
139        let hex = b"0000000000000000000000000000000000000000000000000000000000000001";
140        secret.bytes_mut().copy_from_slice(hex);
141        assert_eq!(secret.expose_str(), std::str::from_utf8(hex).unwrap());
142        assert_eq!(secret.expose_bytes(), hex);
143    }
144
145    #[test]
146    fn test_zeroed_starts_empty() {
147        assert_eq!(SecretWif::zeroed().expose_bytes(), &[0u8; 52]);
148    }
149}