encryption/encryptor/
sodium_xchacha20.rs1use chacha20poly1305::aead::AeadInPlace;
9use chacha20poly1305::{KeyInit, Tag, XChaCha20Poly1305, XNonce};
10use rand_core::{OsRng, RngCore};
11
12use crate::contract::{Identified, SymmetricCipher};
13use crate::error::{Error, Result};
14use crate::key::Key;
15
16const LABEL: &str = "XChaCha20-Poly1305";
17const PREFIX: &[u8] = b"v1";
18const NONCE_LEN: usize = 24;
19const TAG_LEN: usize = 16;
20
21#[derive(Debug)]
23pub struct SodiumXChaCha20Encryptor {
24 key: Key<32>,
25}
26
27impl SodiumXChaCha20Encryptor {
28 pub const IDENTIFIER: &'static str = "sodium-xchacha20";
29
30 pub fn new(key: [u8; 32]) -> Self {
31 Self { key: Key::new(key) }
32 }
33
34 pub fn from_slice(key: &[u8]) -> Result<Self> {
35 Ok(Self {
36 key: Key::from_slice(key, LABEL)?,
37 })
38 }
39}
40
41impl Identified for SodiumXChaCha20Encryptor {
42 fn identifier(&self) -> &str {
43 Self::IDENTIFIER
44 }
45}
46
47impl SymmetricCipher for SodiumXChaCha20Encryptor {
48 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
49 let cipher = XChaCha20Poly1305::new_from_slice(self.key.as_bytes())
50 .expect("key length is fixed at 32");
51
52 let mut nonce = [0u8; NONCE_LEN];
53 OsRng.fill_bytes(&mut nonce);
54
55 let mut buffer = plaintext.to_vec();
56 let tag = cipher
57 .encrypt_in_place_detached(XNonce::from_slice(&nonce), b"", &mut buffer)
58 .map_err(|_| Error::EncryptionFailed { label: LABEL })?;
59
60 let mut blob = Vec::with_capacity(PREFIX.len() + NONCE_LEN + buffer.len() + TAG_LEN);
62 blob.extend_from_slice(PREFIX);
63 blob.extend_from_slice(&nonce);
64 blob.extend_from_slice(&buffer);
65 blob.extend_from_slice(&tag);
66 Ok(blob)
67 }
68
69 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
70 let body = ciphertext
71 .strip_prefix(PREFIX)
72 .ok_or(Error::InvalidPrefix { label: LABEL })?;
73 if body.len() < NONCE_LEN + TAG_LEN {
74 return Err(Error::TooShort { label: LABEL });
75 }
76
77 let (nonce, rest) = body.split_at(NONCE_LEN);
78 let (ct, tag) = rest.split_at(rest.len() - TAG_LEN);
79
80 let cipher = XChaCha20Poly1305::new_from_slice(self.key.as_bytes())
81 .expect("key length is fixed at 32");
82 let mut buffer = ct.to_vec();
83 cipher
84 .decrypt_in_place_detached(
85 XNonce::from_slice(nonce),
86 b"",
87 &mut buffer,
88 Tag::from_slice(tag),
89 )
90 .map_err(|_| Error::DecryptionFailed { label: LABEL })?;
91 Ok(buffer)
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn roundtrip_and_tamper_detection() {
101 let encryptor = SodiumXChaCha20Encryptor::new([9u8; 32]);
102 let blob = encryptor.encrypt(b"data").unwrap();
103 assert_eq!(blob.len(), 2 + NONCE_LEN + 4 + TAG_LEN);
104 assert_eq!(encryptor.decrypt(&blob).unwrap(), b"data");
105
106 let mut tampered = blob.clone();
107 let tag_start = tampered.len() - TAG_LEN;
108 tampered[tag_start] ^= 0x01;
109 assert_eq!(
110 encryptor.decrypt(&tampered).unwrap_err(),
111 Error::DecryptionFailed { label: LABEL }
112 );
113 }
114
115 #[test]
116 fn wrong_key_fails() {
117 let blob = SodiumXChaCha20Encryptor::new([9u8; 32])
118 .encrypt(b"x")
119 .unwrap();
120 assert_eq!(
121 SodiumXChaCha20Encryptor::new([1u8; 32])
122 .decrypt(&blob)
123 .unwrap_err(),
124 Error::DecryptionFailed { label: LABEL }
125 );
126 }
127
128 #[test]
129 fn bad_prefix_and_short_blob() {
130 let encryptor = SodiumXChaCha20Encryptor::new([9u8; 32]);
131 assert_eq!(
132 encryptor.decrypt(b"nope").unwrap_err(),
133 Error::InvalidPrefix { label: LABEL }
134 );
135 assert_eq!(
136 encryptor.decrypt(b"v1tiny").unwrap_err(),
137 Error::TooShort { label: LABEL }
138 );
139 }
140}