// Copyright 2015, 2016 Ethcore (UK) Ltd. // This file is part of Parity. // Parity is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity. If not, see . //! Keys Directory use common::*; use std::path::{PathBuf}; const CURRENT_DECLARED_VERSION: u64 = 3; const MAX_KEY_FILE_LEN: u64 = 1024 * 80; const MAX_CACHE_USAGE_TRACK: usize = 128; /// Cipher type (currently only aes-128-ctr) #[derive(PartialEq, Debug, Clone)] pub enum CryptoCipherType { /// aes-128-ctr with 128-bit initialisation vector(iv) Aes128Ctr(H128) } #[derive(PartialEq, Debug, Clone)] enum KeyFileVersion { V3(u64) } /// key generator function #[derive(PartialEq, Debug, Clone)] pub enum Pbkdf2CryptoFunction { /// keyed-hash generator (HMAC-256) HMacSha256 } #[derive(Clone)] /// Kdf of type `Pbkdf2` /// https://en.wikipedia.org/wiki/PBKDF2 pub struct KdfPbkdf2Params { /// desired length of the derived key, in octets pub dk_len: u32, /// cryptographic salt pub salt: H256, /// number of iterations for derived key pub c: u32, /// pseudo-random 2-parameters function pub prf: Pbkdf2CryptoFunction } #[derive(Debug)] enum Pbkdf2ParseError { InvalidParameter(&'static str), InvalidPrf(Mismatch), InvalidSaltFormat(UtilError), MissingParameter(&'static str), } impl KdfPbkdf2Params { fn from_json(json: &BTreeMap) -> Result { Ok(KdfPbkdf2Params{ salt: match try!(json.get("salt").ok_or(Pbkdf2ParseError::MissingParameter("salt"))).as_string() { None => { return Err(Pbkdf2ParseError::InvalidParameter("salt")) }, Some(salt_value) => match H256::from_str(salt_value) { Ok(salt_hex_value) => salt_hex_value, Err(from_hex_error) => { return Err(Pbkdf2ParseError::InvalidSaltFormat(from_hex_error)); }, } }, prf: match try!(json.get("prf").ok_or(Pbkdf2ParseError::MissingParameter("prf"))).as_string() { Some("hmac-sha256") => Pbkdf2CryptoFunction::HMacSha256, Some(unexpected_prf) => { return Err(Pbkdf2ParseError::InvalidPrf(Mismatch { expected: "hmac-sha256".to_owned(), found: unexpected_prf.to_owned() })); }, None => { return Err(Pbkdf2ParseError::InvalidParameter("prf")); }, }, dk_len: try!(try!(json.get("dklen").ok_or(Pbkdf2ParseError::MissingParameter("dklen"))).as_u64().ok_or(Pbkdf2ParseError::InvalidParameter("dkLen"))) as u32, c: try!(try!(json.get("c").ok_or(Pbkdf2ParseError::MissingParameter("c"))).as_u64().ok_or(Pbkdf2ParseError::InvalidParameter("c"))) as u32, }) } fn to_json(&self) -> Json { let mut map = BTreeMap::new(); map.insert("dklen".to_owned(), json_from_u32(self.dk_len)); map.insert("salt".to_owned(), Json::String(format!("{:?}", self.salt))); map.insert("prf".to_owned(), Json::String("hmac-sha256".to_owned())); map.insert("c".to_owned(), json_from_u32(self.c)); Json::Object(map) } } #[derive(Clone)] /// Kdf of type `Scrypt`. /// https://en.wikipedia.org/wiki/Scrypt pub struct KdfScryptParams { /// Desired length of the derived key, in octets. pub dk_len: u32, /// Parallelization parameter. pub p: u32, /// CPU/memory cost parameter. pub n: u32, /// TODO: comment pub r: u32, /// Cryptographic salt. pub salt: H256, } #[derive(Debug)] enum ScryptParseError { InvalidParameter(&'static str), InvalidSaltFormat(UtilError), MissingParameter(&'static str), } fn json_from_u32(number: u32) -> Json { Json::U64(number as u64) } impl KdfScryptParams { fn from_json(json: &BTreeMap) -> Result { Ok(KdfScryptParams{ salt: match try!(json.get("salt").ok_or(ScryptParseError::MissingParameter("salt"))).as_string() { None => { return Err(ScryptParseError::InvalidParameter("salt")) }, Some(salt_value) => match H256::from_str(salt_value) { Ok(salt_hex_value) => salt_hex_value, Err(from_hex_error) => { return Err(ScryptParseError::InvalidSaltFormat(from_hex_error)); }, } }, dk_len: try!(try!(json.get("dklen").ok_or(ScryptParseError::MissingParameter("dklen"))).as_u64().ok_or(ScryptParseError::InvalidParameter("dkLen"))) as u32, p: try!(try!(json.get("p").ok_or(ScryptParseError::MissingParameter("p"))).as_u64().ok_or(ScryptParseError::InvalidParameter("p"))) as u32, n: try!(try!(json.get("n").ok_or(ScryptParseError::MissingParameter("n"))).as_u64().ok_or(ScryptParseError::InvalidParameter("n"))) as u32, r: try!(try!(json.get("r").ok_or(ScryptParseError::MissingParameter("r"))).as_u64().ok_or(ScryptParseError::InvalidParameter("r"))) as u32, }) } fn to_json(&self) -> Json { let mut map = BTreeMap::new(); map.insert("dklen".to_owned(), json_from_u32(self.dk_len)); map.insert("salt".to_owned(), Json::String(format!("{:?}", self.salt))); map.insert("p".to_owned(), json_from_u32(self.p)); map.insert("n".to_owned(), json_from_u32(self.n)); map.insert("r".to_owned(), json_from_u32(self.r)); Json::Object(map) } } #[derive(Clone)] /// Settings for password derived key geberator function. pub enum KeyFileKdf { /// Password-Based Key Derivation Function 2 (PBKDF2) type. /// https://en.wikipedia.org/wiki/PBKDF2 Pbkdf2(KdfPbkdf2Params), /// Scrypt password-based key derivation function. /// https://en.wikipedia.org/wiki/Scrypt Scrypt(KdfScryptParams) } #[derive(Clone)] /// Encrypted password or other arbitrary message /// with settings for password derived key generator for decrypting content. pub struct KeyFileCrypto { /// Cipher type. pub cipher_type: CryptoCipherType, /// Cipher text (encrypted message). pub cipher_text: Bytes, /// Password derived key generator function settings. pub kdf: KeyFileKdf, /// Mac pub mac: H256 } impl KeyFileCrypto { fn from_json(json: &Json) -> Result { let as_object = match json.as_object() { None => { return Err(CryptoParseError::InvalidJsonFormat); } Some(obj) => obj }; let cipher_type = match try!(as_object.get("cipher").ok_or(CryptoParseError::NoCipherType)).as_string() { None => { return Err(CryptoParseError::InvalidCipherType(Mismatch { expected: "aes-128-ctr".to_owned(), found: "not a json string".to_owned() })); } Some("aes-128-ctr") => CryptoCipherType::Aes128Ctr( match try!(as_object.get("cipherparams").ok_or(CryptoParseError::NoCipherParameters)).as_object() { None => { return Err(CryptoParseError::NoCipherParameters); }, Some(cipher_param) => match H128::from_str(match cipher_param["iv"].as_string() { None => { return Err(CryptoParseError::NoInitialVector); }, Some(iv_hex_string) => iv_hex_string }) { Ok(iv_value) => iv_value, Err(hex_error) => { return Err(CryptoParseError::InvalidInitialVector(hex_error)); } } } ), Some(other_cipher_type) => { return Err(CryptoParseError::InvalidCipherType( Mismatch { expected: "aes-128-ctr".to_owned(), found: other_cipher_type.to_owned() })); } }; let kdf = match (try!(as_object.get("kdf").ok_or(CryptoParseError::NoKdf)).as_string(), try!(as_object.get("kdfparams").ok_or(CryptoParseError::NoKdfType)).as_object()) { (None, _) => { return Err(CryptoParseError::NoKdfType); }, (Some("scrypt"), Some(kdf_params)) => match KdfScryptParams::from_json(kdf_params) { Err(scrypt_params_error) => { return Err(CryptoParseError::Scrypt(scrypt_params_error)); }, Ok(scrypt_params) => KeyFileKdf::Scrypt(scrypt_params) }, (Some("pbkdf2"), Some(kdf_params)) => match KdfPbkdf2Params::from_json(kdf_params) { Err(pbkdf2_params_error) => { return Err(CryptoParseError::KdfPbkdf2(pbkdf2_params_error)); }, Ok(pbkdf2_params) => KeyFileKdf::Pbkdf2(pbkdf2_params) }, (Some(other_kdf), _) => { return Err(CryptoParseError::InvalidKdfType( Mismatch { expected: "pbkdf2/scrypt".to_owned(), found: other_kdf.to_owned()})); } }; let cipher_text = match try!(as_object.get("ciphertext").ok_or(CryptoParseError::NoCipherText)).as_string() { None => { return Err(CryptoParseError::InvalidCipherText); } Some(text) => text }; let mac: H256 = match try!(as_object.get("mac").ok_or(CryptoParseError::NoMac)).as_string() { None => { return Err(CryptoParseError::InvalidMacFormat(None)) }, Some(salt_value) => match H256::from_str(salt_value) { Ok(salt_hex_value) => salt_hex_value, Err(from_hex_error) => { return Err(CryptoParseError::InvalidMacFormat(Some(from_hex_error))); }, } }; Ok(KeyFileCrypto { cipher_text: match FromHex::from_hex(cipher_text) { Ok(bytes) => bytes, Err(_) => { return Err(CryptoParseError::InvalidCipherText); } }, cipher_type: cipher_type, kdf: kdf, mac: mac, }) } fn to_json(&self) -> Json { let mut map = BTreeMap::new(); match self.cipher_type { CryptoCipherType::Aes128Ctr(ref iv) => { map.insert("cipher".to_owned(), Json::String("aes-128-ctr".to_owned())); let mut cipher_params = BTreeMap::new(); cipher_params.insert("iv".to_owned(), Json::String(format!("{:?}", iv))); map.insert("cipherparams".to_owned(), Json::Object(cipher_params)); } } map.insert("ciphertext".to_owned(), Json::String( self.cipher_text.iter().map(|b| format!("{:02x}", b)).collect::>().join(""))); map.insert("kdf".to_owned(), Json::String(match self.kdf { KeyFileKdf::Pbkdf2(_) => "pbkdf2".to_owned(), KeyFileKdf::Scrypt(_) => "scrypt".to_owned() })); map.insert("kdfparams".to_owned(), match self.kdf { KeyFileKdf::Pbkdf2(ref pbkdf2_params) => pbkdf2_params.to_json(), KeyFileKdf::Scrypt(ref scrypt_params) => scrypt_params.to_json() }); map.insert("mac".to_owned(), Json::String(format!("{:?}", self.mac))); Json::Object(map) } /// New pbkdf2-type secret. /// `cipher-text` - encrypted cipher text. /// `dk-len` - desired length of the derived key, in octets. /// `c` - number of iterations for derived key. /// `salt` - cryptographic site, random 256-bit hash (ensure it's crypto-random). /// `iv` - initialisation vector. pub fn new_pbkdf2(cipher_text: Bytes, iv: H128, salt: H256, mac: H256, c: u32, dk_len: u32) -> KeyFileCrypto { KeyFileCrypto { cipher_type: CryptoCipherType::Aes128Ctr(iv), cipher_text: cipher_text, kdf: KeyFileKdf::Pbkdf2(KdfPbkdf2Params { dk_len: dk_len, salt: salt, c: c, prf: Pbkdf2CryptoFunction::HMacSha256 }), mac: mac, } } } /// Universally unique identifier pub type Uuid = H128; fn new_uuid() -> Uuid { H128::random() } fn uuid_to_string(uuid: &Uuid) -> String { let d1 = &uuid.as_slice()[0..4]; let d2 = &uuid.as_slice()[4..6]; let d3 = &uuid.as_slice()[6..8]; let d4 = &uuid.as_slice()[8..10]; let d5 = &uuid.as_slice()[10..16]; format!("{}-{}-{}-{}-{}", d1.to_hex(), d2.to_hex(), d3.to_hex(), d4.to_hex(), d5.to_hex()) } fn uuid_from_string(s: &str) -> Result { let parts: Vec<&str> = s.split("-").collect(); if parts.len() != 5 { return Err(UtilError::BadSize); } let mut uuid = H128::zero(); if parts[0].len() != 8 { return Err(UtilError::BadSize); } uuid[0..4].clone_from_slice(&try!(FromHex::from_hex(parts[0]))); if parts[1].len() != 4 { return Err(UtilError::BadSize); } uuid[4..6].clone_from_slice(&try!(FromHex::from_hex(parts[1]))); if parts[2].len() != 4 { return Err(UtilError::BadSize); } uuid[6..8].clone_from_slice(&try!(FromHex::from_hex(parts[2]))); if parts[3].len() != 4 { return Err(UtilError::BadSize); } uuid[8..10].clone_from_slice(&try!(FromHex::from_hex(parts[3]))); if parts[4].len() != 12 { return Err(UtilError::BadSize); } uuid[10..16].clone_from_slice(&try!(FromHex::from_hex(parts[4]))); Ok(uuid) } #[derive(Clone)] /// Stored key file struct with encrypted message (cipher_text) /// also contains password derivation function settings (PBKDF2/Scrypt) pub struct KeyFileContent { version: KeyFileVersion, /// Holds cypher and decrypt function settings. pub crypto: KeyFileCrypto, /// The identifier. pub id: Uuid, /// Account (if present) pub account: Option
, } #[derive(Debug)] enum CryptoParseError { InvalidMacFormat(Option), NoMac, NoCipherText, InvalidCipherText, NoCipherType, InvalidJsonFormat, InvalidKdfType(Mismatch), InvalidCipherType(Mismatch), NoInitialVector, NoCipherParameters, InvalidInitialVector(UtilError), NoKdf, NoKdfType, Scrypt(ScryptParseError), KdfPbkdf2(Pbkdf2ParseError) } #[derive(Debug)] enum KeyFileParseError { InvalidVersion, UnsupportedVersion(OutOfBounds), InvalidJsonFormat, InvalidJson, InvalidIdentifier, NoCryptoSection, Crypto(CryptoParseError), } impl KeyFileContent { /// New stored key file struct with encrypted message (cipher_text) /// also contains password derivation function settings (PBKDF2/Scrypt) /// to decrypt cipher_text given the password is provided. pub fn new(crypto: KeyFileCrypto) -> KeyFileContent { KeyFileContent { id: new_uuid(), version: KeyFileVersion::V3(3), crypto: crypto, account: None } } pub fn load(json: &Json) -> Result { match Self::from_json(json) { Ok(key_file) => Ok(key_file), Err(e) => { warn!(target: "sstore", "Error parsing json for key: {:?}", e); Err(()) } } } /// Returns key file version if it is known. pub fn version(&self) -> Option { match self.version { KeyFileVersion::V3(declared) => Some(declared) } } fn from_json(json: &Json) -> Result { let as_object = match json.as_object() { None => { return Err(KeyFileParseError::InvalidJsonFormat); }, Some(obj) => obj }; let version = match as_object["version"].as_u64() { None => { return Err(KeyFileParseError::InvalidVersion); }, Some(json_version) => { if json_version <= 2 { return Err(KeyFileParseError::UnsupportedVersion(OutOfBounds { min: Some(3), max: None, found: json_version })) }; KeyFileVersion::V3(json_version) } }; let id_text = try!(as_object.get("id").and_then(|json| json.as_string()).ok_or(KeyFileParseError::InvalidIdentifier)); let id = match uuid_from_string(&id_text) { Err(_) => { return Err(KeyFileParseError::InvalidIdentifier); }, Ok(id) => id }; let account = as_object.get("address").and_then(|json| json.as_string()).and_then( |account_text| match Address::from_str(account_text) { Ok(account) => Some(account), Err(_) => None }); let crypto = match as_object.get("crypto") { None => { return Err(KeyFileParseError::NoCryptoSection); } Some(crypto_json) => match KeyFileCrypto::from_json(crypto_json) { Ok(crypto) => crypto, Err(crypto_error) => { return Err(KeyFileParseError::Crypto(crypto_error)); } } }; Ok(KeyFileContent { version: version, id: id.clone(), crypto: crypto, account: account }) } fn to_json(&self) -> Json { let mut map = BTreeMap::new(); map.insert("id".to_owned(), Json::String(uuid_to_string(&self.id))); map.insert("version".to_owned(), Json::U64(CURRENT_DECLARED_VERSION)); map.insert("crypto".to_owned(), self.crypto.to_json()); Json::Object(map) } } #[derive(Debug)] enum KeyFileLoadError { TooLarge(OutOfBounds), ParseError(KeyFileParseError), ReadError(::std::io::Error), } /// Represents directory for saving/loading key files. pub struct KeyDirectory { /// Directory path for key management. path: String, cache: RefCell>, cache_usage: RefCell>, } impl KeyDirectory { /// Initializes new cache directory context with a given `path` pub fn new(path: &Path) -> KeyDirectory { KeyDirectory { cache: RefCell::new(HashMap::new()), path: path.to_str().expect("Initialized key directory with empty path").to_owned(), cache_usage: RefCell::new(VecDeque::new()), } } /// saves (inserts or updates) given key pub fn save(&mut self, key_file: KeyFileContent) -> Result<(Uuid), ::std::io::Error> { { let mut file = try!(fs::File::create(self.key_path(&key_file.id))); let json = key_file.to_json(); let json_text = format!("{}", json.pretty()); let json_bytes = json_text.into_bytes(); try!(file.write(&json_bytes)); } let mut cache = self.cache.borrow_mut(); let id = key_file.id.clone(); cache.insert(id.clone(), key_file); Ok(id.clone()) } /// Returns key given by id if corresponding file exists and no load error occured. /// Warns if any error occured during the key loading pub fn get(&self, id: &Uuid) -> Option { let path = self.key_path(id); { let mut usage = self.cache_usage.borrow_mut(); usage.push_back(id.clone()); } if !self.cache.borrow().contains_key(id) { match KeyDirectory::load_key(&path) { Ok(loaded_key) => { self.cache.borrow_mut().insert(id.to_owned(), loaded_key); } Err(error) => { warn!(target: "sstore", "error loading key {:?}: {:?}", id, error); return None; } } } // todo: replace with Ref::map when it stabilized to avoid copies Some(self.cache.borrow().get(id) .expect("Key should be there, we have just inserted or checked it.") .clone()) } /// Returns current path to the directory with keys pub fn path(&self) -> &str { &self.path } /// Removes keys that never been requested during last `MAX_USAGE_TRACK` times pub fn collect_garbage(&mut self) { let mut cache_usage = self.cache_usage.borrow_mut(); let total_usages = cache_usage.len(); let untracked_usages = max(total_usages as i64 - MAX_CACHE_USAGE_TRACK as i64, 0) as usize; if untracked_usages > 0 { cache_usage.drain(..untracked_usages); } if self.cache.borrow().len() <= MAX_CACHE_USAGE_TRACK { return; } let uniqs: HashSet<&Uuid> = cache_usage.iter().collect(); let removes:Vec = { let cache = self.cache.borrow(); cache.keys().cloned().filter(|key| !uniqs.contains(key)).collect() }; if removes.is_empty() { return; } let mut cache = self.cache.borrow_mut(); for key in removes { cache.remove(&key); } } /// Reports how many keys are currently cached. pub fn cache_size(&self) -> usize { self.cache.borrow().len() } /// Removes key file from key directory pub fn delete(&mut self, id: &Uuid) -> Result<(), ::std::io::Error> { let path = self.key_path(id); if !self.cache.borrow().contains_key(id) { return match fs::remove_file(&path) { Ok(_) => { self.cache.borrow_mut().remove(&id); Ok(()) }, Err(e) => Err(e) }; } Ok(()) } /// Enumerates all keys in the directory pub fn list(&self) -> Result, ::std::io::Error> { let mut result = Vec::new(); for entry in try!(fs::read_dir(&self.path)) { let entry = try!(entry); if !try!(fs::metadata(entry.path())).is_dir() { match entry.file_name().to_str() { Some(ref name) => { if let Ok(uuid) = uuid_from_string(name) { result.push(uuid); } }, None => { continue; } }; } } Ok(result) } fn key_path(&self, id: &Uuid) -> PathBuf { let mut path = PathBuf::new(); path.push(self.path.clone()); path.push(uuid_to_string(&id)); path } fn load_key(path: &PathBuf) -> Result { match fs::File::open(path.clone()) { Ok(mut open_file) => { match open_file.metadata() { Ok(metadata) => if metadata.len() > MAX_KEY_FILE_LEN { Err(KeyFileLoadError::TooLarge(OutOfBounds { min: Some(2), max: Some(MAX_KEY_FILE_LEN), found: metadata.len() })) } else { KeyDirectory::load_from_file(&mut open_file) }, Err(read_error) => Err(KeyFileLoadError::ReadError(read_error)) } }, Err(read_error) => Err(KeyFileLoadError::ReadError(read_error)) } } fn load_from_file(file: &mut fs::File) -> Result { let mut buf = String::new(); match file.read_to_string(&mut buf) { Ok(_) => {}, Err(read_error) => { return Err(KeyFileLoadError::ReadError(read_error)); } } match Json::from_str(&buf) { Ok(json) => match KeyFileContent::from_json(&json) { Ok(key_file_content) => Ok(key_file_content), Err(parse_error) => Err(KeyFileLoadError::ParseError(parse_error)) }, Err(_) => Err(KeyFileLoadError::ParseError(KeyFileParseError::InvalidJson)) } } } #[cfg(test)] mod file_tests { use super::{KeyFileContent, KeyFileVersion, KeyFileKdf, KeyFileParseError, CryptoParseError, uuid_from_string, uuid_to_string, KeyFileCrypto, KdfPbkdf2Params}; use common::*; #[test] fn uuid_parses() { let uuid = uuid_from_string("3198bc9c-6672-5ab3-d995-4942343ae5b6").unwrap(); assert!(uuid > H128::zero()); } #[test] fn uuid_serializes() { let uuid = uuid_from_string("3198bc9c-6fff-5ab3-d995-4942343ae5b6").unwrap(); assert_eq!(uuid_to_string(&uuid), "3198bc9c-6fff-5ab3-d995-4942343ae5b6"); } #[test] fn can_read_keyfile() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" }, "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", "kdf" : "pbkdf2", "kdfparams" : { "c" : 262144, "dklen" : 32, "prf" : "hmac-sha256", "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" }, "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(key_file) => { assert_eq!(KeyFileVersion::V3(3), key_file.version) }, Err(e) => panic!("Error parsing valid file: {:?}", e) } } #[test] fn can_read_scrypt_krf() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e466191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(key_file) => { match key_file.crypto.kdf { KeyFileKdf::Scrypt(_) => {}, _ => { panic!("expected kdf params of crypto to be of scrypt type" ); } } }, Err(e) => panic!("Error parsing valid file: {:?}", e) } } #[test] fn can_return_error_no_id() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e466191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }, "version" : 3 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(_) => { panic!("Should be error of no crypto section, got ok"); }, Err(KeyFileParseError::InvalidIdentifier) => { }, Err(other_error) => { panic!("should be error of no crypto section, got {:?}", other_error); } } } #[test] fn can_return_error_no_crypto() { let json = Json::from_str( r#" { "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(_) => { panic!("Should be error of no identifier, got ok"); }, Err(KeyFileParseError::NoCryptoSection) => { }, Err(other_error) => { panic!("should be error of no identifier, got {:?}", other_error); } } } #[test] fn can_return_error_unsupported_version() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e466191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 1 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(_) => { panic!("should be error of unsupported version, got ok"); }, Err(KeyFileParseError::UnsupportedVersion(_)) => { }, Err(other_error) => { panic!("should be error of unsupported version, got {:?}", other_error); } } } #[test] fn can_return_error_initial_vector() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e4______66191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(_) => { panic!("should be error of invalid initial vector, got ok"); }, Err(KeyFileParseError::Crypto(CryptoParseError::InvalidInitialVector(_))) => { }, Err(other_error) => { panic!("should be error of invalid initial vector, got {:?}", other_error); } } } #[test] fn can_return_error_for_invalid_scrypt_kdf() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e466191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen2" : 32, "n5" : "xx", "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } "#).unwrap(); match KeyFileContent::from_json(&json) { Ok(_) => { panic!("Should be error of no identifier, got ok"); }, Err(KeyFileParseError::Crypto(CryptoParseError::Scrypt(_))) => { }, Err(other_error) => { panic!("should be error of no identifier, got {:?}", other_error); } } } #[test] fn can_serialize_scrypt_back() { let json = Json::from_str( r#" { "crypto" : { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e466191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }, "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", "version" : 3 } "#).unwrap(); let key = KeyFileContent::from_json(&json).unwrap(); let serialized = key.to_json(); assert!(serialized.as_object().is_some()); } #[test] fn can_create_key_with_new_id() { let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32)); assert!(!uuid_to_string(&key.id).is_empty()); } #[test] fn can_load_json_from_itself() { let cipher_text: Bytes = FromHex::from_hex("aaaaaaaaaaaaaaaaaaaaaaaaaaa22222222222222222222222").unwrap(); let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32)); let json = key.to_json(); let loaded_key = KeyFileContent::from_json(&json).unwrap(); assert_eq!(loaded_key.id, key.id); } #[test] fn can_parse_kdf_params_fail() { let json = Json::from_str( r#" { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" } "#).unwrap(); { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("dklen"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("n"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("r"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("p"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("salt"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } } #[test] fn can_parse_kdf_params_scrypt_fail() { let json = Json::from_str( r#" { "dklen" : 32, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" } "#).unwrap(); { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("dklen"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("r"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("p"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.remove("salt"); let kdf = KdfPbkdf2Params::from_json(&invalid_json); assert!(!kdf.is_ok()); } } #[test] fn can_parse_crypto_fails() { let json = Json::from_str( r#" { "cipher" : "aes-128-ctr", "cipherparams" : { "iv" : "83dbcc02d8ccb40e466191a123791e0e" }, "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", "kdf" : "scrypt", "kdfparams" : { "dklen" : 32, "n" : 262144, "r" : 1, "p" : 8, "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" }, "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" }"#).unwrap(); { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.insert("cipher".to_owned(), Json::String("unknown".to_owned())); let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); assert!(!crypto.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.insert("kdfparams".to_owned(), Json::String("122".to_owned())); let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); assert!(!crypto.is_ok()); } { let mut invalid_json = json.as_object().unwrap().clone(); invalid_json.insert("kdf".to_owned(), Json::String("15522".to_owned())); let crypto = KeyFileCrypto::from_json(&Json::Object(invalid_json)); assert!(!crypto.is_ok()); } } } #[cfg(test)] mod directory_tests { use super::{KeyDirectory, new_uuid, uuid_to_string, KeyFileContent, KeyFileCrypto, MAX_CACHE_USAGE_TRACK}; use common::*; use devtools::*; #[test] fn key_directory_locates_keys() { let temp_path = RandomTempPath::create_dir(); let directory = KeyDirectory::new(temp_path.as_path()); let uuid = new_uuid(); let path = directory.key_path(&uuid); assert!(path.to_str().unwrap().contains(&uuid_to_string(&uuid))); } #[test] fn loads_key() { let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32))).unwrap(); let path = directory.key_path(&uuid); let key = KeyDirectory::load_key(&path).unwrap(); assert_eq!(key.id, uuid); } #[test] fn caches_keys() { let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let mut keys = Vec::new(); for _ in 0..1000 { let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); keys.push(directory.save(key).unwrap()); } for key_id in keys { directory.get(&key_id).unwrap(); } assert_eq!(1000, directory.cache_size()) } #[test] fn collects_garbage() { let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let mut keys = Vec::new(); for _ in 0..1000 { let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); keys.push(directory.save(key).unwrap()); } for key_id in keys { directory.get(&key_id).unwrap(); } directory.collect_garbage(); // since all keys are different, should be exactly MAX_CACHE_USAGE_TRACK assert_eq!(MAX_CACHE_USAGE_TRACK, directory.cache_size()) } #[test] fn collects_garbage_on_empty() { let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); directory.collect_garbage(); assert_eq!(0, directory.cache_size()) } } #[cfg(test)] mod specs { use super::*; use common::*; use devtools::*; #[test] fn can_initiate_key_directory() { let temp_path = RandomTempPath::create_dir(); let directory = KeyDirectory::new(&temp_path.as_path()); assert!(directory.path().len() > 0); } #[test] fn can_save_key() { let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text, H128::zero(), H256::random(), H256::random(), 32, 32))); assert!(uuid.is_ok()); } #[test] fn can_load_key() { let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let uuid = directory.save(KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32))).unwrap(); let key = directory.get(&uuid).unwrap(); assert_eq!(key.crypto.cipher_text, cipher_text); } #[test] fn can_store_10_keys() { let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let mut keys = Vec::new(); for _ in 0..10 { let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); keys.push(directory.save(key).unwrap()); } assert_eq!(10, keys.len()) } #[test] fn can_list_keys() { let temp_path = RandomTempPath::create_dir(); let mut directory = KeyDirectory::new(&temp_path.as_path()); let cipher_text: Bytes = FromHex::from_hex("a0f05555").unwrap(); let mut keys = Vec::new(); for _ in 0..33 { let key = KeyFileContent::new(KeyFileCrypto::new_pbkdf2(cipher_text.clone(), H128::zero(), H256::random(), H256::random(), 32, 32)); keys.push(directory.save(key).unwrap()); } assert_eq!(33, directory.list().unwrap().len()); } }