1use base64::engine::general_purpose::STANDARD as B64;
8use base64::Engine as _;
9use crypto_box::{PublicKey, SecretKey};
10use sha2::{Digest, Sha256};
11use zeroize::{Zeroize, ZeroizeOnDrop};
12
13pub use zlayer_types::secrets::sealed::{RecipientPublicKey, SealedError, SealedSecret};
14
15#[derive(Clone, Zeroize, ZeroizeOnDrop)]
22pub struct RecipientPrivateKey([u8; 32]);
23
24impl std::fmt::Debug for RecipientPrivateKey {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 f.debug_tuple("RecipientPrivateKey")
27 .field(&"<redacted>")
28 .finish()
29 }
30}
31
32impl RecipientPrivateKey {
33 #[must_use]
35 pub fn generate() -> (RecipientPrivateKey, RecipientPublicKey) {
36 let sk = SecretKey::generate(&mut crypto_box::aead::OsRng);
37 let pk = sk.public_key();
38 let sk_bytes: [u8; 32] = sk.to_bytes();
39 let pk_bytes: [u8; 32] = pk.as_bytes().to_owned();
40 (Self(sk_bytes), RecipientPublicKey::from_bytes(pk_bytes))
41 }
42
43 #[must_use]
45 pub fn from_bytes(b: [u8; 32]) -> Self {
46 Self(b)
47 }
48
49 pub fn from_base64(s: &str) -> Result<Self, SealedError> {
55 let bytes = B64.decode(s)?;
56 if bytes.len() != 32 {
57 return Err(SealedError::InvalidLength(bytes.len()));
58 }
59 let mut buf = [0u8; 32];
60 buf.copy_from_slice(&bytes);
61 Ok(Self(buf))
62 }
63
64 #[must_use]
69 pub fn to_base64(&self) -> String {
70 B64.encode(self.0)
71 }
72
73 #[must_use]
75 pub fn public_key(&self) -> RecipientPublicKey {
76 let sk = SecretKey::from_bytes(self.0);
77 let pk = sk.public_key();
78 RecipientPublicKey::from_bytes(pk.as_bytes().to_owned())
79 }
80}
81
82pub fn seal(plaintext: &[u8], recipient_pub: &RecipientPublicKey) -> Result<String, SealedError> {
90 let pk = PublicKey::from(*recipient_pub.as_bytes());
91 let ct = pk
92 .seal(&mut crypto_box::aead::OsRng, plaintext)
93 .map_err(|_| SealedError::Encrypt)?;
94 Ok(B64.encode(ct))
95}
96
97pub fn open(
104 ciphertext_b64: &str,
105 recipient_priv: &RecipientPrivateKey,
106) -> Result<Vec<u8>, SealedError> {
107 let bytes = B64.decode(ciphertext_b64)?;
108 let sk = SecretKey::from_bytes(recipient_priv.0);
109 sk.unseal(&bytes).map_err(|_| SealedError::Decrypt)
110}
111
112pub fn seal_raw(
126 plaintext: &[u8],
127 recipient_pub: &RecipientPublicKey,
128) -> Result<Vec<u8>, SealedError> {
129 let pk = PublicKey::from(*recipient_pub.as_bytes());
130 pk.seal(&mut crypto_box::aead::OsRng, plaintext)
131 .map_err(|_| SealedError::Encrypt)
132}
133
134pub fn open_raw(
141 ciphertext: &[u8],
142 recipient_priv: &RecipientPrivateKey,
143) -> Result<Vec<u8>, SealedError> {
144 let sk = SecretKey::from_bytes(recipient_priv.0);
145 sk.unseal(ciphertext).map_err(|_| SealedError::Decrypt)
146}
147
148#[must_use]
153pub fn fingerprint(public_key: &RecipientPublicKey) -> String {
154 let mut hasher = Sha256::new();
155 hasher.update(public_key.as_bytes());
156 let digest = hasher.finalize();
157 format!("sha256:{}", hex::encode(&digest[..8]))
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use rand::rngs::OsRng;
164 use rand::TryRngCore;
165
166 #[test]
167 fn roundtrip() {
168 let (sk, pk) = RecipientPrivateKey::generate();
169
170 let mut plaintext = [0u8; 32];
171 OsRng.try_fill_bytes(&mut plaintext).expect("OS RNG failed");
172
173 let ct = seal(&plaintext, &pk).expect("seal");
174 let pt = open(&ct, &sk).expect("open");
175 assert_eq!(pt, plaintext);
176 }
177
178 #[test]
179 fn tamper_byte_fails() {
180 let (sk, pk) = RecipientPrivateKey::generate();
181 let plaintext = b"super-secret-payload";
182
183 let ct_b64 = seal(plaintext, &pk).expect("seal");
184
185 let mut bytes = B64.decode(&ct_b64).expect("decode");
187 let last = bytes.last_mut().expect("non-empty ciphertext");
188 *last ^= 0xFF;
189 let tampered = B64.encode(&bytes);
190
191 let result = open(&tampered, &sk);
192 assert!(matches!(result, Err(SealedError::Decrypt)));
193 }
194
195 #[test]
196 fn wrong_key_fails() {
197 let (_sk_a, pk_a) = RecipientPrivateKey::generate();
198 let (sk_b, _pk_b) = RecipientPrivateKey::generate();
199
200 let plaintext = b"sealed to A only";
201 let ct = seal(plaintext, &pk_a).expect("seal");
202
203 let result = open(&ct, &sk_b);
204 assert!(result.is_err());
205 }
206
207 #[test]
208 fn fingerprint_stable() {
209 let (_sk, pk) = RecipientPrivateKey::generate();
210 let f1 = fingerprint(&pk);
211 let f2 = fingerprint(&pk);
212 assert_eq!(f1, f2);
213 assert!(f1.starts_with("sha256:"));
214 }
215
216 #[test]
217 fn pubkey_base64_roundtrip() {
218 let (_sk, pk) = RecipientPrivateKey::generate();
219 let s = pk.to_base64();
220 let pk2 = RecipientPublicKey::from_base64(&s).expect("decode");
221 assert_eq!(pk, pk2);
222 }
223}