reach_encryption/
lib.rs

1// SPDX-FileCopyrightText: 2023—2025 eaon <eaon@posteo.net>
2// SPDX-License-Identifier: EUPL-1.2
3
4#![cfg_attr(docsrs, feature(doc_auto_cfg))]
5#![allow(clippy::needless_doctest_main)]
6#![doc = include_str!("../README.md")]
7#![warn(missing_docs)]
8
9use chacha20poly1305::XNonce;
10
11#[cfg(any(feature = "reaching", feature = "reachable"))]
12pub(crate) mod aliases;
13
14#[cfg(any(feature = "reaching", feature = "reachable"))]
15mod public_key;
16#[cfg(any(feature = "reaching", feature = "reachable"))]
17pub use public_key::*;
18
19mod symmetric;
20pub use symmetric::*;
21
22/// Contains encrypted data.
23///
24/// Provides a common interface for accessing the ciphertext portion of
25/// encrypted data structures.
26pub trait Ciphertext {
27    /// Return the encrypted bytes as a slice.
28    fn ciphertext(&self) -> &[u8];
29}
30
31/// Provides nonces for encryption/decryption operations.
32pub trait Nonce {
33    /// Return the nonce used for this encryption operation.
34    fn nonce(&self) -> &XNonce;
35}
36
37/// Helper macro to implement [`Ciphertext`] and [`Nonce`] traits for encrypted
38/// data structures.
39///
40/// Generates implementations for types that store encrypted data with optional
41/// nonce fields. It supports two patterns:
42///
43/// 1. `ciphertext_nonce!(TypeName, ciphertext_field)` - Implements only
44///    [`Ciphertext`]
45/// 2. `ciphertext_nonce!(TypeName, nonce_field, ciphertext_field)` - Implements
46///    both [`Ciphertext`] and [`Nonce`]
47///
48/// Can use dot notation to access nested fields (e.g., `field.subfield`).
49macro_rules! ciphertext_nonce {
50    (
51    $type_name:ident,
52    $($ciphertext:ident).+
53    ) => {
54        impl Ciphertext for $type_name {
55            fn ciphertext(&self) -> &[u8] {
56                self.$($ciphertext).+.as_slice()
57            }
58        }
59    };
60    (
61        $type_name:ident,
62        $($nonce:ident).+,
63        $($ciphertext:ident).+
64    ) => {
65        ciphertext_nonce!($type_name, $($ciphertext).+);
66
67        impl Nonce for $type_name {
68            fn nonce(&self) -> &XNonce {
69                &self.$($nonce).+
70            }
71        }
72    };
73}
74
75pub(crate) use ciphertext_nonce;
76
77#[cfg(any(feature = "reaching", feature = "reachable"))]
78mod reaching_reachable {
79    use super::*;
80
81    use reach_core::{
82        ParticipantPublicKeys, error,
83        memory::{Credentials, MessageVaultPassport, OpenedEnvelope},
84        wire::{CredentialVault, Envelope, Salts},
85    };
86
87    /// Open and authenticate an envelope.
88    ///
89    /// This function performs the complete envelope opening process:
90    ///
91    /// 1. Trial decryption of [`CredentialVault`]s to find the matching one
92    /// 2. Decryption of the [`MessageVaultPassport`] using the [`Credentials`] key
93    /// 3. Authentication of both the `Credentials` and the verifying keys
94    /// 4. MAC verification for additional security
95    ///
96    /// To open an envelope, a participant must:
97    ///
98    /// - Successfully decrypt one of the `CredentialVaults`
99    /// - Verify that `Credentials` signatures are valid
100    /// - The included verifying keys have a valid MAC
101    ///
102    /// All checks must pass for the envelope to be considered authentic.
103    pub fn open_envelope<D, P>(
104        secret_keys: &D,
105        public_keys: &P,
106        envelope: &Envelope,
107        salts: &Salts,
108    ) -> Result<OpenedEnvelope, error::CryptError>
109    where
110        D: PublicKeyDecrypter<CredentialVault>,
111        P: ParticipantPublicKeys,
112    {
113        let (credential_vault, credentials, mac_shared_secret) = envelope
114            .credential_vaults
115            .iter()
116            .fold(None, |acc, cv| match acc {
117                None => secret_keys
118                    .decrypt::<Credentials>(cv, salts)
119                    .ok()
120                    .map(|c| (cv, c, secret_keys.mac_shared_secret(cv, salts))),
121                Some(_) => acc,
122            })
123            .ok_or(error::CryptError::DecryptionFailed)?;
124
125        let message_vault_passport = MessageVaultPassport::decrypt(&credentials.key, envelope)?;
126
127        if !(credentials.authenticate(
128            credential_vault,
129            public_keys,
130            &message_vault_passport.verifying_keys,
131        ) && message_vault_passport
132            .verifying_keys
133            .verify_mac(&mac_shared_secret, &credentials.verifying_keys_mac)
134            .is_ok())
135        {
136            return Err(error::CryptError::AuthenticationFailed);
137        }
138
139        Ok(OpenedEnvelope {
140            id: envelope.id,
141            message_vault_passport,
142            credentials,
143        })
144    }
145}
146
147#[cfg(any(feature = "reaching", feature = "reachable"))]
148pub use reaching_reachable::*;