documentation effort
This commit is contained in:
parent
1c57214786
commit
f198e53891
@ -14,19 +14,18 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! SecretStore
|
||||
//! module for managing key files, decrypting and encrypting arbitrary data
|
||||
//! Keys Directory
|
||||
|
||||
use common::*;
|
||||
use std::path::{PathBuf};
|
||||
|
||||
const CURRENT_DECLARED_VERSION: u64 = 3;
|
||||
|
||||
const MAX_KEY_FILE_LEN: u64 = 1024 * 80;
|
||||
|
||||
/// Cipher type (currently only aes-128-ctr)
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum CryptoCipherType {
|
||||
// aes-128-ctr with 128-bit initialisation vector(iv)
|
||||
pub enum CryptoCipherType {
|
||||
/// aes-128-ctr with 128-bit initialisation vector(iv)
|
||||
Aes128Ctr(U128)
|
||||
}
|
||||
|
||||
@ -35,23 +34,25 @@ enum KeyFileVersion {
|
||||
V3(u64)
|
||||
}
|
||||
|
||||
/// key generator function
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum Pbkdf2CryptoFunction {
|
||||
pub enum Pbkdf2CryptoFunction {
|
||||
/// keyed-hash generator (HMAC-256)
|
||||
HMacSha256
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
// Kdf of type `Pbkdf2`
|
||||
// https://en.wikipedia.org/wiki/PBKDF2
|
||||
struct KdfPbkdf2Params {
|
||||
// desired length of the derived key, in octets
|
||||
dkLen: u32,
|
||||
// cryptographic salt
|
||||
salt: H256,
|
||||
// number of iterations for derived key
|
||||
c: u32,
|
||||
// pseudo-random 2-parameters function
|
||||
prf: Pbkdf2CryptoFunction
|
||||
/// Kdf of type `Pbkdf2`
|
||||
/// https://en.wikipedia.org/wiki/PBKDF2
|
||||
pub struct KdfPbkdf2Params {
|
||||
/// desired length of the derived key, in octets
|
||||
pub dkLen: 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)]
|
||||
@ -94,25 +95,24 @@ impl KdfPbkdf2Params {
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
// Kdf of type `Scrypt`
|
||||
// https://en.wikipedia.org/wiki/Scrypt
|
||||
struct KdfScryptParams {
|
||||
// desired length of the derived key, in octets
|
||||
dkLen: u32,
|
||||
// parallelization
|
||||
p: u32,
|
||||
// cpu cost
|
||||
n: u32,
|
||||
// TODO: comment
|
||||
r: u32,
|
||||
// cryptographic salt
|
||||
salt: H256,
|
||||
/// Kdf of type `Scrypt`
|
||||
/// https://en.wikipedia.org/wiki/Scrypt
|
||||
pub struct KdfScryptParams {
|
||||
/// desired length of the derived key, in octets
|
||||
pub dkLen: u32,
|
||||
/// parallelization
|
||||
pub p: u32,
|
||||
/// cpu cost
|
||||
pub n: u32,
|
||||
/// TODO: comment
|
||||
pub r: u32,
|
||||
/// cryptographic salt
|
||||
pub salt: H256,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ScryptParseError {
|
||||
InvalidParameter(&'static str),
|
||||
InvalidPrf(Mismatch<String>),
|
||||
InvalidSaltFormat(UtilError),
|
||||
MissingParameter(&'static str),
|
||||
}
|
||||
@ -148,15 +148,22 @@ impl KdfScryptParams {
|
||||
}
|
||||
}
|
||||
|
||||
enum KeyFileKdf {
|
||||
/// 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)
|
||||
}
|
||||
|
||||
struct KeyFileCrypto {
|
||||
cipher_type: CryptoCipherType,
|
||||
cipher_text: Bytes,
|
||||
kdf: KeyFileKdf,
|
||||
/// Encrypted password or other arbitrary message
|
||||
/// with settings for password derived key generator for decrypting content
|
||||
pub struct KeyFileCrypto {
|
||||
pub cipher_type: CryptoCipherType,
|
||||
pub cipher_text: Bytes,
|
||||
pub kdf: KeyFileKdf,
|
||||
}
|
||||
|
||||
impl KeyFileCrypto {
|
||||
@ -229,38 +236,185 @@ impl KeyFileCrypto {
|
||||
|
||||
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` - ini
|
||||
pub fn new_pbkdf2(cipher_text: Bytes, iv: U128, salt: H256, c: u32, dk_len: u32) -> KeyFileCrypto {
|
||||
KeyFileCrypto {
|
||||
cipher_type: CryptoCipherType::Aes128Ctr(iv),
|
||||
cipher_text: cipher_text,
|
||||
kdf: KeyFileKdf::Pbkdf2(KdfPbkdf2Params {
|
||||
dkLen: dk_len,
|
||||
salt: salt,
|
||||
c: c,
|
||||
prf: Pbkdf2CryptoFunction::HMacSha256
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Uuid = String;
|
||||
/// Universally unique identifier
|
||||
pub type Uuid = H128;
|
||||
|
||||
struct KeyFileContent {
|
||||
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<Uuid, UtilError> {
|
||||
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)
|
||||
}
|
||||
|
||||
/// Stored key file struct with encrypted message (cipher_text)
|
||||
/// also contains password derivation function settings (PBKDF2/Scrypt)
|
||||
pub struct KeyFileContent {
|
||||
version: KeyFileVersion,
|
||||
crypto: KeyFileCrypto,
|
||||
id: Uuid
|
||||
/// holds cypher and decrypt function settings
|
||||
pub crypto: KeyFileCrypto,
|
||||
/// identifier
|
||||
pub id: Uuid
|
||||
}
|
||||
|
||||
struct KeyDirectory {
|
||||
cache: HashMap<Uuid, KeyFileContent>,
|
||||
path: Path,
|
||||
#[derive(Debug)]
|
||||
enum CryptoParseError {
|
||||
NoCipherText,
|
||||
NoCipherType,
|
||||
InvalidJsonFormat,
|
||||
InvalidKdfType(Mismatch<String>),
|
||||
InvalidCipherType(Mismatch<String>),
|
||||
NoInitialVector,
|
||||
NoCipherParameters,
|
||||
InvalidInitialVector(FromHexError),
|
||||
NoKdfType,
|
||||
Scrypt(ScryptParseError),
|
||||
KdfPbkdf2(Pbkdf2ParseError)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum KeyFileParseError {
|
||||
InvalidVersion,
|
||||
UnsupportedVersion(OutOfBounds<u64>),
|
||||
InvalidJsonFormat,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn from_json(json: &Json) -> Result<KeyFileContent, KeyFileParseError> {
|
||||
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 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
|
||||
})
|
||||
}
|
||||
|
||||
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 KeyLoadError {
|
||||
NotFound,
|
||||
InvalidEncoding,
|
||||
FileTooLarge(OutOfBounds<u64>),
|
||||
FileParseError(KeyFileParseError),
|
||||
FileReadError(::std::io::Error)
|
||||
FileReadError(::std::io::Error),
|
||||
}
|
||||
|
||||
/// represents directory for saving/loading key files
|
||||
pub struct KeyDirectory {
|
||||
/// directory path for key management
|
||||
path: String,
|
||||
cache: HashMap<Uuid, KeyFileContent>,
|
||||
cache_usage: VecDeque<Uuid>,
|
||||
}
|
||||
|
||||
impl KeyDirectory {
|
||||
fn key_path(&self, id: &Uuid) -> PathBuf {
|
||||
let mut path = self.path.to_path_buf();
|
||||
path.push(&id);
|
||||
path
|
||||
/// Initializes new cache directory context with a given `path`
|
||||
pub fn new(path: &Path) -> KeyDirectory {
|
||||
KeyDirectory {
|
||||
cache: HashMap::new(),
|
||||
path: path.to_str().expect("Initialized key directory with empty path").to_owned(),
|
||||
cache_usage: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn save(&mut self, key_file: KeyFileContent) -> Result<(), ::std::io::Error> {
|
||||
/// saves (inserts or updates) given key
|
||||
pub fn save(&mut self, key_file: KeyFileContent) -> Result<(), ::std::io::Error> {
|
||||
{
|
||||
let mut file = try!(fs::File::create(self.key_path(&key_file.id)));
|
||||
let json = key_file.to_json();
|
||||
@ -272,28 +426,40 @@ impl KeyDirectory {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(&mut self, id: &Uuid) -> Option<&KeyFileContent> {
|
||||
let path = {
|
||||
let mut path = self.path.to_path_buf();
|
||||
path.push(&id);
|
||||
path
|
||||
};
|
||||
|
||||
/// 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(&mut self, id: &Uuid) -> Option<&KeyFileContent> {
|
||||
let path = self.key_path(id);
|
||||
Some(self.cache.entry(id.to_owned()).or_insert(
|
||||
match KeyDirectory::load_key(&path, id) {
|
||||
match KeyDirectory::load_key(&path) {
|
||||
Ok(loaded_key) => loaded_key,
|
||||
Err(error) => { return None; }
|
||||
Err(error) => {
|
||||
warn!(target: "sstore", "error loading key {:?}: {:?}", id, error);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
fn load_key(path: &PathBuf, id: &Uuid) -> Result<KeyFileContent, KeyLoadError> {
|
||||
/// returns current path to the directory with keys
|
||||
pub fn path(&self) -> &str {
|
||||
&self.path
|
||||
}
|
||||
|
||||
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<KeyFileContent, KeyLoadError> {
|
||||
match fs::File::open(path.clone()) {
|
||||
Ok(mut open_file) => {
|
||||
match open_file.metadata() {
|
||||
Ok(metadata) =>
|
||||
if metadata.len() > MAX_KEY_FILE_LEN { Err(KeyLoadError::FileTooLarge(OutOfBounds { min: Some(2), max: Some(MAX_KEY_FILE_LEN), found: metadata.len() })) }
|
||||
else { KeyDirectory::load_from_file(&mut open_file, metadata.len()) },
|
||||
if metadata.len() > MAX_KEY_FILE_LEN { Err(KeyLoadError::FileTooLarge(OutOfBounds { min: Some(2), max: Some(MAX_KEY_FILE_LEN), found: metadata.len() })) }
|
||||
else { KeyDirectory::load_from_file(&mut open_file, metadata.len()) },
|
||||
Err(read_error) => Err(KeyLoadError::FileReadError(read_error))
|
||||
}
|
||||
},
|
||||
@ -315,90 +481,31 @@ impl KeyDirectory {
|
||||
Ok(key_file_content) => Ok(key_file_content),
|
||||
Err(parse_error) => Err(KeyLoadError::FileParseError(parse_error))
|
||||
},
|
||||
Err(json_error) => Err(KeyLoadError::FileParseError(KeyFileParseError::InvalidJsonFormat))
|
||||
Err(_) => Err(KeyLoadError::FileParseError(KeyFileParseError::InvalidJsonFormat))
|
||||
},
|
||||
Err(error) => Err(KeyLoadError::InvalidEncoding)
|
||||
Err(_) => Err(KeyLoadError::InvalidEncoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum CryptoParseError {
|
||||
NoCryptoVersion,
|
||||
NoCipherText,
|
||||
NoCipherType,
|
||||
InvalidJsonFormat,
|
||||
InvalidCryptoVersion,
|
||||
InvalidKdfType(Mismatch<String>),
|
||||
InvalidCipherType(Mismatch<String>),
|
||||
NoInitialVector,
|
||||
NoCipherParameters,
|
||||
InvalidInitialVector(FromHexError),
|
||||
NoKdfType,
|
||||
NoKdfParams,
|
||||
Scrypt(ScryptParseError),
|
||||
KdfPbkdf2(Pbkdf2ParseError)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum KeyFileParseError {
|
||||
InvalidVersion,
|
||||
UnsupportedVersion(OutOfBounds<u64>),
|
||||
InvalidJsonFormat,
|
||||
InvalidIdentifier,
|
||||
NoCryptoSection,
|
||||
Crypto(CryptoParseError),
|
||||
}
|
||||
|
||||
impl KeyFileContent {
|
||||
fn from_json(json: &Json) -> Result<KeyFileContent, KeyFileParseError> {
|
||||
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 = try!(as_object.get("id").and_then(|json| json.as_string()).ok_or(KeyFileParseError::InvalidIdentifier));
|
||||
|
||||
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.to_owned(),
|
||||
crypto: crypto
|
||||
})
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Json {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("id".to_owned(), Json::String(self.id.to_owned()));
|
||||
map.insert("version".to_owned(), Json::U64(CURRENT_DECLARED_VERSION));
|
||||
map.insert("crypto".to_owned(), self.crypto.to_json());
|
||||
|
||||
Json::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{KeyFileContent, KeyFileVersion, KeyFileKdf, KeyFileParseError, CryptoParseError};
|
||||
use super::{KeyFileContent, KeyFileVersion, KeyFileKdf, KeyFileParseError, CryptoParseError, uuid_from_string, uuid_to_string};
|
||||
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(
|
||||
@ -461,7 +568,7 @@ mod tests {
|
||||
match KeyFileContent::from_json(&json) {
|
||||
Ok(key_file) => {
|
||||
match key_file.crypto.kdf {
|
||||
KeyFileKdf::Scrypt(scrypt_params) => {},
|
||||
KeyFileKdf::Scrypt(_) => {},
|
||||
_ => { panic!("expected kdf params of crypto to be of scrypt type" ); }
|
||||
}
|
||||
},
|
||||
@ -593,3 +700,19 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod specs {
|
||||
use super::*;
|
||||
use common::*;
|
||||
use tests::helpers::*;
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
@ -101,6 +101,7 @@ pub mod spec;
|
||||
pub mod transaction;
|
||||
pub mod views;
|
||||
pub mod receipt;
|
||||
pub mod keys_directory;
|
||||
|
||||
mod common;
|
||||
mod basic_types;
|
||||
@ -123,7 +124,6 @@ mod substate;
|
||||
mod executive;
|
||||
mod externalities;
|
||||
mod verification;
|
||||
mod secret_store;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -46,6 +46,15 @@ impl RandomTempPath {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_dir() -> RandomTempPath {
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push(H32::random().hex());
|
||||
fs::create_dir_all(dir.as_path()).unwrap();
|
||||
RandomTempPath {
|
||||
path: dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_path(&self) -> &PathBuf {
|
||||
&self.path
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user