Skip to main content

ap_noise/
psk.rs

1//! PSK (Pre-Shared Key) module.
2//!
3//! Pre-Shared Keys (PSKs) provide authentication and protection against man-in-the-middle attacks
4//! in Noise protocol handshakes. When both parties share a secret PSK established through a secure
5//! out-of-band channel (e.g., QR code, NFC, secure messaging), the handshake will fail if an
6//! attacker attempts to intercept or modify messages, and no fingerprint verification is needed.
7
8use rand::RngCore;
9use std::fmt::Debug;
10
11/// PSK length in bytes (32 bytes for Noise protocol)
12pub const PSK_LENGTH: usize = 32;
13
14#[derive(Clone, PartialEq, zeroize::ZeroizeOnDrop)]
15pub struct Psk([u8; PSK_LENGTH]);
16
17impl Debug for Psk {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        use sha2::{Digest, Sha256 as Sha256Hash};
20        let hash = Sha256Hash::digest(self.0);
21        let preview = format!(
22            "{:02x}{:02x}{:02x}{:02x}...",
23            hash[0], hash[1], hash[2], hash[3]
24        );
25        write!(f, "Psk({preview})")
26    }
27}
28
29impl Psk {
30    /// A null PSK (all zeroes).
31    ///
32    /// This should only be used when PSK authentication is not required and only encryption
33    /// is desired. Using a null PSK provides confidentiality but not authentication.
34    pub fn null() -> Self {
35        Psk([0u8; PSK_LENGTH])
36    }
37
38    /// Generate a random PSK using a cryptographically secure random number generator.
39    ///
40    /// This is the recommended way to create a PSK for secure authentication. The PSK
41    /// must then be shared with the other party through a secure out-of-band channel
42    /// (QR code, NFC, secure messaging, etc.).
43    ///
44    /// # Example
45    ///
46    /// ```rust
47    /// use ap_noise::Psk;
48    ///
49    /// let psk = Psk::generate();
50    /// ```
51    pub fn generate() -> Self {
52        let mut rng = rand::thread_rng();
53        let mut bytes = [0u8; PSK_LENGTH];
54        rng.fill_bytes(&mut bytes);
55        Psk(bytes)
56    }
57
58    /// Construct a PSK from a 32-byte array.
59    pub fn from_bytes(bytes: [u8; 32]) -> Self {
60        Psk(bytes)
61    }
62
63    /// Export the PSK as a 32-byte array.
64    pub fn to_bytes(&self) -> [u8; 32] {
65        self.0
66    }
67
68    /// Encode the PSK as a hexadecimal string.
69    ///
70    /// This is the recommended format for storing or transmitting PSKs (e.g., in QR codes,
71    /// configuration files, or secure storage). The resulting string is 64 characters long.
72    ///
73    /// # Example
74    ///
75    /// ```rust
76    /// use ap_noise::Psk;
77    /// use rand::thread_rng;
78    ///
79    /// let psk = Psk::generate();
80    /// let encoded = psk.to_hex();
81    /// assert_eq!(encoded.len(), 64);
82    /// ```
83    pub fn to_hex(&self) -> String {
84        hex::encode(self.0)
85    }
86
87    /// Decode a PSK from a hexadecimal string.
88    ///
89    /// Returns an error if the string is not valid hex or does not decode to exactly
90    /// 32 bytes.
91    ///
92    /// # Example
93    ///
94    /// ```rust
95    /// use ap_noise::Psk;
96    /// use rand::thread_rng;
97    ///
98    /// let psk = Psk::generate();
99    /// let encoded = psk.to_hex();
100    /// let decoded = Psk::from_hex(&encoded).unwrap();
101    /// assert_eq!(psk.to_bytes(), decoded.to_bytes());
102    /// ```
103    pub fn from_hex(s: &str) -> Result<Self, crate::error::NoiseProtocolError> {
104        let bytes =
105            hex::decode(s).map_err(|_| crate::error::NoiseProtocolError::InvalidPskEncoding)?;
106
107        if bytes.len() != PSK_LENGTH {
108            return Err(crate::error::NoiseProtocolError::InvalidPskLength);
109        }
110
111        let mut arr = [0u8; PSK_LENGTH];
112        arr.copy_from_slice(&bytes);
113        Ok(Psk(arr))
114    }
115
116    pub fn as_slice(&self) -> &[u8] {
117        &self.0
118    }
119
120    /// Compute a stable, non-secret identifier for this PSK.
121    ///
122    /// Returns `hex(SHA256(psk)[0..8])` — a 16-character hex string derived from
123    /// the first 8 bytes of the PSK's SHA-256 hash. This serves as a lookup key
124    /// for matching incoming handshakes to pending pairings without revealing the
125    /// PSK itself.
126    pub fn id(&self) -> String {
127        use sha2::{Digest, Sha256 as Sha256Hash};
128        let hash = Sha256Hash::digest(self.0);
129        hex::encode(&hash[..8])
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn test_null_psk() {
139        let psk = Psk::null();
140        assert_eq!(psk.to_bytes(), [0u8; PSK_LENGTH]);
141    }
142
143    #[test]
144    fn test_generate_psk() {
145        let psk1 = Psk::generate();
146        let psk2 = Psk::generate();
147
148        // PSKs should be different
149        assert_ne!(psk1.to_bytes(), psk2.to_bytes());
150    }
151
152    #[test]
153    fn test_bytes_roundtrip() {
154        let psk = Psk::generate();
155        let bytes = psk.to_bytes();
156        let psk_from_bytes = Psk::from_bytes(bytes);
157        assert_eq!(psk.to_bytes(), psk_from_bytes.to_bytes());
158    }
159
160    #[test]
161    fn test_hex_roundtrip() {
162        let psk = Psk::generate();
163        let encoded = psk.to_hex();
164        assert_eq!(encoded.len(), 64);
165        let decoded = Psk::from_hex(&encoded).expect("Failed to decode PSK from hex");
166        assert_eq!(psk.to_bytes(), decoded.to_bytes());
167    }
168}