Skip to main content

zlayer_types/secrets/
sealed.rs

1//! Sealed-box wire types and errors (recipient pubkey + ciphertext envelope).
2//!
3//! Lifted into `zlayer-types` so cross-crate consumers can name the wire
4//! shape without depending on `zlayer-secrets`. The actual sealed-box
5//! crypto (sealing/opening, `RecipientPrivateKey`, key-material zeroization)
6//! stays in `zlayer-secrets` and consumes these wire types from here.
7
8use base64::engine::general_purpose::STANDARD as B64;
9use base64::Engine as _;
10use serde::{Deserialize, Serialize};
11
12/// A sealed secret payload — recipient-encrypted ciphertext plus identifying metadata.
13///
14/// The ciphertext is base64-encoded (standard alphabet, with padding) so it can be
15/// transported as a string and decrypted by any libsodium-compatible client.
16#[derive(Serialize, Deserialize, Debug, Clone)]
17pub struct SealedSecret {
18    /// The name of the secret this payload corresponds to.
19    pub name: String,
20    /// The version of the secret at the time of sealing.
21    pub version: u32,
22    /// Identifier (typically a fingerprint) of the recipient public key.
23    pub key_id: String,
24    /// Standard-base64 (with padding) libsodium sealed-box ciphertext.
25    pub ciphertext_b64: String,
26}
27
28/// Errors produced by sealed-box operations.
29#[derive(Debug, thiserror::Error)]
30pub enum SealedError {
31    /// Base64 decoding of an input string failed.
32    #[error("base64 decode failed: {0}")]
33    Decode(#[from] base64::DecodeError),
34    /// The decoded byte slice did not match the expected length for its role.
35    #[error("ciphertext invalid length: {0}")]
36    InvalidLength(usize),
37    /// Sealing (encryption) failed.
38    #[error("encryption failed")]
39    Encrypt,
40    /// Opening (decryption / authentication) failed.
41    #[error("decryption failed")]
42    Decrypt,
43}
44
45/// A 32-byte X25519 recipient public key.
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct RecipientPublicKey([u8; 32]);
48
49impl RecipientPublicKey {
50    /// Construct a recipient public key directly from its 32 raw bytes.
51    #[must_use]
52    pub fn from_bytes(b: [u8; 32]) -> Self {
53        Self(b)
54    }
55
56    /// Decode a recipient public key from a standard-base64 (padded) string.
57    ///
58    /// # Errors
59    /// Returns `SealedError::Decode` if the string is not valid base64, or
60    /// `SealedError::InvalidLength` if it does not decode to exactly 32 bytes.
61    pub fn from_base64(s: &str) -> Result<Self, SealedError> {
62        let bytes = B64.decode(s)?;
63        if bytes.len() != 32 {
64            return Err(SealedError::InvalidLength(bytes.len()));
65        }
66        let mut buf = [0u8; 32];
67        buf.copy_from_slice(&bytes);
68        Ok(Self(buf))
69    }
70
71    /// Encode this public key as standard-base64 (with padding).
72    #[must_use]
73    pub fn to_base64(&self) -> String {
74        B64.encode(self.0)
75    }
76
77    /// Borrow the raw 32-byte representation.
78    #[must_use]
79    pub fn as_bytes(&self) -> &[u8; 32] {
80        &self.0
81    }
82}