reach_encryption/
symmetric.rs

1// SPDX-FileCopyrightText: 2023—2025 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4//! Symmetric encryption using XChaCha20-Poly1305.
5//!
6//! Supports both automatic nonce generation and explicit nonce usage for
7//! different use cases.
8
9use chacha20poly1305::{AeadCore, KeyInit, XChaCha20Poly1305, XNonce, aead::Aead};
10use rand_core::CryptoRngCore;
11use reach_core::{ProstDecode, ProstEncode, ProstEncodeOwned, error, memory::Key};
12#[cfg(any(feature = "reachable", feature = "attestant"))]
13use reach_core::{memory::SharedSecretKeys, storage::GenericVault};
14
15use super::*;
16
17/// Decrypt ciphertext using XChaCha20-Poly1305 and decode the result.
18///
19/// Performs authenticated decryption and then decodes the resulting bytes using
20/// Protocol Buffers decoding.
21pub(crate) fn decrypt<D>(
22    cipher: &XChaCha20Poly1305,
23    nonce: &XNonce,
24    ciphertext: &[u8],
25) -> Result<D, error::CryptError>
26where
27    D: ProstDecode,
28{
29    let plaintext = cipher.decrypt(nonce, ciphertext)?;
30
31    Ok(D::decode(plaintext)?)
32}
33
34/// Decryptable when provided with an explicit nonce.
35///
36/// For cases where the nonce is provided separately from the ciphertext.
37pub trait DecryptableWithNonce<E> {
38    /// Decrypt ciphertext using a provided nonce.
39    fn decrypt_with_nonce(
40        key: &Key,
41        nonce: &XNonce,
42        encrypted: &E,
43    ) -> Result<Self, error::CryptError>
44    where
45        E: Ciphertext,
46        Self: ProstDecode,
47    {
48        let cipher = XChaCha20Poly1305::new(key.as_ref());
49
50        decrypt(&cipher, nonce, encrypted.ciphertext())
51    }
52}
53
54/// Decryptable from data structures that include nonces.
55///
56/// This is the main decryption trait for data structures that carry their
57/// own nonce, providing a convenient interface for symmetric decryption.
58pub trait Decryptable<E> {
59    /// Decrypt using a key and encrypted data that includes its own nonce.
60    fn decrypt(key: &Key, encrypted: &E) -> Result<Self, error::CryptError>
61    where
62        E: Ciphertext + Nonce,
63        Self: ProstDecode,
64    {
65        let cipher = XChaCha20Poly1305::new(key.as_ref());
66
67        decrypt(&cipher, encrypted.nonce(), encrypted.ciphertext())
68    }
69
70    /// Decrypt using a pre-initialized cipher instance.
71    ///
72    /// Useful when you already have a cipher instance and want to avoid the
73    /// overhead of re-creating it.
74    fn decrypt_with_cipher(
75        cipher: &XChaCha20Poly1305,
76        encrypted: &E,
77    ) -> Result<Self, error::CryptError>
78    where
79        E: Ciphertext + Nonce,
80        Self: ProstDecode,
81    {
82        decrypt(cipher, encrypted.nonce(), encrypted.ciphertext())
83    }
84}
85
86/// Encrypt plaintext using XChaCha20-Poly1305 with automatic nonce generation.
87///
88/// Generates a random nonce and encrypts the provided plaintext, returning an
89/// encrypted data structure that includes both the nonce and ciphertext.
90fn encrypt<F>(
91    key: &Key,
92    csprng: impl CryptoRngCore,
93    plaintext: &[u8],
94) -> Result<F, error::CryptError>
95where
96    F: From<(XNonce, Vec<u8>)>,
97{
98    let cipher = XChaCha20Poly1305::new(key.as_ref());
99    let nonce = XChaCha20Poly1305::generate_nonce(csprng);
100
101    Ok(F::from((nonce, cipher.encrypt(&nonce, plaintext)?)))
102}
103
104/// Encryptable using symmetric encryption.
105///
106/// Provides multiple encryption methods for data structures, supporting both
107/// automatic nonce generation and explicit nonce usage. The `O` parameter
108/// represents the output type after encryption.
109pub trait Encryptable<O> {
110    /// Encrypt this data structure with automatic nonce generation.
111    fn encrypt<F>(&self, key: &Key, csprng: impl CryptoRngCore) -> Result<F, error::CryptError>
112    where
113        F: From<(XNonce, Vec<u8>)>,
114        Self: ProstEncode + Decryptable<O> + Decryptable<F>,
115    {
116        encrypt(key, csprng, &self.encode_to_vec())
117    }
118
119    /// Encrypt this data structure by taking ownership, with automatic nonce generation.
120    fn encrypt_owned<F>(self, key: &Key, csprng: impl CryptoRngCore) -> Result<F, error::CryptError>
121    where
122        F: From<(XNonce, Vec<u8>)>,
123        Self: ProstEncodeOwned + Decryptable<O> + Decryptable<F>,
124    {
125        encrypt(key, csprng, &self.encode_to_vec())
126    }
127
128    /// Encrypt this data structure using a provided nonce.
129    ///
130    /// For instances where nonces are managed externally.
131    ///
132    /// # Security
133    ///
134    /// Never reuse the same nonce with the same key, as this breaks semantic security.
135    fn encrypt_owned_with_nonce<F>(self, key: &Key, nonce: &XNonce) -> Result<F, error::CryptError>
136    where
137        F: From<Vec<u8>>,
138        Self: ProstEncodeOwned + DecryptableWithNonce<O> + Decryptable<F> + Sized,
139    {
140        let plaintext = self.encode_to_vec();
141        let cipher = XChaCha20Poly1305::new(key.as_ref());
142
143        Ok(F::from(cipher.encrypt(nonce, plaintext.as_slice())?))
144    }
145}
146
147#[cfg(any(feature = "reaching", feature = "reachable"))]
148mod reaching_reachable {
149    use reach_core::{
150        memory::{Credentials, Message, MessageVaultPassport},
151        wire::{
152            CredentialVault, Envelope, EnvelopeId, MessageVault, MessageVaultId, MessageVaultSeed,
153            SealedEnvelopeId, SealedMessageVaultId,
154        },
155    };
156
157    use super::*;
158
159    ciphertext_nonce!(Envelope, nonce, message_vault_passport_ciphertext);
160    ciphertext_nonce!(MessageVault, message_ciphertext);
161
162    impl Encryptable<MessageVault> for Message {}
163    impl Encryptable<Envelope> for MessageVaultPassport {}
164
165    impl Decryptable<CredentialVault> for Credentials {}
166    impl DecryptableWithNonce<MessageVault> for Message {}
167    impl Decryptable<Envelope> for MessageVaultPassport {}
168    impl Decryptable<MessageVaultSeed> for Message {}
169    impl Decryptable<(XNonce, Vec<u8>)> for MessageVaultPassport {}
170    impl Decryptable<SealedEnvelopeId> for EnvelopeId {}
171    impl Decryptable<SealedMessageVaultId> for MessageVaultId {}
172}
173
174#[cfg(any(feature = "reachable", feature = "attestant"))]
175mod reachable_attestant {
176    use super::*;
177
178    ciphertext_nonce!(GenericVault, nonce, ciphertext);
179
180    impl Encryptable<GenericVault> for SharedSecretKeys {}
181    impl Encryptable<GenericVault> for Key {}
182}
183
184#[cfg(feature = "reachable")]
185mod reachable {
186    use super::*;
187
188    impl Decryptable<GenericVault> for SharedSecretKeys {}
189    impl Decryptable<GenericVault> for Key {}
190}