Skip to main content

exo_root/
seal.rs

1//! Share sealing and pairwise round-two payload protection.
2
3use argon2::Argon2;
4use chacha20poly1305::{
5    KeyInit, XChaCha20Poly1305, XNonce,
6    aead::{Aead, Payload},
7};
8use hkdf::Hkdf;
9use serde::{Deserialize, Serialize};
10use sha2::Sha256;
11use x25519_dalek::{PublicKey as X25519PublicKey, StaticSecret};
12use zeroize::Zeroize;
13
14use crate::{Result, RootError};
15
16/// AEAD-wrapped certifier share artifact.
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct SealedShare {
19    /// Argon2id salt.
20    pub salt: Vec<u8>,
21    /// XChaCha20-Poly1305 nonce.
22    pub nonce: [u8; 24],
23    /// Ciphertext and tag.
24    pub ciphertext: Vec<u8>,
25}
26
27/// Recipient-bound encrypted payload for DKG round two exchange.
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct PairwiseEncryptedPayload {
30    /// XChaCha20-Poly1305 nonce.
31    pub nonce: [u8; 24],
32    /// Ciphertext and tag.
33    pub ciphertext: Vec<u8>,
34}
35
36fn chacha_from_key(key: &[u8; 32]) -> XChaCha20Poly1305 {
37    XChaCha20Poly1305::new(key.into())
38}
39
40fn derive_sealing_key(passphrase: &[u8], salt: &[u8]) -> Result<[u8; 32]> {
41    let mut key = [0u8; 32];
42    Argon2::default()
43        .hash_password_into(passphrase, salt, &mut key)
44        .map_err(protection_error)?;
45    Ok(key)
46}
47
48fn derive_pairwise_key(
49    local_secret: &[u8; 32],
50    peer_public: &[u8; 32],
51    associated_data: &[u8],
52) -> Result<[u8; 32]> {
53    let secret = StaticSecret::from(*local_secret);
54    let peer_public = X25519PublicKey::from(*peer_public);
55    let shared = secret.diffie_hellman(&peer_public);
56    let hkdf = Hkdf::<Sha256>::new(Some(associated_data), shared.as_bytes());
57    let mut key = [0u8; 32];
58    hkdf.expand(b"EXOCHAIN_ROOT_PAIRWISE_V1", &mut key)
59        .map_err(protection_error)?;
60    Ok(key)
61}
62
63fn protection_error(error: impl core::fmt::Display) -> RootError {
64    RootError::ProtectionFailed {
65        reason: error.to_string(),
66    }
67}
68
69/// Seal one serialized share artifact with passphrase-derived AEAD.
70pub fn seal_share(
71    share_bytes: &[u8],
72    passphrase: &[u8],
73    associated_data: &[u8],
74    salt: &[u8; 16],
75    nonce: &[u8; 24],
76) -> Result<SealedShare> {
77    let mut key = derive_sealing_key(passphrase, salt)?;
78    let cipher = chacha_from_key(&key);
79    let ciphertext = cipher
80        .encrypt(
81            XNonce::from_slice(nonce),
82            Payload {
83                msg: share_bytes,
84                aad: associated_data,
85            },
86        )
87        .map_err(|_| protection_message("share encryption failed"))?;
88    key.zeroize();
89    Ok(SealedShare {
90        salt: salt.to_vec(),
91        nonce: *nonce,
92        ciphertext,
93    })
94}
95
96/// Open one sealed share artifact.
97pub fn unseal_share(
98    sealed: &SealedShare,
99    passphrase: &[u8],
100    associated_data: &[u8],
101) -> Result<Vec<u8>> {
102    let mut key = derive_sealing_key(passphrase, sealed.salt.as_slice())?;
103    let cipher = chacha_from_key(&key);
104    let plaintext = cipher
105        .decrypt(
106            XNonce::from_slice(&sealed.nonce),
107            Payload {
108                msg: sealed.ciphertext.as_slice(),
109                aad: associated_data,
110            },
111        )
112        .map_err(|_| protection_message("share opening failed"))?;
113    key.zeroize();
114    Ok(plaintext)
115}
116
117/// Encrypt a DKG round-two payload for exactly one recipient.
118pub fn encrypt_pairwise_payload(
119    sender_transport_secret: &[u8; 32],
120    recipient_transport_public: &[u8; 32],
121    payload: &[u8],
122    associated_data: &[u8],
123    nonce: &[u8; 24],
124) -> Result<PairwiseEncryptedPayload> {
125    let mut key = derive_pairwise_key(
126        sender_transport_secret,
127        recipient_transport_public,
128        associated_data,
129    )?;
130    let cipher = chacha_from_key(&key);
131    let ciphertext = cipher
132        .encrypt(
133            XNonce::from_slice(nonce),
134            Payload {
135                msg: payload,
136                aad: associated_data,
137            },
138        )
139        .map_err(|_| protection_message("pairwise encryption failed"))?;
140    key.zeroize();
141    Ok(PairwiseEncryptedPayload {
142        nonce: *nonce,
143        ciphertext,
144    })
145}
146
147/// Decrypt a DKG round-two payload from one sender.
148pub fn decrypt_pairwise_payload(
149    recipient_transport_secret: &[u8; 32],
150    sender_transport_public: &[u8; 32],
151    encrypted: &PairwiseEncryptedPayload,
152    associated_data: &[u8],
153) -> Result<Vec<u8>> {
154    let mut key = derive_pairwise_key(
155        recipient_transport_secret,
156        sender_transport_public,
157        associated_data,
158    )?;
159    let cipher = chacha_from_key(&key);
160    let plaintext = cipher
161        .decrypt(
162            XNonce::from_slice(&encrypted.nonce),
163            Payload {
164                msg: encrypted.ciphertext.as_slice(),
165                aad: associated_data,
166            },
167        )
168        .map_err(|_| protection_message("pairwise opening failed"))?;
169    key.zeroize();
170    Ok(plaintext)
171}
172
173fn protection_message(reason: &str) -> RootError {
174    RootError::ProtectionFailed {
175        reason: reason.to_owned(),
176    }
177}