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 ciphertext: Vec<u8>,
683 nonce: [u8; XNONCE_SIZE],
684}
685
686#[derive(Debug)]
689pub enum EncryptedValueBase64DecodeError {
690 DecodeError(base64::DecodeSliceError),
692
693 IncorrectNonceLength(usize),
695}
696
697impl std::fmt::Display for EncryptedValueBase64DecodeError {
698 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
699 let msg = match self {
700 EncryptedValueBase64DecodeError::DecodeError(e) => e.to_string(),
701 EncryptedValueBase64DecodeError::IncorrectNonceLength(length) => {
702 format!("Incorrect nonce length {length}. Expected length: {XNONCE_SIZE}.")
703 }
704 };
705
706 f.write_str(&msg)
707 }
708}
709
710impl From<base64::DecodeSliceError> for EncryptedValueBase64DecodeError {
711 fn from(value: base64::DecodeSliceError) -> Self {
712 Self::DecodeError(value)
713 }
714}
715
716impl From<base64::DecodeError> for EncryptedValueBase64DecodeError {
717 fn from(value: base64::DecodeError) -> Self {
718 Self::DecodeError(value.into())
719 }
720}
721
722impl From<Vec<u8>> for EncryptedValueBase64DecodeError {
723 fn from(value: Vec<u8>) -> Self {
724 Self::IncorrectNonceLength(value.len())
725 }
726}
727
728impl From<EncryptedValueBase64DecodeError> for Error {
729 fn from(value: EncryptedValueBase64DecodeError) -> Self {
730 Error::Deserialization(rmp_serde::decode::Error::Uncategorized(value.to_string()))
731 }
732}
733
734impl TryFrom<EncryptedValueBase64> for EncryptedValue {
735 type Error = EncryptedValueBase64DecodeError;
736
737 fn try_from(value: EncryptedValueBase64) -> Result<Self, Self::Error> {
738 let mut nonce = [0; XNONCE_SIZE];
739 BASE64.decode_slice(value.nonce, &mut nonce)?;
740
741 Ok(Self { version: value.version, ciphertext: BASE64.decode(value.ciphertext)?, nonce })
742 }
743}
744
745#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
748pub struct EncryptedValueBase64 {
749 version: u8,
750 ciphertext: String,
751 nonce: String,
752}
753
754impl EncryptedValueBase64 {
755 pub fn new(version: u8, ciphertext: &str, nonce: &str) -> Self {
757 Self { version, ciphertext: ciphertext.to_owned(), nonce: nonce.to_owned() }
758 }
759}
760
761impl From<EncryptedValue> for EncryptedValueBase64 {
762 fn from(value: EncryptedValue) -> Self {
763 Self {
764 version: value.version,
765 ciphertext: BASE64.encode(value.ciphertext),
766 nonce: BASE64.encode(value.nonce),
767 }
768 }
769}
770
771#[derive(ZeroizeOnDrop)]
772struct Keys {
773 encryption_key: Box<[u8; 32]>,
774 mac_key_seed: Box<MacKeySeed>,
775}
776
777impl Keys {
778 fn new() -> Result<Self, Error> {
779 let mut encryption_key = Box::new([0u8; 32]);
780 let mut mac_key_seed = Box::new([0u8; 32]);
781
782 let mut rng = rng();
783
784 rng.fill_bytes(encryption_key.as_mut_slice());
785 rng.fill_bytes(mac_key_seed.as_mut_slice());
786
787 Ok(Self { encryption_key, mac_key_seed })
788 }
789
790 fn encryption_key(&self) -> &ChachaKey {
791 ChachaKey::cast_from_core(&self.encryption_key)
792 }
793
794 fn mac_key_seed(&self) -> &MacKeySeed {
795 &self.mac_key_seed
796 }
797
798 fn get_mac_key_for_table(&self, table_name: &str) -> MacKey {
799 let mut key = MacKey(Box::new([0u8; 32]));
800 let mut output = derive_key(table_name, self.mac_key_seed());
801
802 key.0.copy_from_slice(&output);
803
804 output.zeroize();
805
806 key
807 }
808
809 fn get_nonce() -> [u8; XNONCE_SIZE] {
810 let mut nonce = [0u8; XNONCE_SIZE];
811 let mut rng = rng();
812
813 rng.fill_bytes(&mut nonce);
814
815 nonce
816 }
817}
818
819#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
821enum KdfInfo {
822 None,
825 HkdfSha256,
830 Pbkdf2ToChaCha20Poly1305 {
832 rounds: u32,
835 kdf_salt: [u8; KDF_SALT_SIZE],
838 },
839}
840
841#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
844enum CipherTextInfo {
845 ChaCha20Poly1305 {
847 nonce: [u8; XNONCE_SIZE],
849 ciphertext: Vec<u8>,
851 },
852}
853
854#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
857struct EncryptedStoreCipher {
858 pub kdf_info: KdfInfo,
861 pub ciphertext_info: CipherTextInfo,
864}
865
866pub trait EncryptableValue {
874 fn as_bytes(&self) -> &[u8];
876
877 fn zeroiize(&mut self);
881}
882
883impl EncryptableValue for Vec<u8> {
884 fn as_bytes(&self) -> &[u8] {
885 AsRef::as_ref(self)
886 }
887
888 fn zeroiize(&mut self) {
889 Zeroize::zeroize(self);
890 }
891}
892
893impl EncryptableValue for String {
894 fn as_bytes(&self) -> &[u8] {
895 str::as_bytes(self)
896 }
897
898 fn zeroiize(&mut self) {
899 Zeroize::zeroize(self);
900 }
901}
902
903impl EncryptableValue for &mut [u8] {
904 fn as_bytes(&self) -> &[u8] {
905 self
906 }
907
908 fn zeroiize(&mut self) {
909 self.iter_mut().zeroize();
910 }
911}
912
913#[cfg(test)]
914mod tests {
915 use serde_json::{Value, json};
916
917 use super::{Error, StoreCipher};
918 use crate::{
919 EncryptedStoreCipher, EncryptedValue, EncryptedValueBase64, EncryptedValueBase64DecodeError,
920 };
921
922 #[test]
923 fn generating() {
924 StoreCipher::new().unwrap();
925 }
926
927 #[test]
928 fn exporting_store_cipher() -> Result<(), Error> {
929 let passphrase = "it's a secret to everybody";
930 let store_cipher = StoreCipher::new()?;
931
932 let value = json!({
933 "some": "data"
934 });
935
936 let encrypted_value = store_cipher.encrypt_value(&value)?;
937
938 let encrypted = store_cipher._insecure_export_fast_for_testing(passphrase)?;
939 let decrypted = StoreCipher::import(passphrase, &encrypted)?;
940
941 assert_eq!(store_cipher.inner.encryption_key, decrypted.inner.encryption_key);
942 assert_eq!(store_cipher.inner.mac_key_seed, decrypted.inner.mac_key_seed);
943
944 let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?;
945
946 assert_eq!(value, decrypted_value);
947
948 match StoreCipher::import_with_key(&[0u8; 32], &encrypted) {
951 Err(Error::KdfMismatch) => {}
952 _ => panic!(
953 "Invalid error when importing a passphrase-encrypted store cipher with a key"
954 ),
955 }
956
957 let store_cipher = StoreCipher::new()?;
958 let encrypted_value = store_cipher.encrypt_value(&value)?;
959
960 let export = store_cipher.export_with_key(&[0u8; 32])?;
961 let decrypted = StoreCipher::import_with_key(&[0u8; 32], &export)?;
962
963 let decrypted_value: Value = decrypted.decrypt_value(&encrypted_value)?;
964 assert_eq!(value, decrypted_value);
965
966 match StoreCipher::import_with_key(&[0u8; 32], &encrypted) {
968 Err(Error::KdfMismatch) => {}
969 _ => panic!(
970 "Invalid error when importing a key-encrypted store cipher with a passphrase"
971 ),
972 }
973
974 let old_export = json!({
975 "ciphertext_info": {
976 "ChaCha20Poly1305":{
977 "ciphertext":[
978 136,202,212,194,9,223,171,109,152,84,140,183,14,55,198,22,150,130,80,135,
979 161,202,79,205,151,202,120,91,108,154,252,94,56,178,108,216,186,179,167,128,
980 154,107,243,195,14,138,86,78,140,159,245,170,204,227,27,84,255,161,196,69,
981 60,150,69,123,67,134,28,50,10,179,250,141,221,19,202,132,28,122,92,116
982 ],
983 "nonce":[
984 108,3,115,54,65,135,250,188,212,204,93,223,78,11,52,46,
985 124,140,218,73,88,167,50,230
986 ]
987 }
988 },
989 "kdf_info":{
990 "Pbkdf2ToChaCha20Poly1305":{
991 "kdf_salt":[
992 221,133,149,116,199,122,172,189,236,42,26,204,53,164,245,158,137,113,
993 31,220,239,66,64,51,242,164,185,166,176,218,209,245
994 ],
995 "rounds":1000
996 }
997 }
998 });
999
1000 let old_export = serde_json::to_vec(&old_export)?;
1001
1002 StoreCipher::import(passphrase, &old_export)
1003 .expect("We can import the old store-cipher export");
1004
1005 Ok(())
1006 }
1007
1008 #[test]
1009 fn import_with_key_no_kdf_variant() {
1010 let old_export = json!({
1011 "kdf_info": "None",
1012 "ciphertext_info": {
1013 "ChaCha20Poly1305": {
1014 "nonce": [
1015 239,147,78,71,225,166,233,69,75,161,181,241,171,197,174,102,228,176,161,158,
1016 21,32,208,216
1017 ],
1018 "ciphertext":[
1019 63,195,248,146,13,60,40,131,62,209,2,113,184,79,121,242,180,170,51,194,85,
1020 96,11,97,248,68,2,178,108,30,39,215,96,119,216,38,6,203,79,42,32,220,69,41,
1021 120,44,218,88,37,176,79,198,198,209,26,62,251,20,181,55,88,83,196,131,140,
1022 245,89,167,58,146,150,10,136,90,194,123,221,147,128,255
1023 ]
1024 }
1025 }
1026 });
1027
1028 let old_export: EncryptedStoreCipher = serde_json::from_value(old_export)
1029 .expect("We should be able to serialize the old export");
1030 let old_export = rmp_serde::to_vec(&old_export).unwrap();
1031
1032 StoreCipher::import_with_key(&[0u8; 32], &old_export)
1033 .expect("We can import the old store-cipher export");
1034 }
1035
1036 #[test]
1037 fn test_importing_invalid_store_cipher_does_not_panic() {
1038 assert!(StoreCipher::import_with_key(&[0; 32], &[0; 64]).is_err())
1040 }
1041
1042 #[test]
1043 fn encrypting_values() -> Result<(), Error> {
1044 let event = json!({
1045 "content": {
1046 "body": "Bee Gees - Stayin' Alive",
1047 "info": {
1048 "duration": 2140786u32,
1049 "mimetype": "audio/mpeg",
1050 "size": 1563685u32
1051 },
1052 "msgtype": "m.audio",
1053 "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
1054 },
1055 });
1056
1057 let store_cipher = StoreCipher::new()?;
1058
1059 let encrypted = store_cipher.encrypt_value(&event)?;
1060 let decrypted: Value = store_cipher.decrypt_value(&encrypted)?;
1061
1062 assert_eq!(event, decrypted);
1063
1064 Ok(())
1065 }
1066
1067 #[test]
1068 fn encrypting_values_base64() -> Result<(), Error> {
1069 let event = json!({
1070 "content": {
1071 "body": "Bee Gees - Stayin' Alive",
1072 "info": {
1073 "duration": 2140786u32,
1074 "mimetype": "audio/mpeg",
1075 "size": 1563685u32
1076 },
1077 "msgtype": "m.audio",
1078 "url": "mxc://example.org/ffed755USFFxlgbQYZGtryd"
1079 },
1080 });
1081
1082 let store_cipher = StoreCipher::new()?;
1083
1084 let data = serde_json::to_vec(&event)?;
1085 let encrypted = store_cipher.encrypt_value_base64_data(data)?;
1086
1087 let plaintext = store_cipher.decrypt_value_base64_data(encrypted)?;
1088 let decrypted: Value = serde_json::from_slice(&plaintext)?;
1089
1090 assert_eq!(event, decrypted);
1091
1092 Ok(())
1093 }
1094
1095 #[test]
1096 fn encrypting_keys() -> Result<(), Error> {
1097 let store_cipher = StoreCipher::new()?;
1098
1099 let first = store_cipher.hash_key("some_table", b"It's dangerous to go alone");
1100 let second = store_cipher.hash_key("some_table", b"It's dangerous to go alone");
1101 let third = store_cipher.hash_key("another_table", b"It's dangerous to go alone");
1102 let fourth = store_cipher.hash_key("another_table", b"It's dangerous to go alone");
1103 let fifth = store_cipher.hash_key("another_table", b"It's not dangerous to go alone");
1104
1105 assert_eq!(first, second);
1106 assert_ne!(first, third);
1107 assert_eq!(third, fourth);
1108 assert_ne!(fourth, fifth);
1109
1110 Ok(())
1111 }
1112
1113 #[test]
1114 fn can_round_trip_normal_to_base64_encrypted_values() {
1115 let normal1 = EncryptedValue { version: 2, ciphertext: vec![1, 2, 4], nonce: make_nonce() };
1116 let normal2 = EncryptedValue { version: 2, ciphertext: vec![1, 2, 4], nonce: make_nonce() };
1117
1118 let base64: EncryptedValueBase64 = normal1.into();
1120 assert_eq!(base64.ciphertext, "AQIE");
1121
1122 let new_normal: EncryptedValue = base64.try_into().unwrap();
1124 assert_eq!(normal2, new_normal);
1125 }
1126
1127 #[test]
1128 fn can_round_trip_base64_to_normal_encrypted_values() {
1129 let base64_1 = EncryptedValueBase64 {
1130 version: 2,
1131 ciphertext: "abc".to_owned(),
1132 nonce: make_nonce_base64(),
1133 };
1134 let base64_2 = EncryptedValueBase64 {
1135 version: 2,
1136 ciphertext: "abc".to_owned(),
1137 nonce: make_nonce_base64(),
1138 };
1139
1140 let normal: EncryptedValue = base64_1.try_into().unwrap();
1142 assert_eq!(normal.ciphertext, &[105, 183]);
1143
1144 let new_base64: EncryptedValueBase64 = normal.into();
1146 assert_eq!(base64_2, new_base64);
1147 }
1148
1149 #[test]
1150 fn decoding_invalid_base64_returns_an_error() {
1151 let base64 =
1152 EncryptedValueBase64 { version: 2, ciphertext: "a".to_owned(), nonce: "b".to_owned() };
1153
1154 let result: Result<EncryptedValue, EncryptedValueBase64DecodeError> = base64.try_into();
1155
1156 let Err(err) = result else {
1157 panic!("Should be an error!");
1158 };
1159
1160 assert_eq!(err.to_string(), "DecodeError: Invalid input length: 1");
1161 }
1162
1163 fn make_nonce() -> [u8; 24] {
1164 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
1165 }
1166
1167 fn make_nonce_base64() -> String {
1168 "AAECAwQFBgcICQoLDA0ODxAREhMUFRYX".to_owned()
1169 }
1170}