diff --git a/Cargo.lock b/Cargo.lock index db57d61..9a51fbd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + [[package]] name = "aes" version = "0.6.0" @@ -31,7 +40,19 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ "aes-soft", "aesni", - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if 1.0.0", + "cipher 0.3.0", + "cpufeatures", + "opaque-debug", ] [[package]] @@ -40,11 +61,25 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5278b5fabbb9bd46e24aa69b2fdea62c99088e0a950a9be40e3e0101298f88da" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.3.2", + "aes 0.6.0", + "cipher 0.2.5", + "ctr 0.6.0", + "ghash 0.3.1", + "subtle", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead 0.4.3", + "aes 0.7.5", + "cipher 0.3.0", + "ctr 0.8.0", + "ghash 0.4.4", "subtle", ] @@ -54,7 +89,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -64,7 +99,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -297,9 +332,11 @@ dependencies = [ name = "authenticator" version = "0.1.0" dependencies = [ + "aes-gcm 0.9.4", "anyhow", "ashpd", "async-std", + "base64", "binascii", "diesel", "diesel_migrations", @@ -321,6 +358,7 @@ dependencies = [ "rand 0.8.4", "ring", "rust-argon2", + "scrypt", "search-provider", "secret-service", "serde", @@ -423,6 +461,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + [[package]] name = "block-modes" version = "0.7.0" @@ -430,7 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57a0e8073e8baa88212fb5823574c02ebccb395136ba9a164ab89379ec6072f0" dependencies = [ "block-padding", - "cipher", + "cipher 0.2.5", ] [[package]] @@ -585,6 +632,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + [[package]] name = "clang-sys" version = "1.3.0" @@ -644,13 +700,13 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a5d7b21829bc7b4bf4754a978a241ae54ea55a40f92bb20216e54096f4b951" dependencies = [ - "aes-gcm", + "aes-gcm 0.8.0", "base64", "hkdf", - "hmac", + "hmac 0.10.1", "percent-encoding", "rand 0.8.4", - "sha2", + "sha2 0.9.9", "time", "version_check", ] @@ -729,6 +785,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array", +] + [[package]] name = "crypto-mac" version = "0.10.1" @@ -755,7 +820,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" dependencies = [ - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher 0.3.0", ] [[package]] @@ -862,6 +936,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array", + "subtle", +] + [[package]] name = "discard" version = "1.0.4" @@ -1332,7 +1418,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97304e4cd182c3846f7575ced3890c53012ce534ad9114046b0a9e00bb30a375" dependencies = [ "opaque-debug", - "polyval", + "polyval 0.4.5", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval 0.5.3", ] [[package]] @@ -1716,6 +1812,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hkdf" @@ -1723,8 +1822,8 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" dependencies = [ - "digest", - "hmac", + "digest 0.9.0", + "hmac 0.10.1", ] [[package]] @@ -1734,7 +1833,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" dependencies = [ "crypto-mac", - "digest", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddca131f3e7f2ce2df364b57949a9d47915cfbd35e46cfee355ccebbf794d6a2" +dependencies = [ + "digest 0.10.1", ] [[package]] @@ -2493,6 +2601,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" +[[package]] +name = "pbkdf2" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4628cc3cf953b82edcd3c1388c5715401420ce5524fedbab426bd5aba017434" +dependencies = [ + "digest 0.10.1", +] + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -2617,6 +2734,18 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -2946,6 +3075,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "salsa20" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0fbb5f676da676c260ba276a8f43a8dc67cf02d1438423aeb1c677a7212686" +dependencies = [ + "cipher 0.3.0", +] + [[package]] name = "schannel" version = "0.1.19" @@ -2983,6 +3121,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scrypt" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e73d6d7c6311ebdbd9184ad6c4447b2f36337e327bda107d3ba9e3c374f9d325" +dependencies = [ + "hmac 0.12.0", + "pbkdf2", + "salsa20", + "sha2 0.10.1", +] + [[package]] name = "search-provider" version = "0.3.0" @@ -3002,14 +3152,14 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2400fb1bf2a87b303ada204946294f932ade4929477e9e2bf66d7b49a66656ec" dependencies = [ - "aes", + "aes 0.6.0", "block-modes", "hkdf", "lazy_static", "num", "rand 0.8.4", "serde", - "sha2", + "sha2 0.9.9", "zbus 1.9.1", "zbus_macros 1.9.1", "zvariant 2.10.0", @@ -3135,13 +3285,24 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if 1.0.0", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest 0.10.1", +] + [[package]] name = "shlex" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 493b7a6..898122c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ gst4gtk = { package = "gst-plugin-gtk4", version = "0.1"} gtk = {package = "gtk4", version = "0.4"} gtk-macros = "0.3" search-provider = "0.3" -hex = "0.4" +hex = { version = "0.4.3", features = [ "serde" ] } image = "0.23" log = "0.4" once_cell = "1.9" @@ -37,3 +37,6 @@ unicase = "2.6" url = "2.2" zbar-rust = "0.0" svg_metadata = "0.4" +scrypt = { version = "0.8.1", default-features = false } +aes-gcm = "0.9.4" +base64 = "0.13.0" diff --git a/src/backup/aegis.rs b/src/backup/aegis.rs index 2e11b50..7ad1a7a 100644 --- a/src/backup/aegis.rs +++ b/src/backup/aegis.rs @@ -5,46 +5,88 @@ use super::{Backupable, Restorable, RestorableItem}; use crate::models::{Account, Algorithm, OTPMethod, Provider, ProvidersModel}; +use aes_gcm::aead::Aead; +use aes_gcm::NewAead; use anyhow::Result; use gettextrs::gettext; use gtk::{glib::Cast, prelude::*}; -use serde::{Deserialize, Serialize}; +use hex; use log; +use serde::{Deserialize, Serialize}; /// Root of the Aegis JSON Backup Format #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct Aegis { - pub version: u32, - pub header: std::collections::HashMap, - pub db: AegisDatabase, +#[serde(untagged)] +pub enum Aegis { + /// Plaintext version of the JSON format. + Plaintext { + version: u32, + header: std::collections::HashMap, + db: Database, + }, + /// Encrypted version of the JSON format. `db` is simply a base64 encoded string with encrypted AegisDatabase. + Encrypted { + version: u32, + header: HeaderEncrypted, + db: String, + }, } -/// Header of the Aegis JSON File +/// Header of the Encrypted Aegis JSON File /// /// Contains all necessary information for encrypting / decrypting the vault (db field). #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct AegisHeader { - pub slots: Vec>, - pub params: std::collections::HashMap +pub struct HeaderEncrypted { + pub slots: Vec, + pub params: HeaderParam, } +/// Header Slots +/// +/// Containts information to decrypt the master key. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum HeaderSlot { + // We are not interested in this field. Thus, we omit the other information. + #[serde(rename = "2")] + Biometric, + #[serde(rename = "1")] + Password(HeaderSlotPassword), +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct HeaderSlotPassword { + pub uuid: String, + #[serde(with = "hex::serde")] + pub key: [u8; 32], + // First tuple entry is the nonce, the second is the tag. + pub key_params: HeaderParam, + pub n: u32, + pub r: u32, + pub p: u32, + #[serde(with = "hex::serde")] + pub salt: [u8; 32], +} + +/// Parameters to Database Encryption +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct HeaderParam { + #[serde(with = "hex::serde")] + pub nonce: [u8; 12], + #[serde(with = "hex::serde")] + pub tag: [u8; 16], +} /// Contains All OTP Entries -/// -/// This database can be either encrypted or in plaintext format. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum AegisDatabase { - Encrypted(String), - Plaintext { - version: u32, - entries: Vec, - }, +pub struct Database { + pub version: u32, + pub entries: Vec, } /// An OTP Entry #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct AegisItem { +pub struct Item { #[serde(rename = "type")] pub method: OTPMethod, // UUID is omitted @@ -58,15 +100,15 @@ pub struct AegisItem { // Icon: // TODO: Aegis encodes icons as JPEG's encoded in Base64 with padding. Does authenticator support // this? - // TODO tags are not importet/exported right now. + // TODO tags are not imported/exported right now. #[serde(rename = "icon")] pub thumbnail: Option, - pub info: AegisDetail, + pub info: Detail, } /// OTP Entry Details #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct AegisDetail { +pub struct Detail { pub secret: String, #[serde(rename = "algo")] pub algorithm: Algorithm, @@ -76,35 +118,139 @@ pub struct AegisDetail { } impl Aegis { - fn restore_from_slice(data: &[u8]) -> Result> { + fn restore_from_slice(data: &[u8], password: Option<&str>) -> Result> { // TODO check whether file / database is encrypted by aegis let aegis_root: Aegis = serde_json::de::from_slice(data)?; - // Check for correct aegis file version and correct database version. - // Additionally, we check whether the file is encrypted. We can't open / decrypt them yet, - // because there is no UI possibility to enter the password. - log::info!("Found aegis database version {}", aegis_root.version); - if aegis_root.version != 1 { - anyhow::bail!( - "Aegis file version expected to be 1. Found {} instead.", - aegis_root.version - ); - } + // Check whether file is encrypted or in plaintext + match aegis_root { + Aegis::Plaintext { version, db, .. } => { + log::info!( + "Found unencrypted aegis vault with version {} and database version {}.", + version, + db.version + ); - match aegis_root.db { - AegisDatabase::Encrypted(_) => anyhow::bail!( - "Aegis file is encrypted. Authenticator supports only plaintext files." - ), - AegisDatabase::Plaintext { version, entries } if version == 2 => Ok(entries), - AegisDatabase::Plaintext { version, .. } => anyhow::bail!( - "Aegis file version expected to be 2. Found {} instead.", - version - ), + // Check for correct aegis vault version and correct database version. + if version != 1 { + anyhow::bail!( + "Aegis vault version expected to be 1. Found {} instead.", + version + ); + // There is no version 0. So this should be okay ... + } else if db.version > 2 { + anyhow::bail!( + "Aegis database version expected to be 1 or 2. Found {} instead.", + db.version + ); + } else { + Ok(db.entries) + } + } + Aegis::Encrypted { + version, + header, + db, + } => { + log::info!("Found encrypted aegis vault with version {}.", version); + + // Check for correct aegis vault version and whether a password was supplied. + if version != 1 { + anyhow::bail!( + "Aegis vault version expected to be 1. Found {} instead.", + version + ); + } else if password.is_none() { + anyhow::bail!("Found encrypted aegis database but no password given."); + } + + // Ciphertext is stored in base64, we have to decode it. + let mut ciphertext = + base64::decode(db).expect("Cannot decode (base64) encrypted database"); + + // Add the encryption tag + ciphertext.append(&mut header.params.tag.into()); + + // Find slots with type password and derive the corresponding key. This key is used + // to decrypt the master key which in turn can be used to decrypt the database. + let master_keys: Vec> = header + .slots + .iter() + .map(|slot| match slot { + HeaderSlot::Password(slot) => slot, + _ => unreachable!("All biometric slots are filtered by serde."), + }) + .map(|slot| { + log::info!("Found possible master key with UUID {}.", slot.uuid); + + // Create parameters for scrypt function and derive decryption key for master key + let params = scrypt::Params::new( + // TODO log2 for u64 is not stable yet. Change this in the future. + (slot.n as f64).log2() as u8, // Defaults to 15 by aegis + slot.r, // Defaults to 8 by aegis + slot.p, // Defaults to 1 by aegis + ) + .expect("Invalid scrypt params in header"); + let mut temp_key: [u8; 32] = [0u8; 32]; + scrypt::scrypt( + password.unwrap().as_bytes(), + &slot.salt, + ¶ms, + &mut temp_key, + ) + .expect("Scrypt key derivation failed"); + + // Now, try to decrypt the master key. + let cipher = aes_gcm::Aes256Gcm::new(aes_gcm::Key::from_slice(&temp_key)); + let mut ciphertext: Vec = slot.key.to_vec(); + ciphertext.append(&mut slot.key_params.tag.to_vec()); + let master_key = cipher + .decrypt( + aes_gcm::Nonce::from_slice(&slot.key_params.nonce), + ciphertext.as_ref(), + ) + .expect("failed decrypting master key"); + + master_key + }) + .collect(); + + // Choose the first valid master key. I don't think there are aegis installations with two valid password slots. + log::info!( + "Found {} valid password slots / master keys. Using only the first one.", + master_keys.len() + ); + let master_key = &master_keys[0]; + + // Try to decrypt the database with this master key + let cipher = aes_gcm::Aes256Gcm::new(aes_gcm::Key::from_slice(master_key)); + let plaintext = cipher + .decrypt( + aes_gcm::Nonce::from_slice(&header.params.nonce), + ciphertext.as_ref(), + ) + .expect("failed decrypting database"); + + // Now, we have the decrypted string. Trying to load it with JSON. + let db: Database = serde_json::de::from_slice(&plaintext).unwrap(); + + // Check version of the database + log::info!("Found aegis database with version {}.", db.version); + if version > 2 { + anyhow::bail!( + "Aegis database version expected to be 1 or 2. Found {} instead.", + db.version + ); + } + + // Return items + Ok(db.entries) + } } } } -impl RestorableItem for AegisItem { +impl RestorableItem for Item { fn account(&self) -> String { self.label.clone() } @@ -164,15 +310,17 @@ impl Backupable for Aegis { for j in 0..accounts.n_items() { let account = accounts.item(j).unwrap().downcast::().unwrap(); - let aegis_detail = AegisDetail { + let aegis_detail = Detail { secret: account.token(), algorithm: provider.algorithm(), digits: provider.digits(), + // TODO should be none for hotp period: Some(provider.period()), + // TODO should be none for totp counter: Some(account.counter()), }; - let aegis_item = AegisItem { + let aegis_item = Item { method: provider.method(), label: account.name(), issuer: provider.name(), @@ -186,11 +334,15 @@ impl Backupable for Aegis { } // Create structure around items - let aegis_db = AegisDatabase::Plaintext { + let aegis_db = Database { version: 2, entries: items, }; - let aegis_root = Aegis { + // let aegis_header = AegisHeader { + // slots: vec![], + // params: std::collections::HashMap::new(), + // }; + let aegis_root = Aegis::Plaintext { version: 1, header: std::collections::HashMap::from([ (String::from("slots"), serde_json::Value::Null), @@ -215,7 +367,7 @@ impl Backupable for Aegis { impl Restorable for Aegis { const ENCRYPTABLE: bool = true; - type Item = AegisItem; + type Item = Item; fn identifier() -> String { "Aegis".to_string() @@ -230,9 +382,9 @@ impl Restorable for Aegis { gettext("From a JSON file containing plain-text or encrypted fields.") } - fn restore(from: >k::gio::File, _key: Option<&str>) -> Result> { + fn restore(from: >k::gio::File, key: Option<&str>) -> Result> { let (data, _) = from.load_contents(gtk::gio::Cancellable::NONE)?; - Aegis::restore_from_slice(&data) + Aegis::restore_from_slice(&data, key) } } @@ -294,7 +446,7 @@ mod tests { } }"#; - let aegis_items = Aegis::restore_from_slice(&aegis_data.as_bytes()) + let aegis_items = Aegis::restore_from_slice(&aegis_data.as_bytes(), None) .expect("Restoring from json should work"); assert_eq!(aegis_items[0].account(), "Bob"); @@ -326,8 +478,7 @@ mod tests { } #[test] - #[should_panic] - fn detect_encrypted_file() { + fn restore_encrypted_file() { // See https://github.com/beemdevelopment/Aegis/blob/master/app/src/test/resources/com/beemdevelopment/aegis/importers/aegis_encrypted.json // for this example file. let aegis_data = r#"{ @@ -357,8 +508,35 @@ mod tests { "db": "RtGfUrZ01nzRnvHjPJGyWjfa6shQ7NYwa491CgAWNBM8OeGZVIHhnDAVlVWNlSoq2V097p5Yq5m+SFl5g9nBBBQBNePQnj6CCvu1NfNtoA6R3hyp77gd+e+O2MRnOGH1Z1laV2Tl6p3q8IUHWgAJ36LbUxiCXmfh7bWm198uA4bgLwrEmo04MrqeYXggLuXrJrp6dUJQFD72dgoPbHijlSycY5GLel3ZbAXRsUHszd+xdywpj7\/TYa4OYFel0M0QcCpsKA1LRQz365X9OXPJdTsmVyR4dJ6x5RIVeh39lAYKUf7T4w7BLC8taST5m4J\/VXDueKbvg8R13bNWF0aRHUgeuI9BNzMZINJlzKFKNRknTaJ\/1kEUU0sLkgcaVkX\/DVTGG+pWi5MHijicrK0i4LHN3CUwV2\/\/ZNJCGXM5ErsKMOnJfma52gMdifPiXU317Klvc5oOZFYGnhbhJ2WtPIuqjdvnfuLat2JxA7Xx3LqquRWGL2113yjzVzGBDCVY6iIdedBEgH8CGD826\/3R3m6dR5sfSggQ2SbtQA\/DZNhLSNSU+bfNScVQvUWfR2Lf7Q\/4FR\/xATAQJ9IIBeL+w2ErLUPjURocFXup5YOBHxFdDjZ2FqhbAq4h3Zn\/BJ57xUcYEA+YtP5uOP2lQwUh\/0vFWizDVotzraO8tZiBZBsODyb69eJrXNwFbIjeUczY6wrJs1+676IilbCsmtoYvWEpUZF4hIi7TYAD+nyXX\/olrkog9omWZk8R7hJ9KRDfckXEc\/XSzWhk3Kmfa7pRNh9wYZsaR7VPZGZebQMuUKfRRci2qMsZOJvQsDBJvVze0xW9SqiySDgGyRX\/DwzuaZEGZZriaLf6ox7LwY2Qi6QpYOYbAaEaXAesCR1DPxFfGKsUHVjF8hKA6ZBXDXdqM3Y+14naIOH9S7UzYn32botoVLOykSjnW6z6M0ZPkz3dwowMJiVQcyD7p+9p4J6f1S81pFS7DP+jF+PTyC3c3q\/dwFhNdoG6iV9eQEAxjUi6MpzvFRsk9RsLcQqYgzJGmRjYeXlKH8k8tTu1A4puo6w3Daz8hZz9NafMgMsuqY0oKVLgdNqFz8yVMsxYfBW\/oW56SuQyyVWyxXjXmbk1vpYCTL5kXvIZWoTmBRRDb0ay5S\/dlD6z\/WR45\/C4AwcCE9m4Yf3zisRNa7AqWLVgkmJxFdfJxjiuPtUIK79s+lIJkyRENEqkvm809qIxDhkQzY8zcCt4oXCEbJUfSG4awBs1VvilJIwe6qi0bNtqXtAb5TctgxTh29A9oGlsRG4o8sHqA1mtjp5QiLWp5Hh6rOH95W6+fnBiOW+Iw0evBTduroWvx37HBTktJz79zGe0l3c0Y6VmiFvB7knmT2CrgP7woRkxGbXxdE9zMPQJM9ursD538MVDdD\/0tdkxHxilt47f1DPo2CKUWU8Q1KMm1zLXfVO8BbGUWIv4YeDKHfMUL\/HcStv5VJY+LbnOEjzGT4e1\/avSQmqBL4G9XNkYmyMhC8tlLQcmMMH4bNfPOO3vi5Pb5E7XveSgxlOHs4F0+nqxnFOAu8494MEtx6u5+B7d8LI\/DhEO5zTDwE+THiKej6vCsFxTZ519rm67HycOwRR4LKrwfDeUEK3X1PzryOD5zcv3PMcSBgZ8EWvTfZ9ygKP8BmRQRpydTbSt8Hj5fTUuajADCP0Ggw+6G7n+5FhExJNd+o9D8d4KgLPOe08M8InW7pLB389TWtSo4v3VNjcmmJNQ26wlPkhO\/xBU1URFR0fXU3eCO+w++IMt\/fOSqSpNF9bWElfWHIQ23ntxVke\/hR9j\/GG3tHGxYS5pL42sJF\/Re\/UlUJTGSQP6up2xVYs6gncQ0zACDOPjLQmQzYhz\/hr8S6EjYfK++yLZmRTjEI7xT9u\/B5YLyOQCYVTaF\/pDEegjsehXM3qJBfsA+XY7F9TRsmM\/MSVaPDkdIJ7zvL9xtaF6bXdZoZ6po3ml8uu41pSkNmMKgyEy5E0UQUTWMPLC8drUoQ\/KWQnVIN6HUXGBjYy6aax\/LYZaBcbZi97FHK0h+wsx3WN\/uQozNkQjwGYE8fwYxRYh1RaFi5PkiCM505ib7e82Yuts0l+cBb6nG1IruDplg9BD\/G9w4vVDePEikhcPyY\/p7AZ4i7u\/bL2YKlbE3HyJa+7dkbWJgGidtRZgu+Fdl2T\/rrRJ4+lVaKPVKGKT7ItZdIeitIYUdRxCzrOf1ItZCC8BWa4PElDAjj2yDNmMYRpXJBe3gQHWs\/H5SZgFuwsfCu23uzNRQYib8SuwIJQDvPiXo7m4oIySO8VyvemcExlbXSlbZbvwVxYavTVfcUpAXI6qlsg2jjk+JZahfKrWNC5COZPdVjdAXCoiKU+HBPmEFCwQv\/7zlSBEiI2piyqd+MPwnP63RdGO+oXYid6hn4Nm8kcOhtRyvYm95p66jzGlEugsfxJCED7MTh3XShqa2tt4lFG25icllzTvIJboRkz5oIB4dZVS9+q2TgGUoX7UCpobD8WkHo\/y0cpTuZr8vzXqx2fObxzPNoVgxJmp9E06G2bhMVHPpT17xbfq\/KhJJn7k1S0sfXPG+SmYlX4U7zNSe1M7JXtLf3uVOLz7Ccjp3yvcdq8nRmVym3Zwsz+vv57FA2A0dy3Db97ypJa9HGaxnnYIZHHzep0gJCeeIKE9L32zGCoUg+cPu9B2lPEgIr64iGiuvKSRwNQpOBktM6qqjQntE0Me6mh426irFQ\/3tcfH9a4lZEwwuU1X+lUBUWQp3n5Ej4BSJEs8E6H0EjBvyk69q3qjy5yi7ROVRis6y6S1v4er77RHQUf3phK5354VJHrp9pR926t5qngH5RVF4eljwtXDs3MkejADJ6stBHa\/w7FcbUClO8U+S4Bidxb3mZCiZkUVTpbzvBfYAiQvAfdkMa49o3a5DXKsbXyUPrmr6fWRfM1fS0Ehp0lUv6BDj0yR13CLMpKDU4GfDrl8UEvwh7gwtBRkuaBFzyMtd3NeE7kIGf9vFs6MEl2dmMDFSDid7MdVSDVTlhaAtp+zsRejKW3OQr5n051FzkUsIFGty9AWOkwjZCbstHYCOtyJnsnXP1i9lRDFBgPpFgmDD+bzzg0g9AOAxzqTiLF7bb1jejfe5qVr5V9+7zLpwRLiYaLkNOmpsqvNMuYVwdqTp6nyoougdgBlvve3EG0k09sFKi2Ep9lq+QkS7zGre2jJDrqgdC08+V4PXHYkP3V3Zjgn1x6RfQ2PE+2zvk1GGEgzcNww3byoYw0Ra5qS5yftMy\/2WahbA8fjUYvtmksFH8VjN3yasZt3sdQLWtv8qXxZscy+pCyjTdyxW+ddFnrWuqMIV3jbGMvngq6dL\/n5+DumjbA1gmBJVOpmyEsc1iwHDS36cNnyi1htGFO\/6\/Va4YPYK7dG6LY387UoBUU9Q9ijrBrSGpzPWYmXBLZ8e1MMPfHIN1WsaTgYO9leg3MAJTjQFTFrQ5dguYpWhlm2sWJT45jrda4uWqduB+aQLzYRWhEDBFzPV3ZgIe0SB+7h04Vm0Pu\/LDRvqaolpZ86CEm+zgjBOKeEGFwzTXxH\/5pBoca1bZ6wvsbVZxJNBeH8\/w==" }"#; - Aegis::restore_from_slice(&aegis_data.as_bytes()) - .expect("Reading encrypted file should fail."); + let aegis_items = Aegis::restore_from_slice(&aegis_data.as_bytes(), Some("test")) + .expect("Restoring from encrypted json should work"); + + assert_eq!(aegis_items[0].account(), "Mason"); + assert_eq!(aegis_items[0].issuer(), "Deno"); + assert_eq!(aegis_items[0].secret(), "4SJHB4GSD43FZBAI7C2HLRJGPQ"); + assert_eq!(aegis_items[0].period(), Some(30)); + assert_eq!(aegis_items[0].algorithm(), Algorithm::SHA1); + assert_eq!(aegis_items[0].digits(), Some(6)); + assert_eq!(aegis_items[0].counter(), None); + assert_eq!(aegis_items[0].method(), OTPMethod::TOTP); + + assert_eq!(aegis_items[3].account(), "James"); + assert_eq!(aegis_items[3].issuer(), "Issuu"); + assert_eq!(aegis_items[3].secret(), "YOOMIXWS5GN6RTBPUFFWKTW5M4"); + assert_eq!(aegis_items[3].period(), None); + assert_eq!(aegis_items[3].algorithm(), Algorithm::SHA1); + assert_eq!(aegis_items[3].digits(), Some(6)); + assert_eq!(aegis_items[3].counter(), Some(1)); + assert_eq!(aegis_items[3].method(), OTPMethod::HOTP); + + assert_eq!(aegis_items[6].account(), "Sophia"); + assert_eq!(aegis_items[6].issuer(), "Boeing"); + assert_eq!(aegis_items[6].secret(), "JRZCL47CMXVOQMNPZR2F7J4RGI"); + assert_eq!(aegis_items[6].period(), Some(30)); + assert_eq!(aegis_items[6].algorithm(), Algorithm::SHA1); + assert_eq!(aegis_items[6].digits(), Some(5)); + assert_eq!(aegis_items[6].counter(), None); + assert_eq!(aegis_items[6].method(), OTPMethod::Steam); } // TODO: add tests for importing