1#![doc = include_str!("../README.md")]
17#![warn(missing_debug_implementations, missing_docs)]
18
19use std::ops::DerefMut;
20
21use base64::{
22 Engine, alphabet,
23 engine::{GeneralPurpose, general_purpose},
24};
25use blake3::{Hash, derive_key};
26use chacha20poly1305::{
27 Key as ChachaKey, KeyInit, XChaCha20Poly1305, XNonce,
28 aead::{Aead, Error as EncryptionError},
29};
30use hkdf::Hkdf;
31use hmac::Hmac;
32use pbkdf2::pbkdf2;
33use rand::{Rng, rng};
34use serde::{Deserialize, Serialize, de::DeserializeOwned};
35use sha2::Sha256;
36use zeroize::{Zeroize, ZeroizeOnDrop};
37
38const VERSION: u8 = 1;
39const KDF_SALT_SIZE: usize = 32;
40const XNONCE_SIZE: usize = 24;
41const KDF_ROUNDS: u32 = 200_000;
42
43const BASE64: GeneralPurpose = GeneralPurpose::new(&alphabet::STANDARD, general_purpose::NO_PAD);
44
45type MacKeySeed = [u8; 32];
46
47#[derive(Debug, thiserror::Error)]
49pub enum Error {
50 #[error("Failed to serialize a value: `{0}`")]
52 Serialization(#[from] rmp_serde::encode::Error),
53
54 #[error("Failed to deserialize a value: `{0}`")]
56 Deserialization(#[from] rmp_serde::decode::Error),
57
58 #[error("Failed to deserialize or serialize a JSON value: `{0}`")]
60 Json(#[from] serde_json::Error),
61
62 #[error("Error encrypting or decrypting a value: `{0}`")]
64 Encryption(#[from] EncryptionError),
65
66 #[error("Unsupported ciphertext version, expected `{0}`, got `{1}`")]
68 Version(u8, u8),
69
70 #[error("The ciphertext had an invalid length, expected `{0}`, got `{1}`")]
72 Length(usize, usize),
73
74 #[error(
78 "Failed to import the store cipher. The export was created using a different encryption
79 mechanism than the one being used for import (passphrase vs. key)"
80 )]
81 KdfMismatch,
82}
83
84#[allow(missing_debug_implementations)]
109pub struct StoreCipher {
110 inner: Keys,
111}
112
113impl StoreCipher {
114 pub fn new() -> Result<Self, Error> {
116 Ok(Self { inner: Keys::new()? })
117 }
118
119 pub fn export(&self, passphrase: &str) -> Result<Vec<u8>, Error> {
148 self.export_kdf(passphrase, KDF_ROUNDS)
149 }
150
151 pub fn export_with_key(&self, key: &[u8]) -> Result<Vec<u8>, Error> {
180 let mut derived_key = Box::new([0u8; 32]);
181
182 Self::expand_key_from_key(key, &mut derived_key);
183 let store_cipher = self.export_helper(&derived_key, KdfInfo::HkdfSha256)?;
184
185 derived_key.zeroize();
186
187 Ok(rmp_serde::to_vec_named(&store_cipher).expect("Can't serialize the store cipher"))
188 }
189
190 fn export_helper(
191 &self,
192 key: &[u8; 32],
193 kdf_info: KdfInfo,
194 ) -> Result<EncryptedStoreCipher, Error> {
195 let key = ChachaKey::cast_from_core(key);
196 let cipher = XChaCha20Poly1305::new(key);
197
198 let nonce = Keys::get_nonce();
199
200 let mut keys = [0u8; 64];
201
202 keys[0..32].copy_from_slice(self.inner.encryption_key.as_ref());
203 keys[32..64].copy_from_slice(self.inner.mac_key_seed.as_ref());
204
205 let ciphertext = cipher.encrypt(XNonce::cast_from_core(&nonce), keys.as_ref())?;
206
207 keys.zeroize();
208
209 Ok(EncryptedStoreCipher {
210 kdf_info,
211 ciphertext_info: CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext },
212 })
213 }
214
215 #[doc(hidden)]
216 pub fn _insecure_export_fast_for_testing(&self, passphrase: &str) -> Result<Vec<u8>, Error> {
217 self.export_kdf(passphrase, 1000)
218 }
219
220 fn export_kdf(&self, passphrase: &str, kdf_rounds: u32) -> Result<Vec<u8>, Error> {
221 let mut rng = rng();
222
223 let mut salt = [0u8; KDF_SALT_SIZE];
224 rng.fill_bytes(&mut salt);
225
226 let key = StoreCipher::expand_key(passphrase, &salt, kdf_rounds);
227
228 let store_cipher = self.export_helper(
229 &key,
230 KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds: kdf_rounds, kdf_salt: salt },
231 )?;
232
233 Ok(rmp_serde::to_vec_named(&store_cipher).expect("Can't serialize the store cipher"))
234 }
235
236 fn import_helper(key: &ChachaKey, encrypted: EncryptedStoreCipher) -> Result<Self, Error> {
237 let mut decrypted = match encrypted.ciphertext_info {
238 CipherTextInfo::ChaCha20Poly1305 { nonce, ciphertext } => {
239 let cipher = XChaCha20Poly1305::new(key);
240 let nonce = XNonce::cast_from_core(&nonce);
241 cipher.decrypt(nonce, ciphertext.as_ref())?
242 }
243 };
244
245 if decrypted.len() != 64 {
246 decrypted.zeroize();
247
248 Err(Error::Length(64, decrypted.len()))
249 } else {
250 let mut encryption_key = Box::new([0u8; 32]);
251 let mut mac_key_seed = Box::new([0u8; 32]);
252
253 encryption_key.copy_from_slice(&decrypted[0..32]);
254 mac_key_seed.copy_from_slice(&decrypted[32..64]);
255
256 let keys = Keys { encryption_key, mac_key_seed };
257
258 decrypted.zeroize();
259
260 Ok(Self { inner: keys })
261 }
262 }
263
264 pub fn import(passphrase: &str, encrypted: &[u8]) -> Result<Self, Error> {
292 let encrypted: EncryptedStoreCipher =
295 if let Ok(deserialized) = rmp_serde::from_slice(encrypted) {
296 deserialized
297 } else {
298 serde_json::from_slice(encrypted)?
299 };
300
301 let key = match encrypted.kdf_info {
302 KdfInfo::Pbkdf2ToChaCha20Poly1305 { rounds, kdf_salt } => {
303 Self::expand_key(passphrase, &kdf_salt, rounds)
304 }
305 KdfInfo::None | KdfInfo::HkdfSha256 => {
306 return Err(Error::KdfMismatch);
307 }
308 };
309
310 let key = ChachaKey::cast_from_core(key.as_ref());
311
312 Self::import_helper(key, encrypted)
313 }
314
315 pub fn import_with_key(key: &[u8], encrypted: &[u8]) -> Result<Self, Error> {
343 let encrypted: EncryptedStoreCipher = rmp_serde::from_slice(encrypted)?;
344
345 let mut key = match &encrypted.kdf_info {
346 KdfInfo::None => {
347 if key.len() != 32 {
352 return Err(Error::KdfMismatch);
353 }
354
355 let mut key_copy = Box::new([0u8; 32]);
358 key_copy.copy_from_slice(key);
359
360 key_copy
361 }
362 KdfInfo::HkdfSha256 => {
363 let mut derived_key = Box::new([0u8; 32]);
364 Self::expand_key_from_key(key, &mut derived_key);
365
366 derived_key
367 }
368 KdfInfo::Pbkdf2ToChaCha20Poly1305 { .. } => {
369 return Err(Error::KdfMismatch);
370 }
371 };
372
373 if let KdfInfo::Pbkdf2ToChaCha20Poly1305 { .. } = encrypted.kdf_info {
374 return Err(Error::KdfMismatch);
375 }
376
377 let chacha_key = ChachaKey::cast_from_core(key.as_ref());
378
379 let ret = Self::import_helper(chacha_key, encrypted);
380
381 key.zeroize();
382
383 ret
384 }
385
386 pub fn hash_key(&self, table_name: &str, key: &[u8]) -> [u8; 32] {
423 let mac_key = self.inner.get_mac_key_for_table(table_name);
424
425 mac_key.mac(key).into()
426 }
427
428 pub fn encrypt_value(&self, value: &impl Serialize) -> Result<Vec<u8>, Error> {
459 let data = serde_json::to_vec(value)?;
460 Ok(serde_json::to_vec(&self.encrypt_value_data(data)?)?)
461 }
462
463 pub fn encrypt_value_data<D>(&self, mut data: D) -> Result<EncryptedValue, Error>
492 where
493 D: EncryptableValue,
494 {
495 let nonce = Keys::get_nonce();
496 let cipher = XChaCha20Poly1305::new(self.inner.encryption_key());
497
498 let ciphertext = cipher.encrypt(XNonce::cast_from_core(&nonce), data.as_bytes())?;
499
500 data.zeroiize();
501 Ok(EncryptedValue { version: VERSION, ciphertext, nonce })
502 }
503
504 pub fn encrypt_value_base64_data(&self, data: Vec<u8>) -> Result<EncryptedValueBase64, Error> {
534 self.encrypt_value_data(data).map(EncryptedValueBase64::from)
535 }
536
537 pub fn decrypt_value<T: DeserializeOwned>(&self, value: &[u8]) -> Result<T, Error> {
568 let value: EncryptedValue = serde_json::from_slice(value)?;
569 let mut plaintext = self.decrypt_value_data(value)?;
570 let ret = serde_json::from_slice(&plaintext);
571 plaintext.zeroize();
572 Ok(ret?)
573 }
574
575 pub fn decrypt_value_base64_data(&self, value: EncryptedValueBase64) -> Result<Vec<u8>, Error> {
608 self.decrypt_value_data(value.try_into()?)
609 }
610
611 pub fn decrypt_value_data(&self, value: EncryptedValue) -> Result<Vec<u8>, Error> {
642 if value.version != VERSION {
643 return Err(Error::Version(VERSION, value.version));
644 }
645
646 let cipher = XChaCha20Poly1305::new(self.inner.encryption_key());
647 let nonce = XNonce::cast_from_core(&value.nonce);
648 Ok(cipher.decrypt(nonce, value.ciphertext.as_ref())?)
649 }
650
651 fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Box<[u8; 32]> {
653 let mut key = Box::new([0u8; 32]);
654 pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), salt, rounds, key.deref_mut()).expect(
655 "We should be able to expand a passphrase of any length due to \
656 HMAC being able to be initialized with any input size",
657 );
658
659 key
660 }
661
662 fn expand_key_from_key(key: &[u8], output: &mut [u8; 32]) {
663 let hkdf = Hkdf::<Sha256>::new(None, key);
664 hkdf.expand(b"matrix-sdk-store-encryption", output)
665 .expect("32 bytes is a valid HKDF-SHA256 output length");
666 }
667}
668
669#[derive(ZeroizeOnDrop)]
670struct MacKey(Box<[u8; 32]>);
671
672impl MacKey {
673 fn mac(&self, input: &[u8]) -> Hash {
674 blake3::keyed_hash(&self.0, input)
675 }
676}
677
678#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
681pub struct EncryptedValue {
682 version: u8,
683 #[serde(with = "serde_bytes")]
684 ciphertext: Vec<u8>,
685 nonce: [u8; XNONCE_SIZE],
686}
687
688#[derive(Debug)]
691pub enum EncryptedValueBase64DecodeError {
692 DecodeError(base64::DecodeSliceError),
694
695 IncorrectNonceLength(usize),
697}
698
699impl std::fmt::Display for EncryptedValueBase64DecodeError {
700 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
701 let msg = match self {
702 EncryptedValueBase64DecodeError::DecodeError(e) => e.to_string(),
703 EncryptedValueBase64DecodeError::IncorrectNonceLength(length) => {
704 format!("Incorrect nonce length {length}. Expected length: {XNONCE_SIZE}.")
705 }
706 };
707
708 f.write_str(&msg)
709 }
710}
711
712impl From<base64::DecodeSliceError> for EncryptedValueBase64DecodeError {
713 fn from(value: base64::DecodeSliceError) -> Self {
714 Self::DecodeError(value)
715 }
716}
717
718impl From<base64::DecodeError> for EncryptedValueBase64DecodeError {
719 fn from(value: base64::DecodeError) -> Self {
720 Self::DecodeError(value.into())
721 }
722}
723
724impl From<Vec<u8>> for EncryptedValueBase64DecodeError {
725 fn from(value: Vec<u8>) -> Self {
726 Self::IncorrectNonceLength(value.len())
727 }
728}
729
730impl From<EncryptedValueBase64DecodeError> for Error {
731 fn from(value: EncryptedValueBase64DecodeError) -> Self {
732 Error::Deserialization(rmp_serde::decode::Error::Uncategorized(value.to_string()))
733 }
734}
735
736impl TryFrom<EncryptedValueBase64> for EncryptedValue {
737 type Error = EncryptedValueBase64DecodeError;
738
739 fn try_from(value: EncryptedValueBase64) -> Result<Self, Self::Error> {
740 let mut nonce = [0; XNONCE_SIZE];
741 BASE64.decode_slice(value.nonce, &mut nonce)?;
742
743 Ok(Self { version: value.version, ciphertext: BASE64.decode(value.ciphertext)?, nonce })
744 }
745}
746
747#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
750pub struct EncryptedValueBase64 {
751 version: u8,
752 ciphertext: String,
753 nonce: String,
754}
755
756impl EncryptedValueBase64 {
757 pub fn new(version: u8, ciphertext: &str, nonce: &str) -> Self {
759 Self { version, ciphertext: ciphertext.to_owned(), nonce: nonce.to_owned() }
760 }
761}
762
763impl From<EncryptedValue> for EncryptedValueBase64 {
764 fn from(value: EncryptedValue) -> Self {
765 Self {
766 version: value.version,
767 ciphertext: BASE64.encode(value.ciphertext),
768 nonce: BASE64.encode(value.nonce),
769 }
770 }
771}
772
773#[derive(ZeroizeOnDrop)]
774struct Keys {
775 encryption_key: Box<[u8; 32]>,
776 mac_key_seed: Box<MacKeySeed>,
777}
778
779impl Keys {
780 fn new() -> Result<Self, Error> {
781 let mut encryption_key = Box::new([0u8; 32]);
782 let mut mac_key_seed = Box::new([0u8; 32]);
783
784 let mut rng = rng();
785
786 rng.fill_bytes(encryption_key.as_mut_slice());
787 rng.fill_bytes(mac_key_seed.as_mut_slice());
788
789 Ok(Self { encryption_key, mac_key_seed })
790 }
791
792 fn encryption_key(&self) -> &ChachaKey {
793 ChachaKey::cast_from_core(&self.encryption_key)
794 }
795
796 fn mac_key_seed(&self) -> &MacKeySeed {
797 &self.mac_key_seed
798 }
799
800 fn get_mac_key_for_table(&self, table_name: &str) -> MacKey {
801 let mut key = MacKey(Box::new([0u8; 32]));
802 let mut output = derive_key(table_name, self.mac_key_seed());
803
804 key.0.copy_from_slice(&output);
805
806 output.zeroize();
807
808 key
809 }
810
811 fn get_nonce() -> [u8; XNONCE_SIZE] {
812 let mut nonce = [0u8; XNONCE_SIZE];
813 let mut rng = rng();
814
815 rng.fill_bytes(&mut nonce);
816
817 nonce
818 }
819}
820
821#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
823enum KdfInfo {
824 None,
827 HkdfSha256,
832 Pbkdf2ToChaCha20Poly1305 {
834 rounds: u32,
837 kdf_salt: [u8; KDF_SALT_SIZE],
840 },
841}
842
843#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
846enum CipherTextInfo {
847 ChaCha20Poly1305 {
849 nonce: [u8; XNONCE_SIZE],
851 ciphertext: Vec<u8>,
853 },
854}
855
856#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
859struct EncryptedStoreCipher {
860 pub kdf_info: KdfInfo,
863 pub ciphertext_info: CipherTextInfo,
866}
867
868pub trait EncryptableValue {
876 fn as_bytes(&self) -> &[u8];
878
879 fn zeroiize(&mut self);
883}
884
885impl EncryptableValue for Vec<u8> {
886 fn as_bytes(&self) -> &[u8] {
887 AsRef::as_ref(self)
888 }
889
890 fn zeroiize(&mut self) {
891 Zeroize::zeroize(self);
892 }
893}
894
895impl EncryptableValue for String {
896 fn as_bytes(&self) -> &[u8] {
897 str::as_bytes(self)
898 }
899
900 fn zeroiize(&mut self) {
901 Zeroize::zeroize(self);
902 }
903}
904
905impl EncryptableValue for &mut [u8] {
906 fn as_bytes(&self) -> &[u8] {
907 self
908 }
909
910 fn zeroiize(&mut self) {
911 self.iter_mut().zeroize();
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use serde_json::{Value, json};
918
919 use super::{Error, StoreCipher};
920 use crate::{
921 EncryptedStoreCipher, EncryptedValue, EncryptedValueBase64, EncryptedValueBase64DecodeError,
922 };
923
924 #[test]
925 fn generating() {
926 StoreCipher::new().unwrap();
927 }
928
929 #[test]
930 fn exporting_store_cipher() -> Result<(), Error> {
931 let passphrase = "it's a secret to everybody";
932 let store_cipher = StoreCipher::new()?;
933
934 let value = json!({
935 "some": "data"
936 });
937
938 let encrypted_value = store_cipher.encrypt_value(&value)?;
939
940 let encrypted = store_cipher._insecure_export_fast_for_testing(passphrase)?;
941 let decrypted = StoreCipher::import(passphrase, &encrypted)?;
942
943 assert_eq!(store_cipher.inner.encryption_key, decrypted.inner.encryption_key);
944 assert_eq!(store_cipher.inner.mac_key_seed, decrypted.inner.mac_key_seed);
945
946 let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?;
947
948 assert_eq!(value, decrypted_value);
949
950 match StoreCipher::import_with_key(&[0u8; 32], &encrypted) {
953 Err(Error::KdfMismatch) => {}
954 _ => panic!(
955 "Invalid error when importing a passphrase-encrypted store cipher with a key"
956 ),
957 }
958
959 let store_cipher = StoreCipher::new()?;
960 let encrypted_value = store_cipher.encrypt_value(&value)?;
961
962 let export = store_cipher.export_with_key(&[0u8; 32])?;
963 let decrypted = StoreCipher::import_with_key(&[0u8; 32], &export)?;
964
965 let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?;
966 assert_eq!(value, decrypted_value);
967
968 match StoreCipher::import_with_key(&[0u8; 32], &encrypted) {
970 Err(Error::KdfMismatch) => {}
971 _ => panic!(
972 "Invalid error when importing a key-encrypted store cipher with a passphrase"
973 ),
974 }
975
976 let old_export = json!({
977 "ciphertext_info": {
978 "ChaCha20Poly1305":{
979 "ciphertext":[
980 136,202,212,194,9,223,171,109,152,84,140,183,14,55,198,22,150,130,80,135,
981 161,202,79,205,151,202,120,91,108,154,252,94,56,178,108,216,186,179,167,128,
982 154,107,243,195,14,138,86,78,140,159,245,170,204,227,27,84,255,161,196,69,
983 60,150,69,123,67,134,28,50,10,179,250,141,221,19,202,132,28,122,92,116
984 ],
985 "nonce":[
986 108,3,115,54,65,135,250,188,212,204,93,223,78,11,52,46,
987 124,140,218,73,88,167,50,230
988 ]
989 }
990 },
991 "kdf_info":{
992 "Pbkdf2ToChaCha20Poly1305":{
993 "kdf_salt":[
994 221,133,149,116,199,122,172,189,236,42,26,204,53,164,245,158,137,113,
995 31,220,239,66,64,51,242,164,185,166,176,218,209,245
996 ],
997 "rounds":1000
998 }
999 }
1000 });
1001
1002 let old_export = serde_json::to_vec(&old_export)?;
1003
1004 StoreCipher::import(passphrase, &old_export)
1005 .expect("We can import the old store-cipher export");
1006
1007 Ok(())
1008 }
1009
1010 #[test]
1011 fn import_with_key_no_kdf_variant() {
1012 let old_export = json!({
1013 "kdf_info": "None",
1014 "ciphertext_info": {
1015 "ChaCha20Poly1305": {
1016 "nonce": [
1017 239,147,78,71,225,166,233,69,75,161,181,241,171,197,174,102,228,176,161,158,
1018 21,32,208,216
1019 ],
1020 "ciphertext":[
1021 63,195,248,146,13,60,40,131,62,209,2,113,184,79,121,242,180,170,51,194,85,
1022 96,11,97,248,68,2,178,108,30,39,215,96,119,216,38,6,203,79,42,32,220,69,41,
1023 120,44,218,88,37,176,79,198,198,209,26,62,251,20,181,55,88,83,196,131,140,
1024 245,89,167,58,146,150,10,136,90,194,123,221,147,128,255
1025 ]
1026 }
1027 }
1028 });
1029
1030 let old_export: EncryptedStoreCipher = serde_json::from_value(old_export)
1031 .expect("We should be able to serialize the old export");
1032 let old_export = rmp_serde::to_vec(&old_export).unwrap();
1033
1034 StoreCipher::import_with_key(&[0u8; 32], &old_export)
1035 .expect("We can import the old store-cipher export");
1036 }
1037
1038 #[test]
1039 fn test_importing_invalid_store_cipher_does_not_panic() {
1040 assert!(StoreCipher::import_with_key(&[0; 32], &[0; 64]).is_err())
1042 }
1043
1044 #[test]
1045 fn encrypting_values() -> Result<(), Error> {
1046 let event = json!({
1047 "content": {
1048 "body": "Bee Gees - Stayin' Alive",
1049 "info": {
1050 "duration": 2140786u32,
1051 "mimetype": "audio/mpeg",
1052 "size": 1563685u32
1053 },
1054 "msgtype": "m.audio",
1055 "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
1056 },
1057 });
1058
1059 let store_cipher = StoreCipher::new()?;
1060
1061 let encrypted = store_cipher.encrypt_value(&event)?;
1062 let decrypted: Value = store_cipher.decrypt_value(&encrypted)?;
1063
1064 assert_eq!(event, decrypted);
1065
1066 Ok(())
1067 }
1068
1069 #[test]
1070 fn encrypting_values_base64() -> Result<(), Error> {
1071 let event = json!({
1072 "content": {
1073 "body": "Bee Gees - Stayin' Alive",
1074 "info": {
1075 "duration": 2140786u32,
1076 "mimetype": "audio/mpeg",
1077 "size": 1563685u32
1078 },
1079 "msgtype": "m.audio",
1080 "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
1081 },
1082 });
1083
1084 let store_cipher = StoreCipher::new()?;
1085
1086 let data = serde_json::to_vec(&event)?;
1087 let encrypted = store_cipher.encrypt_value_base64_data(data)?;
1088
1089 let plaintext = store_cipher.decrypt_value_base64_data(encrypted)?;
1090 let decrypted: Value = serde_json::from_slice(&plaintext)?;
1091
1092 assert_eq!(event, decrypted);
1093
1094 Ok(())
1095 }
1096
1097 #[test]
1098 fn encrypting_keys() -> Result<(), Error> {
1099 let store_cipher = StoreCipher::new()?;
1100
1101 let first = store_cipher.hash_key("some_table", b"It's dangerous to go alone");
1102 let second = store_cipher.hash_key("some_table", b"It's dangerous to go alone");
1103 let third = store_cipher.hash_key("another_table", b"It's dangerous to go alone");
1104 let fourth = store_cipher.hash_key("another_table", b"It's dangerous to go alone");
1105 let fifth = store_cipher.hash_key("another_table", b"It's not dangerous to go alone");
1106
1107 assert_eq!(first, second);
1108 assert_ne!(first, third);
1109 assert_eq!(third, fourth);
1110 assert_ne!(fourth, fifth);
1111
1112 Ok(())
1113 }
1114
1115 #[test]
1116 fn can_round_trip_normal_to_base64_encrypted_values() {
1117 let normal1 = EncryptedValue { version: 2, ciphertext: vec![1, 2, 4], nonce: make_nonce() };
1118 let normal2 = EncryptedValue { version: 2, ciphertext: vec![1, 2, 4], nonce: make_nonce() };
1119
1120 let base64: EncryptedValueBase64 = normal1.into();
1122 assert_eq!(base64.ciphertext, "AQIE");
1123
1124 let new_normal: EncryptedValue = base64.try_into().unwrap();
1126 assert_eq!(normal2, new_normal);
1127 }
1128
1129 #[test]
1130 fn can_round_trip_base64_to_normal_encrypted_values() {
1131 let base64_1 = EncryptedValueBase64 {
1132 version: 2,
1133 ciphertext: "abc".to_owned(),
1134 nonce: make_nonce_base64(),
1135 };
1136 let base64_2 = EncryptedValueBase64 {
1137 version: 2,
1138 ciphertext: "abc".to_owned(),
1139 nonce: make_nonce_base64(),
1140 };
1141
1142 let normal: EncryptedValue = base64_1.try_into().unwrap();
1144 assert_eq!(normal.ciphertext, &[105, 183]);
1145
1146 let new_base64: EncryptedValueBase64 = normal.into();
1148 assert_eq!(base64_2, new_base64);
1149 }
1150
1151 #[test]
1152 fn decoding_invalid_base64_returns_an_error() {
1153 let base64 =
1154 EncryptedValueBase64 { version: 2, ciphertext: "a".to_owned(), nonce: "b".to_owned() };
1155
1156 let result: Result<EncryptedValue, EncryptedValueBase64DecodeError> = base64.try_into();
1157
1158 let Err(err) = result else {
1159 panic!("Should be an error!");
1160 };
1161
1162 assert_eq!(err.to_string(), "DecodeError: Invalid input length: 1");
1163 }
1164
1165 fn make_nonce() -> [u8; 24] {
1166 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
1167 }
1168
1169 fn make_nonce_base64() -> String {
1170 "AAECAwQFBgcICQoLDA0ODxAREhMUFRYX".to_owned()
1171 }
1172}