1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18pub struct SealedShare {
19 pub salt: Vec<u8>,
21 pub nonce: [u8; 24],
23 pub ciphertext: Vec<u8>,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct PairwiseEncryptedPayload {
30 pub nonce: [u8; 24],
32 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
69pub 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
96pub 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
117pub 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
147pub 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}