ouisync/crypto/
cipher.rs

1//! Encryption / Decryption utilities.
2
3use super::{hash::Digest, password::PasswordSalt};
4use chacha20::{
5    cipher::{KeyIvInit, StreamCipher},
6    ChaCha20,
7};
8use generic_array::{sequence::GenericSequence, typenum::Unsigned};
9use hex;
10use ouisync_macros::api;
11use rand::{rngs::OsRng, CryptoRng, Rng};
12use serde::{de::Error as _, Deserialize, Deserializer, Serialize, Serializer};
13use std::{fmt, sync::Arc};
14use subtle::ConstantTimeEq;
15use thiserror::Error;
16use zeroize::{Zeroize, Zeroizing};
17
18/// Nonce
19pub(crate) type Nonce = [u8; NONCE_SIZE];
20pub(crate) const NONCE_SIZE: usize =
21    <<chacha20::Nonce as GenericSequence<_>>::Length as Unsigned>::USIZE;
22
23/// Symmetric encryption/decryption secret key.
24///
25/// Note: this implementation tries to prevent certain types of attacks by making sure the
26/// underlying sensitive key material is always stored at most in one place. This is achieved by
27/// putting it on the heap which means it is not moved when the key itself is moved which could
28/// otherwise leave a copy of the data in memory. Additionally, the data is behind a `Arc` which
29/// means the key can be cheaply cloned without actually cloning the data. Finally, the data is
30/// scrambled (overwritten with zeros) when the key is dropped to make sure it does not stay in
31/// the memory past its lifetime.
32#[derive(Clone)]
33#[api(repr(Bytes), secret)]
34pub struct SecretKey(Arc<Zeroizing<[u8; Self::SIZE]>>);
35
36impl SecretKey {
37    /// Size of the key in bytes.
38    pub const SIZE: usize = <<chacha20::Key as GenericSequence<_>>::Length as Unsigned>::USIZE;
39
40    /// Parse secret key from hexadecimal string of size 2*SIZE.
41    pub fn parse_hex(hex_str: &str) -> Result<Self, hex::FromHexError> {
42        let mut bytes = [0; Self::SIZE];
43        hex::decode_to_slice(hex_str, &mut bytes)?;
44
45        let mut key = Self::zero();
46        key.as_mut().copy_from_slice(&bytes);
47
48        bytes.zeroize();
49
50        Ok(key)
51    }
52
53    /// Generate a random secret key using the given cryptographically secure random number
54    /// generator.
55    ///
56    /// Note: this is purposefully not implemented as `impl Distribution<SecretKey> for Standard`
57    /// to enforce the additional `CryptoRng` bound.
58    pub fn generate<R: Rng + CryptoRng + ?Sized>(rng: &mut R) -> Self {
59        // Create all-zero array initially, then fill it with random bytes in place to avoid moving
60        // the array which could leave the sensitive data in memory.
61        let mut key = Self::zero();
62        rng.fill(key.as_mut());
63        key
64    }
65
66    /// Generate a random secret key using the default RNG.
67    pub fn random() -> Self {
68        Self::generate(&mut OsRng)
69    }
70
71    /// Derive a secret key from another secret key and a nonce.
72    pub fn derive_from_key(master_key: &[u8; Self::SIZE], nonce: &[u8]) -> Self {
73        let mut sub_key = Self::zero();
74
75        let mut hasher = blake3::Hasher::new_keyed(master_key);
76        hasher.update(nonce);
77        hasher.finalize_into(sub_key.as_mut().into());
78
79        sub_key
80    }
81
82    /// Derive a secret key from user's password and salt.
83    pub fn derive_from_password(user_password: &str, salt: &PasswordSalt) -> Self {
84        use argon2::{Algorithm, Argon2, ParamsBuilder, Version};
85
86        let mut result = Self::zero();
87
88        Argon2::new(
89            Algorithm::default(),
90            Version::default(),
91            // Using explicit params to preserve the output from v0.4 of argon2. See
92            // https://github.com/equalitie/ouisync/issues/144 for details.
93            ParamsBuilder::new().m_cost(4096).t_cost(3).build().unwrap(),
94        )
95        .hash_password_into(user_password.as_ref(), salt.as_ref(), result.as_mut())
96        // Note: we control the output and salt size. And the only other check that this
97        // function does is whether the password isn't too long, but that would have to be more
98        // than 0xffffffff so the `.expect` shouldn't be an issue.
99        .expect("failed to hash password");
100
101        result
102    }
103
104    // TODO: the following two functions have identical implementations. Consider replacing them
105    // with a single function (what should it be called?).
106
107    /// Encrypt a message in place without using Authenticated Encryption with Associated Data
108    pub(crate) fn encrypt_no_aead(&self, nonce: &Nonce, buffer: &mut [u8]) {
109        let mut cipher = ChaCha20::new(self.as_ref().into(), nonce.into());
110        cipher.apply_keystream(buffer)
111    }
112
113    /// Decrypt a message in place without using Authenticated Encryption with Associated Data.
114    pub(crate) fn decrypt_no_aead(&self, nonce: &Nonce, buffer: &mut [u8]) {
115        let mut cipher = ChaCha20::new(self.as_ref().into(), nonce.into());
116        cipher.apply_keystream(buffer)
117    }
118
119    /// Note this method is somewhat dangerous because if used carelessly the underlying sensitive data
120    /// can be copied or revealed.
121    pub fn as_array(&self) -> &[u8; Self::SIZE] {
122        &self.0
123    }
124
125    // Use this only for initialization.
126    fn zero() -> Self {
127        Self(Arc::new(Zeroizing::new([0; Self::SIZE])))
128    }
129
130    // Use this only for initialization. Panics if this key has more than one clone.
131    fn as_mut(&mut self) -> &mut [u8] {
132        &mut **Arc::get_mut(&mut self.0).unwrap()
133    }
134}
135
136impl TryFrom<&[u8]> for SecretKey {
137    type Error = SecretKeyLengthError;
138
139    fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
140        if slice.len() >= Self::SIZE {
141            let mut key = Self::zero();
142            key.as_mut().copy_from_slice(slice);
143            Ok(key)
144        } else {
145            Err(SecretKeyLengthError)
146        }
147    }
148}
149
150/// Note this trait is somewhat dangerous because if used carelessly the underlying sensitive data
151/// can be copied or revealed.
152impl AsRef<[u8]> for SecretKey {
153    fn as_ref(&self) -> &[u8] {
154        &self.0[..]
155    }
156}
157
158/// Note this impl uses constant-time operations (using [subtle](https://crates.io/crates/subtle))
159/// and so provides protection against software side-channel attacks.
160impl PartialEq for SecretKey {
161    fn eq(&self, other: &Self) -> bool {
162        self.as_array().ct_eq(other.as_array()).into()
163    }
164}
165
166impl Eq for SecretKey {}
167
168impl fmt::Debug for SecretKey {
169    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170        write!(f, "****")
171    }
172}
173
174impl Serialize for SecretKey {
175    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
176    where
177        S: Serializer,
178    {
179        serde_bytes::Bytes::new(self.as_ref()).serialize(s)
180    }
181}
182
183impl<'de> Deserialize<'de> for SecretKey {
184    fn deserialize<D>(d: D) -> Result<Self, D::Error>
185    where
186        D: Deserializer<'de>,
187    {
188        let bytes: &serde_bytes::Bytes = Deserialize::deserialize(d)?;
189
190        if bytes.len() != Self::SIZE {
191            return Err(D::Error::invalid_length(
192                bytes.len(),
193                &format!("{}", Self::SIZE).as_str(),
194            ));
195        }
196
197        let mut key = Self::zero();
198        key.as_mut().copy_from_slice(bytes);
199
200        Ok(key)
201    }
202}
203
204#[derive(Debug, Error)]
205#[error("invalid secret key length")]
206pub struct SecretKeyLengthError;
207
208#[cfg(test)]
209mod tests {
210
211    use super::*;
212
213    #[test]
214    fn serialize_deserialize_bincode() {
215        let orig = SecretKey::try_from(&b"abcdefghijklmnopqrstuvwxyz012345"[..]).unwrap();
216        let expected_serialized_hex =
217            "20000000000000006162636465666768696a6b6c6d6e6f707172737475767778797a303132333435";
218
219        let serialized = bincode::serialize(&orig).unwrap();
220        assert_eq!(hex::encode(&serialized), expected_serialized_hex);
221
222        let deserialized: SecretKey = bincode::deserialize(&serialized).unwrap();
223        assert_eq!(deserialized.as_ref(), orig.as_ref());
224    }
225
226    #[test]
227    fn serialize_deserialize_msgpack() {
228        let orig = SecretKey::try_from(&b"abcdefghijklmnopqrstuvwxyz012345"[..]).unwrap();
229        let expected_serialized_hex =
230            "c4206162636465666768696a6b6c6d6e6f707172737475767778797a303132333435";
231
232        let serialized = rmp_serde::to_vec(&orig).unwrap();
233        assert_eq!(hex::encode(&serialized), expected_serialized_hex);
234
235        let deserialized: SecretKey = rmp_serde::from_slice(&serialized).unwrap();
236        assert_eq!(deserialized.as_ref(), orig.as_ref());
237    }
238
239    #[test]
240    fn derive_from_password_snapshot() {
241        let test_vectors = [
242            (
243                "jxzBql3QHxENyynvh2SICH9ND",
244                "a2849a63283cbaf0fdbceb1f6479b197",
245                "c7ffa7d05f0898d71839cbd62a00a9616904d795c0372704c22bf76c363b371c",
246            ),
247            (
248                "4oveW4y3uE7KNbT6Yr",
249                "9410973ae328ad92916268128edb4710",
250                "5796c8ff977611fa48009fa690cd4091928c65a5c8587babcd06bdb76f8a4793",
251            ),
252            (
253                "3wQ3KFPW5L4hnaQZQeq",
254                "7d49d2b38763a12b2bbdfa93275aff18",
255                "b358fb33b5aacda99d59cd6b9c1c8e7e2162bea31addc22361220f05781e9736",
256            ),
257            (
258                "DL3NuUTWnjMocBkFMUuP",
259                "8c89c7108fff2095e18ddfef8986b118",
260                "8ebf12a4306b97b4978cac088e80b76a2cf31ae19b3c9c746a53c6d4101ee3be",
261            ),
262            (
263                "9wD4BW85Ji8GjS2XvxC2dLVgpMZGeS",
264                "b2a44461cc0bebb325280ed9130a59bb",
265                "f2847b060d5ffdcd8a1baf0264b12fc7f67e5ea61350defe612f7e9d1a1b29d3",
266            ),
267            (
268                "HFaiwNNJuSULjQlWRSo",
269                "46ece5c682cd598a65eabff63a3572df",
270                "05b17ea51240753a7d7fc707830316017dbf108dda822752ed0a31dd02655ac3",
271            ),
272            (
273                "pA4Zlnc7NQYej1nbQYWERDh1fH",
274                "2e0151573fe9c69df29b830987990985",
275                "186451be15369704049a1237c80306e0a0d6638a97a1e737b59043cc8578d374",
276            ),
277            (
278                "XPrtoJzqdo03fArXYvLCbqxTckLhi",
279                "2b852c5409d6c6813c49d1379cbbc1e9",
280                "77a08ef20a56b62a89c8b66ea6a9a489b863b3fcf06d5a9edf663729ee0ea2cd",
281            ),
282            (
283                "oWeBi4ghF",
284                "277c27b1587751f2af2001be3712ef0d",
285                "16e2b5b01852443672da19b47bacd31e3fb3cda972a9f52440a54ad811f7bcb8",
286            ),
287            (
288                "9FvIzpn7tV0l7lADIsQDbAZ",
289                "f864670399430d1671c31a2431183625",
290                "20ce1c73cfed98a27b8d2b63cb5a06709a92d471188c7398e521c83ea51cb766",
291            ),
292            (
293                "uEqxiIP533EbTK8MK5tEozAsn1nS",
294                "69b52967216f8f3ff5a1fa73e5046315",
295                "40e51fb3f0ecaf38abdcb018bdeb0935a484d9d365a0ce47ce277e72d68ea4ea",
296            ),
297            (
298                "wMybpBIOzN5P",
299                "bca1f48e60bad68798a828d3efd5258a",
300                "833acd5273e323cff011aefd83ff64ac33d72d30d3cdd1b01a03dc16a4a6b1c6",
301            ),
302            (
303                "YQ3Z8mGqp97XGJRV1LT",
304                "b469f80e382214b5f157d1c7d36a2058",
305                "8d1b82de3ed96198378a9a8f9fcffa5203f94e2c344092be4ecc89ccfa7f0173",
306            ),
307            (
308                "otSkYO0uFASqZ",
309                "10b6a29fa50416e276a0e79cbe66534e",
310                "4d3226dc093cbbfa6247c26f317e784f9f3f975fe05b9abb84fd58996e603534",
311            ),
312            (
313                "jWwusg8vdvIQCeC2y9",
314                "956071ee6e80c856f20744a8e5d6ca27",
315                "00eec6c9a868bae430f1f4588b7f2357bf5114fcbb65beb2e65e9e4202c7f75e",
316            ),
317            (
318                "NBNh4UYsHlc",
319                "f61877af4e7f8313ad8234302950b331",
320                "5a5685bf7a635ea8d86967251ebbe029f2782d36f5e0296a9775076002ab34d7",
321            ),
322        ];
323
324        for (password, salt, expected_secret_key) in test_vectors {
325            let salt: [u8; PasswordSalt::SIZE] = hex::decode(salt).unwrap().try_into().unwrap();
326            let salt = PasswordSalt::from(salt);
327
328            let actual_secret_key = SecretKey::derive_from_password(password, &salt);
329            let actual_secret_key = hex::encode(actual_secret_key.as_array());
330
331            assert_eq!(actual_secret_key, expected_secret_key);
332        }
333    }
334}