1use aes_gcm::aead::KeyInit;
15use aes_gcm::{Aes256Gcm, Nonce};
16use hkdf::Hkdf;
17use rand::rngs::OsRng;
18use sha2::Sha256;
19use x25519_dalek::{PublicKey as X25519PublicKey, SharedSecret, StaticSecret};
20
21#[derive(Debug, thiserror::Error)]
24pub enum E2eError {
26 #[error("ciphertext too short to contain E2E header")]
28 TooShort,
29 #[error("decryption failed (invalid ciphertext or wrong key)")]
31 DecryptionFailed,
32 #[error("encryption failed")]
34 EncryptionFailed,
35}
36
37const EPHEMERAL_PUB_SIZE: usize = 32;
40const NONCE_SIZE: usize = 12;
41const TAG_SIZE: usize = 16;
42const HEADER_SIZE: usize = EPHEMERAL_PUB_SIZE + NONCE_SIZE; fn derive_e2e_cipher(shared_secret: &SharedSecret) -> Aes256Gcm {
45 let hk = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
46 let mut okm = [0u8; 32];
47 hk.expand(b"pim-e2e-v1", &mut okm)
48 .expect("32 bytes is valid");
49 Aes256Gcm::new_from_slice(&okm).expect("32 bytes is valid")
50}
51
52pub fn x25519_from_seed(ed25519_seed: &[u8; 32]) -> StaticSecret {
60 let hk = Hkdf::<Sha256>::new(None, ed25519_seed);
61 let mut key_bytes = [0u8; 32];
62 hk.expand(b"pim-x25519-identity-v1", &mut key_bytes)
63 .expect("32 bytes is valid");
64 StaticSecret::from(key_bytes)
65}
66
67pub fn x25519_public_from_seed(ed25519_seed: &[u8; 32]) -> [u8; 32] {
69 X25519PublicKey::from(&x25519_from_seed(ed25519_seed)).to_bytes()
70}
71
72pub fn e2e_encrypt(plaintext: &[u8], gateway_x25519_pub: &[u8; 32]) -> Result<Vec<u8>, E2eError> {
76 let ephemeral_secret = StaticSecret::random_from_rng(OsRng);
78 let ephemeral_pub = X25519PublicKey::from(&ephemeral_secret);
79
80 let gateway_pub = X25519PublicKey::from(*gateway_x25519_pub);
82 let shared_secret = ephemeral_secret.diffie_hellman(&gateway_pub);
83
84 let mut nonce_bytes = [0u8; NONCE_SIZE];
86 rand::RngCore::fill_bytes(&mut OsRng, &mut nonce_bytes);
87 let nonce = Nonce::from_slice(&nonce_bytes);
88
89 let cipher = derive_e2e_cipher(&shared_secret);
91
92 let mut out = Vec::with_capacity(HEADER_SIZE + plaintext.len() + TAG_SIZE);
94 out.extend_from_slice(&ephemeral_pub.to_bytes());
95 out.extend_from_slice(&nonce_bytes);
96 out.extend_from_slice(plaintext);
97
98 use aes_gcm::aead::AeadInPlace;
99 let tag = cipher
100 .encrypt_in_place_detached(nonce, b"", &mut out[HEADER_SIZE..])
101 .map_err(|_| E2eError::EncryptionFailed)?;
102
103 out.extend_from_slice(&tag);
104
105 Ok(out)
106}
107
108pub fn e2e_decrypt<'a>(
112 ciphertext: &'a mut [u8],
113 gateway_ed25519_seed: &[u8; 32],
114) -> Result<&'a [u8], E2eError> {
115 if ciphertext.len() < HEADER_SIZE + TAG_SIZE {
116 return Err(E2eError::TooShort);
117 }
118
119 let mut ephemeral_pub_bytes = [0u8; 32];
121 ephemeral_pub_bytes.copy_from_slice(&ciphertext[..32]);
122 let ephemeral_pub = X25519PublicKey::from(ephemeral_pub_bytes);
123
124 let nonce_bytes = &ciphertext[32..44];
125
126 let gw_secret = x25519_from_seed(gateway_ed25519_seed);
128
129 let shared_secret = gw_secret.diffie_hellman(&ephemeral_pub);
131
132 use aes_gcm::aead::AeadInPlace;
134 let cipher = derive_e2e_cipher(&shared_secret);
135
136 let mut nonce_array = [0u8; 12];
137 nonce_array.copy_from_slice(nonce_bytes);
138 let nonce = Nonce::from_slice(&nonce_array);
139
140 let tag_start = ciphertext.len() - TAG_SIZE;
141 let mut tag_array = [0u8; 16];
142 tag_array.copy_from_slice(&ciphertext[tag_start..]);
143 let tag = aes_gcm::aead::Tag::<Aes256Gcm>::from_slice(&tag_array);
144
145 let (body, _) = ciphertext.split_at_mut(tag_start);
146 let (_, payload) = body.split_at_mut(HEADER_SIZE);
147
148 cipher
149 .decrypt_in_place_detached(nonce, b"", payload, tag)
150 .map_err(|_| E2eError::DecryptionFailed)?;
151
152 Ok(&ciphertext[HEADER_SIZE..tag_start])
153}
154
155pub fn e2e_decrypt_in_place(
160 buffer: &mut Vec<u8>,
161 gateway_ed25519_seed: &[u8; 32],
162) -> Result<(), E2eError> {
163 if buffer.len() < HEADER_SIZE + TAG_SIZE {
164 return Err(E2eError::TooShort);
165 }
166
167 let mut ephemeral_pub_bytes = [0u8; 32];
169 ephemeral_pub_bytes.copy_from_slice(&buffer[..32]);
170 let ephemeral_pub = X25519PublicKey::from(ephemeral_pub_bytes);
171
172 let nonce = *Nonce::from_slice(&buffer[32..44]);
173
174 let gw_secret = x25519_from_seed(gateway_ed25519_seed);
176
177 let shared_secret = gw_secret.diffie_hellman(&ephemeral_pub);
179
180 let cipher = derive_e2e_cipher(&shared_secret);
181
182 let ct_len = buffer.len() - HEADER_SIZE - TAG_SIZE;
184 let mut tag_bytes = [0u8; TAG_SIZE];
185 tag_bytes.copy_from_slice(&buffer[buffer.len() - TAG_SIZE..]);
186 let tag = aes_gcm::aead::Tag::<Aes256Gcm>::from_slice(&tag_bytes);
187
188 buffer.copy_within(HEADER_SIZE..HEADER_SIZE + ct_len, 0);
190 buffer.truncate(ct_len);
191
192 aes_gcm::aead::AeadInPlace::decrypt_in_place_detached(&cipher, &nonce, b"", buffer, tag)
193 .map_err(|_| E2eError::DecryptionFailed)?;
194
195 Ok(())
196}
197
198#[cfg(test)]
201mod tests;