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
21pub struct SodiumXChaCha20Encryptor {
27 cipher: XChaCha20Poly1305,
28}
29
30impl SodiumXChaCha20Encryptor {
31 pub const IDENTIFIER: &'static str = "sodium-xchacha20";
32
33 pub fn new(key: [u8; 32]) -> Self {
34 Self {
35 cipher: XChaCha20Poly1305::new_from_slice(&key).expect("key length is fixed at 32"),
36 }
37 }
38
39 pub fn from_slice(key: &[u8]) -> Result<Self> {
40 let key = Key::<32>::from_slice(key, LABEL)?;
42 Ok(Self {
43 cipher: XChaCha20Poly1305::new_from_slice(key.as_bytes())
44 .expect("key length is fixed at 32"),
45 })
46 }
47}
48
49impl std::fmt::Debug for SodiumXChaCha20Encryptor {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 f.debug_struct("SodiumXChaCha20Encryptor")
54 .field("identifier", &Self::IDENTIFIER)
55 .finish_non_exhaustive()
56 }
57}
58
59impl Identified for SodiumXChaCha20Encryptor {
60 fn identifier(&self) -> &str {
61 Self::IDENTIFIER
62 }
63}
64
65impl SymmetricCipher for SodiumXChaCha20Encryptor {
66 fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
67 let mut nonce = [0u8; NONCE_LEN];
68 OsRng.fill_bytes(&mut nonce);
69
70 let mut buffer = plaintext.to_vec();
71 let tag = self
72 .cipher
73 .encrypt_in_place_detached(XNonce::from_slice(&nonce), b"", &mut buffer)
74 .map_err(|_| Error::EncryptionFailed { label: LABEL })?;
75
76 let mut blob = Vec::with_capacity(PREFIX.len() + NONCE_LEN + buffer.len() + TAG_LEN);
78 blob.extend_from_slice(PREFIX);
79 blob.extend_from_slice(&nonce);
80 blob.extend_from_slice(&buffer);
81 blob.extend_from_slice(&tag);
82 Ok(blob)
83 }
84
85 fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>> {
86 let body = ciphertext
87 .strip_prefix(PREFIX)
88 .ok_or(Error::InvalidPrefix { label: LABEL })?;
89 if body.len() < NONCE_LEN + TAG_LEN {
90 return Err(Error::TooShort { label: LABEL });
91 }
92
93 let (nonce, rest) = body.split_at(NONCE_LEN);
94 let (ct, tag) = rest.split_at(rest.len() - TAG_LEN);
95
96 let mut buffer = ct.to_vec();
97 self.cipher
98 .decrypt_in_place_detached(
99 XNonce::from_slice(nonce),
100 b"",
101 &mut buffer,
102 Tag::from_slice(tag),
103 )
104 .map_err(|_| Error::DecryptionFailed { label: LABEL })?;
105 Ok(buffer)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn roundtrip_and_tamper_detection() {
115 let encryptor = SodiumXChaCha20Encryptor::new([9u8; 32]);
116 let blob = encryptor.encrypt(b"data").unwrap();
117 assert_eq!(blob.len(), 2 + NONCE_LEN + 4 + TAG_LEN);
118 assert_eq!(encryptor.decrypt(&blob).unwrap(), b"data");
119
120 let mut tampered = blob.clone();
121 let tag_start = tampered.len() - TAG_LEN;
122 tampered[tag_start] ^= 0x01;
123 assert_eq!(
124 encryptor.decrypt(&tampered).unwrap_err(),
125 Error::DecryptionFailed { label: LABEL }
126 );
127 }
128
129 #[test]
130 fn wrong_key_fails() {
131 let blob = SodiumXChaCha20Encryptor::new([9u8; 32])
132 .encrypt(b"x")
133 .unwrap();
134 assert_eq!(
135 SodiumXChaCha20Encryptor::new([1u8; 32])
136 .decrypt(&blob)
137 .unwrap_err(),
138 Error::DecryptionFailed { label: LABEL }
139 );
140 }
141
142 #[test]
143 fn bad_prefix_and_short_blob() {
144 let encryptor = SodiumXChaCha20Encryptor::new([9u8; 32]);
145 assert_eq!(
146 encryptor.decrypt(b"nope").unwrap_err(),
147 Error::InvalidPrefix { label: LABEL }
148 );
149 assert_eq!(
150 encryptor.decrypt(b"v1tiny").unwrap_err(),
151 Error::TooShort { label: LABEL }
152 );
153 }
154}