1use base64::engine::general_purpose::STANDARD as B64;
8use base64::Engine as _;
9use crypto_box::{PublicKey, SecretKey};
10use serde::{Deserialize, Serialize};
11use sha2::{Digest, Sha256};
12use zeroize::{Zeroize, ZeroizeOnDrop};
13
14#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct SealedSecret {
20 pub name: String,
22 pub version: u32,
24 pub key_id: String,
26 pub ciphertext_b64: String,
28}
29
30#[derive(Debug, thiserror::Error)]
32pub enum SealedError {
33 #[error("base64 decode failed: {0}")]
35 Decode(#[from] base64::DecodeError),
36 #[error("ciphertext invalid length: {0}")]
38 InvalidLength(usize),
39 #[error("encryption failed")]
41 Encrypt,
42 #[error("decryption failed")]
44 Decrypt,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct RecipientPublicKey([u8; 32]);
50
51impl RecipientPublicKey {
52 #[must_use]
54 pub fn from_bytes(b: [u8; 32]) -> Self {
55 Self(b)
56 }
57
58 pub fn from_base64(s: &str) -> Result<Self, SealedError> {
64 let bytes = B64.decode(s)?;
65 if bytes.len() != 32 {
66 return Err(SealedError::InvalidLength(bytes.len()));
67 }
68 let mut buf = [0u8; 32];
69 buf.copy_from_slice(&bytes);
70 Ok(Self(buf))
71 }
72
73 #[must_use]
75 pub fn to_base64(&self) -> String {
76 B64.encode(self.0)
77 }
78
79 #[must_use]
81 pub fn as_bytes(&self) -> &[u8; 32] {
82 &self.0
83 }
84}
85
86#[derive(Clone, Zeroize, ZeroizeOnDrop)]
91pub struct RecipientPrivateKey([u8; 32]);
92
93impl RecipientPrivateKey {
94 #[must_use]
96 pub fn generate() -> (RecipientPrivateKey, RecipientPublicKey) {
97 let sk = SecretKey::generate(&mut crypto_box::aead::OsRng);
98 let pk = sk.public_key();
99 let sk_bytes: [u8; 32] = sk.to_bytes();
100 let pk_bytes: [u8; 32] = pk.as_bytes().to_owned();
101 (Self(sk_bytes), RecipientPublicKey(pk_bytes))
102 }
103
104 #[must_use]
106 pub fn from_bytes(b: [u8; 32]) -> Self {
107 Self(b)
108 }
109
110 pub fn from_base64(s: &str) -> Result<Self, SealedError> {
116 let bytes = B64.decode(s)?;
117 if bytes.len() != 32 {
118 return Err(SealedError::InvalidLength(bytes.len()));
119 }
120 let mut buf = [0u8; 32];
121 buf.copy_from_slice(&bytes);
122 Ok(Self(buf))
123 }
124
125 #[must_use]
130 pub fn to_base64(&self) -> String {
131 B64.encode(self.0)
132 }
133
134 #[must_use]
136 pub fn public_key(&self) -> RecipientPublicKey {
137 let sk = SecretKey::from_bytes(self.0);
138 let pk = sk.public_key();
139 RecipientPublicKey(pk.as_bytes().to_owned())
140 }
141}
142
143pub fn seal(plaintext: &[u8], recipient_pub: &RecipientPublicKey) -> Result<String, SealedError> {
151 let pk = PublicKey::from(*recipient_pub.as_bytes());
152 let ct = pk
153 .seal(&mut crypto_box::aead::OsRng, plaintext)
154 .map_err(|_| SealedError::Encrypt)?;
155 Ok(B64.encode(ct))
156}
157
158pub fn open(
165 ciphertext_b64: &str,
166 recipient_priv: &RecipientPrivateKey,
167) -> Result<Vec<u8>, SealedError> {
168 let bytes = B64.decode(ciphertext_b64)?;
169 let sk = SecretKey::from_bytes(recipient_priv.0);
170 sk.unseal(&bytes).map_err(|_| SealedError::Decrypt)
171}
172
173#[must_use]
178pub fn fingerprint(public_key: &RecipientPublicKey) -> String {
179 let mut hasher = Sha256::new();
180 hasher.update(public_key.as_bytes());
181 let digest = hasher.finalize();
182 format!("sha256:{}", hex::encode(&digest[..8]))
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use rand::rngs::OsRng;
189 use rand::TryRngCore;
190
191 #[test]
192 fn roundtrip() {
193 let (sk, pk) = RecipientPrivateKey::generate();
194
195 let mut plaintext = [0u8; 32];
196 OsRng.try_fill_bytes(&mut plaintext).expect("OS RNG failed");
197
198 let ct = seal(&plaintext, &pk).expect("seal");
199 let pt = open(&ct, &sk).expect("open");
200 assert_eq!(pt, plaintext);
201 }
202
203 #[test]
204 fn tamper_byte_fails() {
205 let (sk, pk) = RecipientPrivateKey::generate();
206 let plaintext = b"super-secret-payload";
207
208 let ct_b64 = seal(plaintext, &pk).expect("seal");
209
210 let mut bytes = B64.decode(&ct_b64).expect("decode");
212 let last = bytes.last_mut().expect("non-empty ciphertext");
213 *last ^= 0xFF;
214 let tampered = B64.encode(&bytes);
215
216 let result = open(&tampered, &sk);
217 assert!(matches!(result, Err(SealedError::Decrypt)));
218 }
219
220 #[test]
221 fn wrong_key_fails() {
222 let (_sk_a, pk_a) = RecipientPrivateKey::generate();
223 let (sk_b, _pk_b) = RecipientPrivateKey::generate();
224
225 let plaintext = b"sealed to A only";
226 let ct = seal(plaintext, &pk_a).expect("seal");
227
228 let result = open(&ct, &sk_b);
229 assert!(result.is_err());
230 }
231
232 #[test]
233 fn fingerprint_stable() {
234 let (_sk, pk) = RecipientPrivateKey::generate();
235 let f1 = fingerprint(&pk);
236 let f2 = fingerprint(&pk);
237 assert_eq!(f1, f2);
238 assert!(f1.starts_with("sha256:"));
239 }
240
241 #[test]
242 fn pubkey_base64_roundtrip() {
243 let (_sk, pk) = RecipientPrivateKey::generate();
244 let s = pk.to_base64();
245 let pk2 = RecipientPublicKey::from_base64(&s).expect("decode");
246 assert_eq!(pk, pk2);
247 }
248}