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