1use base64::Engine;
2use base64::engine::general_purpose::STANDARD_NO_PAD;
3use hpke::{Deserializable, Kem as KemTrait, Serializable};
4use zeroize::{ZeroizeOnDrop, Zeroizing};
5
6use crate::{Error, XKem, rng};
7
8#[derive(Clone, PartialEq, Eq)]
13pub struct SecretKey(<XKem as KemTrait>::PrivateKey);
14
15impl ZeroizeOnDrop for SecretKey {}
19
20const _: fn() = || {
22 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
23 assert_zeroize_on_drop::<x_wing::DecapsulationKey>();
24};
25
26#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct PublicKey(<XKem as KemTrait>::PublicKey);
32
33impl SecretKey {
34 #[must_use]
36 pub fn from_seed(seed: &[u8; 32]) -> Self {
37 let Ok(sk) = <XKem as KemTrait>::PrivateKey::from_bytes(seed) else {
38 unreachable!("a 32-byte array is always a valid X-Wing seed")
39 };
40 Self(sk)
41 }
42
43 pub fn generate() -> Result<Self, Error> {
53 let mut seed = Zeroizing::new([0u8; 32]);
54 rng::fill(&mut seed[..])?;
55 Ok(Self::from_seed(&seed))
56 }
57
58 pub(crate) fn as_hpke(&self) -> &<XKem as KemTrait>::PrivateKey {
59 &self.0
60 }
61
62 #[must_use]
64 pub fn public_key(&self) -> PublicKey {
65 PublicKey::new(<XKem as KemTrait>::sk_to_pk(&self.0))
66 }
67}
68
69impl std::fmt::Debug for SecretKey {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.write_str("SecretKey(REDACTED)")
72 }
73}
74
75impl PublicKey {
76 pub(crate) fn new(pk: <XKem as KemTrait>::PublicKey) -> Self {
77 Self(pk)
78 }
79
80 pub(crate) fn as_hpke(&self) -> &<XKem as KemTrait>::PublicKey {
81 &self.0
82 }
83
84 #[must_use]
86 pub fn to_bytes(&self) -> Vec<u8> {
87 self.0.to_bytes().as_slice().to_vec()
88 }
89
90 pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
95 <XKem as KemTrait>::PublicKey::from_bytes(bytes)
96 .map(Self)
97 .map_err(|_| Error::KeyFormat)
98 }
99}
100
101impl std::fmt::Display for PublicKey {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 f.write_str(&STANDARD_NO_PAD.encode(self.to_bytes()))
104 }
105}
106
107#[expect(clippy::unwrap_used, reason = "clearer in tests")]
108#[cfg(test)]
109mod tests {
110 use super::{Error, PublicKey, SecretKey};
111 use base64::Engine;
112 use base64::engine::general_purpose::STANDARD_NO_PAD;
113 use std::collections::HashSet;
114
115 const PUBLIC_KEY_LEN: usize = 1216;
117
118 #[test]
119 fn from_seed_is_deterministic() {
120 let a = SecretKey::from_seed(&[5u8; 32]);
121 let b = SecretKey::from_seed(&[5u8; 32]);
122
123 assert_eq!(a, b);
124 assert_eq!(a.public_key(), b.public_key());
125 }
126
127 #[test]
128 fn different_seeds_produce_different_keys() {
129 let a = SecretKey::from_seed(&[1u8; 32]);
130 let b = SecretKey::from_seed(&[2u8; 32]);
131
132 assert_ne!(a.public_key(), b.public_key());
133 }
134
135 #[test]
136 fn generate_produces_distinct_keys() {
137 let mut seen = HashSet::new();
138 for _ in 0..32 {
139 let pk = SecretKey::generate().unwrap().public_key();
140 assert!(seen.insert(pk.to_bytes()), "generated key seed collided");
141 }
142 assert_eq!(seen.len(), 32);
143 }
144
145 #[test]
146 fn public_key_bytes_roundtrip() {
147 let pk = SecretKey::from_seed(&[2u8; 32]).public_key();
148
149 let bytes = pk.to_bytes();
150 assert_eq!(bytes.len(), PUBLIC_KEY_LEN);
151 let Ok(parsed) = PublicKey::from_bytes(&bytes) else {
152 unreachable!("bytes produced by to_bytes must parse back")
153 };
154 assert_eq!(parsed, pk);
155 }
156
157 #[test]
158 fn from_bytes_rejects_wrong_length() {
159 assert_eq!(PublicKey::from_bytes(&[]), Err(Error::KeyFormat));
160 assert_eq!(PublicKey::from_bytes(&[0u8; 32]), Err(Error::KeyFormat));
161 assert_eq!(
162 PublicKey::from_bytes(&[0u8; PUBLIC_KEY_LEN + 1]),
163 Err(Error::KeyFormat)
164 );
165 }
166
167 #[test]
168 fn debug_redacts_secret() {
169 let sk = SecretKey::from_seed(&[42u8; 32]);
170
171 assert_eq!(format!("{sk:?}"), "SecretKey(REDACTED)");
172 }
173
174 #[test]
175 fn display_encodes_public_key_as_base64() {
176 let pk = SecretKey::from_seed(&[1u8; 32]).public_key();
177
178 let Ok(decoded) = STANDARD_NO_PAD.decode(pk.to_string()) else {
179 unreachable!("Display output must be valid base64")
180 };
181
182 assert_eq!(decoded, pk.to_bytes());
183 }
184}