Skip to main content

silent_payments_core/
keys.rs

1//! Newtype wrappers for Silent Payments key types.
2//!
3//! Public keys: [`ScanPublicKey`], [`SpendPublicKey`] -- derive standard traits.
4//! Secret keys: [`ScanSecretKey`], [`SpendSecretKey`] -- zeroized on drop, no Copy/Clone.
5//!
6//! All EC types use `bitcoin::secp256k1` re-exports only (SEC-04).
7//! Secret key newtypes implement `Drop` calling `non_secure_erase()` (SEC-01).
8//! Secret key comparisons delegate to `SecretKey::eq` which is constant-time (SEC-02).
9
10use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
11
12use crate::error::CryptoError;
13
14// ---------------------------------------------------------------------------
15// Public key newtypes
16// ---------------------------------------------------------------------------
17
18/// The receiver's scan public key (B_scan in BIP 352).
19///
20/// Used by the sender to compute ECDH shared secrets for output generation.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct ScanPublicKey(PublicKey);
24
25impl ScanPublicKey {
26    /// Returns a reference to the inner `secp256k1::PublicKey`.
27    pub fn as_inner(&self) -> &PublicKey {
28        &self.0
29    }
30}
31
32impl From<PublicKey> for ScanPublicKey {
33    fn from(key: PublicKey) -> Self {
34        Self(key)
35    }
36}
37
38impl From<ScanPublicKey> for PublicKey {
39    fn from(key: ScanPublicKey) -> Self {
40        key.0
41    }
42}
43
44/// The receiver's spend public key (B_spend in BIP 352).
45///
46/// Combined with the ECDH shared secret to produce per-output public keys.
47#[derive(Debug, Clone, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct SpendPublicKey(PublicKey);
50
51impl SpendPublicKey {
52    /// Returns a reference to the inner `secp256k1::PublicKey`.
53    pub fn as_inner(&self) -> &PublicKey {
54        &self.0
55    }
56}
57
58impl From<PublicKey> for SpendPublicKey {
59    fn from(key: PublicKey) -> Self {
60        Self(key)
61    }
62}
63
64impl From<SpendPublicKey> for PublicKey {
65    fn from(key: SpendPublicKey) -> Self {
66        key.0
67    }
68}
69
70// ---------------------------------------------------------------------------
71// Secret key newtypes
72// ---------------------------------------------------------------------------
73
74/// The receiver's scan secret key (b_scan in BIP 352).
75///
76/// Zeroized on drop via `non_secure_erase()` (SEC-01).
77/// Does NOT derive `Copy`, `Clone`, or `Debug` (SEC-02 timing protection).
78pub struct ScanSecretKey(SecretKey);
79
80impl ScanSecretKey {
81    /// Returns a reference to the inner `secp256k1::SecretKey`.
82    pub fn as_inner(&self) -> &SecretKey {
83        &self.0
84    }
85
86    /// Create from a 32-byte slice.
87    ///
88    /// Returns `CryptoError::InvalidSecretKey` if the bytes are all zeros
89    /// or represent a value >= the curve order.
90    pub fn from_slice(data: &[u8]) -> Result<Self, CryptoError> {
91        SecretKey::from_slice(data)
92            .map(Self)
93            .map_err(|_| CryptoError::InvalidSecretKey)
94    }
95
96    /// Create from an existing `SecretKey`.
97    pub fn from_secret_key(sk: SecretKey) -> Self {
98        Self(sk)
99    }
100
101    /// Derive the corresponding [`ScanPublicKey`].
102    pub fn public_key(&self, secp: &Secp256k1<bitcoin::secp256k1::All>) -> ScanPublicKey {
103        ScanPublicKey(self.0.public_key(secp))
104    }
105}
106
107impl PartialEq for ScanSecretKey {
108    fn eq(&self, other: &Self) -> bool {
109        // Delegates to SecretKey::eq which is constant-time (SEC-02).
110        self.0 == other.0
111    }
112}
113
114impl Eq for ScanSecretKey {}
115
116impl std::fmt::Debug for ScanSecretKey {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        f.write_str("ScanSecretKey(***)")
119    }
120}
121
122impl Drop for ScanSecretKey {
123    fn drop(&mut self) {
124        // SEC-01: zeroize secret material on drop.
125        self.0.non_secure_erase();
126    }
127}
128
129/// The receiver's spend secret key (b_spend in BIP 352).
130///
131/// Zeroized on drop via `non_secure_erase()` (SEC-01).
132/// Does NOT derive `Copy`, `Clone`, or `Debug` (SEC-02 timing protection).
133pub struct SpendSecretKey(SecretKey);
134
135impl SpendSecretKey {
136    /// Returns a reference to the inner `secp256k1::SecretKey`.
137    pub fn as_inner(&self) -> &SecretKey {
138        &self.0
139    }
140
141    /// Create from a 32-byte slice.
142    ///
143    /// Returns `CryptoError::InvalidSecretKey` if the bytes are all zeros
144    /// or represent a value >= the curve order.
145    pub fn from_slice(data: &[u8]) -> Result<Self, CryptoError> {
146        SecretKey::from_slice(data)
147            .map(Self)
148            .map_err(|_| CryptoError::InvalidSecretKey)
149    }
150
151    /// Create from an existing `SecretKey`.
152    pub fn from_secret_key(sk: SecretKey) -> Self {
153        Self(sk)
154    }
155
156    /// Derive the corresponding [`SpendPublicKey`].
157    pub fn public_key(&self, secp: &Secp256k1<bitcoin::secp256k1::All>) -> SpendPublicKey {
158        SpendPublicKey(self.0.public_key(secp))
159    }
160}
161
162impl PartialEq for SpendSecretKey {
163    fn eq(&self, other: &Self) -> bool {
164        // Delegates to SecretKey::eq which is constant-time (SEC-02).
165        self.0 == other.0
166    }
167}
168
169impl Eq for SpendSecretKey {}
170
171impl std::fmt::Debug for SpendSecretKey {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        f.write_str("SpendSecretKey(***)")
174    }
175}
176
177impl Drop for SpendSecretKey {
178    fn drop(&mut self) {
179        // SEC-01: zeroize secret material on drop.
180        self.0.non_secure_erase();
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    // A valid 32-byte secret key (not all zeros, not >= curve order).
189    const VALID_KEY_BYTES: [u8; 32] = [
190        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
191        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e,
192        0x1f, 0x20,
193    ];
194
195    #[test]
196    fn scan_secret_key_from_valid_slice() {
197        let sk = ScanSecretKey::from_slice(&VALID_KEY_BYTES);
198        assert!(
199            sk.is_ok(),
200            "from_slice should succeed with valid 32-byte key"
201        );
202    }
203
204    #[test]
205    fn scan_secret_key_rejects_all_zeros() {
206        let zeros = [0u8; 32];
207        let result = ScanSecretKey::from_slice(&zeros);
208        assert!(
209            matches!(result, Err(CryptoError::InvalidSecretKey)),
210            "from_slice should reject all-zero key"
211        );
212    }
213
214    #[test]
215    fn scan_secret_key_debug_hides_material() {
216        let sk = ScanSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
217        let debug_str = format!("{:?}", sk);
218        assert_eq!(debug_str, "ScanSecretKey(***)");
219        // Ensure no key bytes leak through Debug.
220        assert!(
221            !debug_str.contains("0102"),
222            "Debug output must not contain key material"
223        );
224    }
225
226    #[test]
227    fn scan_secret_key_equality() {
228        let sk1 = ScanSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
229        let sk2 = ScanSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
230        assert_eq!(sk1, sk2, "keys from same bytes should be equal");
231    }
232
233    #[test]
234    fn scan_secret_key_derives_public_key() {
235        let secp = Secp256k1::new();
236        let sk = ScanSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
237        let pk = sk.public_key(&secp);
238
239        // Verify the derived public key matches what secp256k1 produces directly.
240        let raw_sk = SecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
241        let expected_pk = raw_sk.public_key(&secp);
242        assert_eq!(
243            *pk.as_inner(),
244            expected_pk,
245            "public_key() should derive the correct public key"
246        );
247    }
248
249    #[test]
250    fn scan_secret_key_drop_runs_without_panic() {
251        // Create a key in a block and let it drop. If Drop panics, the test fails.
252        {
253            let _sk = ScanSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
254        }
255        // If we reach here, Drop completed without panicking.
256    }
257
258    #[test]
259    fn spend_secret_key_from_valid_slice() {
260        let sk = SpendSecretKey::from_slice(&VALID_KEY_BYTES);
261        assert!(
262            sk.is_ok(),
263            "from_slice should succeed with valid 32-byte key"
264        );
265    }
266
267    #[test]
268    fn spend_secret_key_rejects_all_zeros() {
269        let zeros = [0u8; 32];
270        let result = SpendSecretKey::from_slice(&zeros);
271        assert!(
272            matches!(result, Err(CryptoError::InvalidSecretKey)),
273            "from_slice should reject all-zero key"
274        );
275    }
276
277    #[test]
278    fn spend_secret_key_debug_hides_material() {
279        let sk = SpendSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
280        let debug_str = format!("{:?}", sk);
281        assert_eq!(debug_str, "SpendSecretKey(***)");
282    }
283
284    #[test]
285    fn spend_secret_key_derives_public_key() {
286        let secp = Secp256k1::new();
287        let sk = SpendSecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
288        let pk = sk.public_key(&secp);
289
290        let raw_sk = SecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
291        let expected_pk = raw_sk.public_key(&secp);
292        assert_eq!(*pk.as_inner(), expected_pk);
293    }
294
295    #[test]
296    fn public_key_from_and_into_conversions() {
297        let secp = Secp256k1::new();
298        let raw_sk = SecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
299        let raw_pk = raw_sk.public_key(&secp);
300
301        // From<PublicKey> for ScanPublicKey
302        let scan_pk = ScanPublicKey::from(raw_pk);
303        assert_eq!(*scan_pk.as_inner(), raw_pk);
304
305        // From<ScanPublicKey> for PublicKey (bidirectional)
306        let recovered: PublicKey = scan_pk.into();
307        assert_eq!(recovered, raw_pk);
308
309        // Same for SpendPublicKey
310        let spend_pk = SpendPublicKey::from(raw_pk);
311        assert_eq!(*spend_pk.as_inner(), raw_pk);
312        let recovered: PublicKey = spend_pk.into();
313        assert_eq!(recovered, raw_pk);
314    }
315
316    #[test]
317    fn from_secret_key_constructor() {
318        let raw_sk = SecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
319        let scan_sk = ScanSecretKey::from_secret_key(raw_sk);
320        assert_eq!(
321            *scan_sk.as_inner(),
322            SecretKey::from_slice(&VALID_KEY_BYTES).unwrap()
323        );
324
325        let raw_sk = SecretKey::from_slice(&VALID_KEY_BYTES).unwrap();
326        let spend_sk = SpendSecretKey::from_secret_key(raw_sk);
327        assert_eq!(
328            *spend_sk.as_inner(),
329            SecretKey::from_slice(&VALID_KEY_BYTES).unwrap()
330        );
331    }
332}