diff --git a/Cargo.lock b/Cargo.lock index 9a44c5f559..878ee69468 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,6 +474,16 @@ dependencies = [ "types", ] +[[package]] +name = "bincode" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30d3a39baa26f9651f17b375061f3233dde33424a8b72b0dbe93a68a0bc896d" +dependencies = [ + "byteorder", + "serde", +] + [[package]] name = "bitflags" version = "0.9.1" @@ -1632,7 +1642,7 @@ dependencies = [ "hmac 0.9.0", "pbkdf2 0.5.0", "rand 0.7.3", - "scrypt", + "scrypt 0.4.1", "serde", "serde_json", "serde_repr", @@ -2347,6 +2357,16 @@ dependencies = [ "digest 0.8.1", ] +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + [[package]] name = "hmac" version = "0.9.0" @@ -3933,6 +3953,15 @@ dependencies = [ "crypto-mac 0.7.0", ] +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac 0.8.0", +] + [[package]] name = "pbkdf2" version = "0.5.0" @@ -4761,6 +4790,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scrypt" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e7e75e27e8cd47e4be027d4b9fdc0b696116f981c22de21ca7bad63a9cb33a" +dependencies = [ + "hmac 0.8.1", + "pbkdf2 0.4.0", + "sha2 0.9.1", +] + [[package]] name = "scrypt" version = "0.4.1" @@ -6356,6 +6396,7 @@ name = "validator_client" version = "0.2.13" dependencies = [ "account_utils", + "bincode", "bls", "clap", "clap_utils", @@ -6381,6 +6422,7 @@ dependencies = [ "rand 0.7.3", "rayon", "ring", + "scrypt 0.3.1", "serde", "serde_derive", "serde_json", diff --git a/crypto/bls/src/zeroize_hash.rs b/crypto/bls/src/zeroize_hash.rs index 3d81df1d81..41136f97a7 100644 --- a/crypto/bls/src/zeroize_hash.rs +++ b/crypto/bls/src/zeroize_hash.rs @@ -1,9 +1,11 @@ use super::SECRET_KEY_BYTES_LEN; +use serde_derive::{Deserialize, Serialize}; use zeroize::Zeroize; /// Provides a wrapper around a `[u8; SECRET_KEY_BYTES_LEN]` that implements `Zeroize` on `Drop`. -#[derive(Zeroize)] +#[derive(Zeroize, Serialize, Deserialize)] #[zeroize(drop)] +#[serde(transparent)] pub struct ZeroizeHash([u8; SECRET_KEY_BYTES_LEN]); impl ZeroizeHash { diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 1557aa2b45..a4035b731e 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -24,6 +24,7 @@ slot_clock = { path = "../common/slot_clock" } types = { path = "../consensus/types" } serde = "1.0.116" serde_derive = "1.0.116" +bincode = "1.3.1" serde_json = "1.0.58" serde_yaml = "0.8.13" slog = { version = "2.5.2", features = ["max_level_trace", "release_max_level_trace"] } @@ -57,3 +58,4 @@ serde_utils = { path = "../consensus/serde_utils" } libsecp256k1 = "0.3.5" ring = "0.16.12" rand = "0.7.3" +scrypt = { version = "0.3.0", default-features = false } diff --git a/validator_client/src/initialized_validators.rs b/validator_client/src/initialized_validators.rs index dbab008e89..09fd2ae9d4 100644 --- a/validator_client/src/initialized_validators.rs +++ b/validator_client/src/initialized_validators.rs @@ -11,15 +11,19 @@ use account_utils::{ validator_definitions::{ self, SigningDefinition, ValidatorDefinition, ValidatorDefinitions, CONFIG_FILENAME, }, + ZeroizeString, }; use eth2_keystore::Keystore; -use slog::{error, info, warn, Logger}; -use std::collections::HashMap; +use slog::{debug, error, info, warn, Logger}; +use std::collections::{HashMap, HashSet}; use std::fs::{self, File, OpenOptions}; use std::io; use std::path::PathBuf; use types::{Keypair, PublicKey}; +use crate::key_cache; +use crate::key_cache::KeyCache; + // Use TTY instead of stdin to capture passwords from users. const USE_STDIN: bool = false; @@ -37,9 +41,11 @@ pub enum Error { }, /// There was a filesystem error when opening the keystore. UnableToOpenVotingKeystore(io::Error), + UnableToOpenKeyCache(key_cache::Error), /// The keystore path is not as expected. It should be a file, not `..` or something obscure /// like that. BadVotingKeystorePath(PathBuf), + BadKeyCachePath(PathBuf), /// The keystore could not be parsed, it is likely bad JSON. UnableToParseVotingKeystore(eth2_keystore::Error), /// The keystore could not be decrypted. The password might be wrong. @@ -79,6 +85,59 @@ pub struct InitializedValidator { signing_method: SigningMethod, } +fn open_keystore(path: &PathBuf) -> Result { + let keystore_file = File::open(path).map_err(Error::UnableToOpenVotingKeystore)?; + Keystore::from_json_reader(keystore_file).map_err(Error::UnableToParseVotingKeystore) +} + +fn get_lockfile_path(file_path: &PathBuf) -> Option { + file_path + .file_name() + .and_then(|os_str| os_str.to_str()) + .map(|filename| { + file_path + .clone() + .with_file_name(format!("{}.lock", filename)) + }) +} + +fn create_lock_file( + file_path: &PathBuf, + delete_lockfiles: bool, + log: &Logger, +) -> Result<(), Error> { + if file_path.exists() { + if delete_lockfiles { + warn!( + log, + "Deleting validator lockfile"; + "file" => format!("{:?}", file_path) + ); + + fs::remove_file(file_path).map_err(Error::UnableToDeleteLockfile)?; + } else { + return Err(Error::LockfileExists(file_path.clone())); + } + } + // Create a new lockfile. + OpenOptions::new() + .write(true) + .create_new(true) + .open(file_path) + .map_err(Error::UnableToCreateLockfile)?; + Ok(()) +} + +fn remove_lock(lock_path: &PathBuf) { + if lock_path.exists() { + if let Err(e) = fs::remove_file(&lock_path) { + eprintln!("Failed to remove {:?}: {:?}", lock_path, e) + } + } else { + eprintln!("Lockfile missing: {:?}", lock_path) + } +} + impl InitializedValidator { /// Instantiate `self` from a `ValidatorDefinition`. /// @@ -88,10 +147,12 @@ impl InitializedValidator { /// ## Errors /// /// If the validator is unable to be initialized for whatever reason. - pub fn from_definition( + async fn from_definition( def: ValidatorDefinition, delete_lockfiles: bool, log: &Logger, + key_cache: &mut KeyCache, + key_stores: &mut HashMap, ) -> Result { if !def.enabled { return Err(Error::UnableToInitializeDisabledValidator); @@ -105,30 +166,55 @@ impl InitializedValidator { voting_keystore_password_path, voting_keystore_password, } => { - let keystore_file = - File::open(&voting_keystore_path).map_err(Error::UnableToOpenVotingKeystore)?; - let voting_keystore = Keystore::from_json_reader(keystore_file) - .map_err(Error::UnableToParseVotingKeystore)?; + use std::collections::hash_map::Entry::*; + let voting_keystore = match key_stores.entry(voting_keystore_path.clone()) { + Vacant(entry) => entry.insert(open_keystore(&voting_keystore_path)?), + Occupied(entry) => entry.into_mut(), + }; - let voting_keypair = match (voting_keystore_password_path, voting_keystore_password) - { - // If the password is supplied, use it and ignore the path (if supplied). - (_, Some(password)) => voting_keystore - .decrypt_keypair(password.as_ref()) - .map_err(Error::UnableToDecryptKeystore)?, - // If only the path is supplied, use the path. - (Some(path), None) => { - let password = read_password(path) - .map_err(Error::UnableToReadVotingKeystorePassword)?; - - voting_keystore - .decrypt_keypair(password.as_bytes()) - .map_err(Error::UnableToDecryptKeystore)? - } - // If there is no password available, maybe prompt for a password. - (None, None) => { - unlock_keystore_via_stdin_password(&voting_keystore, &voting_keystore_path)? - } + let voting_keypair = if let Some(keypair) = key_cache.get(voting_keystore.uuid()) { + keypair + } else { + let keystore = voting_keystore.clone(); + let keystore_path = voting_keystore_path.clone(); + // Decoding a local keystore can take several seconds, therefore it's best + // to keep if off the core executor. This also has the fortunate effect of + // interrupting the potentially long-running task during shut down. + let (password, keypair) = tokio::task::spawn_blocking(move || { + Ok( + match (voting_keystore_password_path, voting_keystore_password) { + // If the password is supplied, use it and ignore the path + // (if supplied). + (_, Some(password)) => ( + password.as_ref().to_vec().into(), + keystore + .decrypt_keypair(password.as_ref()) + .map_err(Error::UnableToDecryptKeystore)?, + ), + // If only the path is supplied, use the path. + (Some(path), None) => { + let password = read_password(path) + .map_err(Error::UnableToReadVotingKeystorePassword)?; + let keypair = keystore + .decrypt_keypair(password.as_bytes()) + .map_err(Error::UnableToDecryptKeystore)?; + (password, keypair) + } + // If there is no password available, maybe prompt for a password. + (None, None) => { + let (password, keypair) = unlock_keystore_via_stdin_password( + &keystore, + &keystore_path, + )?; + (password.as_ref().to_vec().into(), keypair) + } + }, + ) + }) + .await + .map_err(Error::TokioJoin)??; + key_cache.add(keypair.clone(), voting_keystore.uuid(), password); + keypair }; if voting_keypair.pk != def.voting_public_key { @@ -139,47 +225,16 @@ impl InitializedValidator { } // Append a `.lock` suffix to the voting keystore. - let voting_keystore_lockfile_path = voting_keystore_path - .file_name() - .ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone())) - .and_then(|os_str| { - os_str.to_str().ok_or_else(|| { - Error::BadVotingKeystorePath(voting_keystore_path.clone()) - }) - }) - .map(|filename| { - voting_keystore_path - .clone() - .with_file_name(format!("{}.lock", filename)) - })?; + let voting_keystore_lockfile_path = get_lockfile_path(&voting_keystore_path) + .ok_or_else(|| Error::BadVotingKeystorePath(voting_keystore_path.clone()))?; - if voting_keystore_lockfile_path.exists() { - if delete_lockfiles { - warn!( - log, - "Deleting validator lockfile"; - "file" => format!("{:?}", voting_keystore_lockfile_path) - ); - - fs::remove_file(&voting_keystore_lockfile_path) - .map_err(Error::UnableToDeleteLockfile)?; - } else { - return Err(Error::LockfileExists(voting_keystore_lockfile_path)); - } - } else { - // Create a new lockfile. - OpenOptions::new() - .write(true) - .create_new(true) - .open(&voting_keystore_lockfile_path) - .map_err(Error::UnableToCreateLockfile)?; - } + create_lock_file(&voting_keystore_lockfile_path, delete_lockfiles, &log)?; Ok(Self { signing_method: SigningMethod::LocalKeystore { voting_keystore_path, voting_keystore_lockfile_path, - voting_keystore, + voting_keystore: voting_keystore.clone(), voting_keypair, }, }) @@ -210,16 +265,7 @@ impl Drop for InitializedValidator { voting_keystore_lockfile_path, .. } => { - if voting_keystore_lockfile_path.exists() { - if let Err(e) = fs::remove_file(&voting_keystore_lockfile_path) { - eprintln!( - "Failed to remove {:?}: {:?}", - voting_keystore_lockfile_path, e - ) - } - } else { - eprintln!("Lockfile missing: {:?}", voting_keystore_lockfile_path) - } + remove_lock(voting_keystore_lockfile_path); } } } @@ -229,7 +275,7 @@ impl Drop for InitializedValidator { fn unlock_keystore_via_stdin_password( keystore: &Keystore, keystore_path: &PathBuf, -) -> Result { +) -> Result<(ZeroizeString, Keypair), Error> { eprintln!(""); eprintln!( "The {} file does not contain either of the following fields for {:?}:", @@ -255,7 +301,7 @@ fn unlock_keystore_via_stdin_password( eprintln!(""); match keystore.decrypt_keypair(password.as_ref()) { - Ok(keystore) => break Ok(keystore), + Ok(keystore) => break Ok((password, keystore)), Err(eth2_keystore::Error::InvalidPassword) => { eprintln!("Invalid password, try again (or press Ctrl+c to exit):"); } @@ -269,9 +315,8 @@ fn unlock_keystore_via_stdin_password( /// /// Forms the fundamental list of validators that are managed by this validator client instance. pub struct InitializedValidators { - /// If `true`, no validator will be opened if a lockfile exists. If `false`, a warning will be - /// raised for an existing lockfile, but it will ultimately be ignored. - strict_lockfiles: bool, + /// If `true`, delete any validator keystore lockfiles that would prevent starting. + delete_lockfiles: bool, /// A list of validator definitions which can be stored on-disk. definitions: ValidatorDefinitions, /// The directory that the `self.definitions` will be saved into. @@ -287,11 +332,11 @@ impl InitializedValidators { pub async fn from_definitions( definitions: ValidatorDefinitions, validators_dir: PathBuf, - strict_lockfiles: bool, + delete_lockfiles: bool, log: Logger, ) -> Result { let mut this = Self { - strict_lockfiles, + delete_lockfiles, validators_dir, definitions, validators: HashMap::default(), @@ -393,6 +438,84 @@ impl InitializedValidators { Ok(()) } + /// Tries to decrypt the key cache. + /// + /// Returns `Ok(true)` if decryption was successful, `Ok(false)` if it couldn't get decrypted + /// and an error if a needed password couldn't get extracted. + /// + async fn decrypt_key_cache( + &self, + mut cache: KeyCache, + key_stores: &mut HashMap, + ) -> Result { + //read relevant key_stores + let mut definitions_map = HashMap::new(); + for def in self.definitions.as_slice() { + match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => { + use std::collections::hash_map::Entry::*; + let key_store = match key_stores.entry(voting_keystore_path.clone()) { + Vacant(entry) => entry.insert(open_keystore(voting_keystore_path)?), + Occupied(entry) => entry.into_mut(), + }; + definitions_map.insert(*key_store.uuid(), def); + } + } + } + + //check if all paths are in the definitions_map + for uuid in cache.uuids() { + if !definitions_map.contains_key(uuid) { + warn!( + self.log, + "Unknown uuid in cache"; + "uuid" => format!("{}", uuid) + ); + return Ok(KeyCache::new()); + } + } + + //collect passwords + let mut passwords = Vec::new(); + let mut public_keys = Vec::new(); + for uuid in cache.uuids() { + let def = definitions_map.get(uuid).expect("Existence checked before"); + let pw = match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_password_path, + voting_keystore_password, + voting_keystore_path, + } => { + if let Some(p) = voting_keystore_password { + p.as_ref().to_vec().into() + } else if let Some(path) = voting_keystore_password_path { + read_password(path).map_err(Error::UnableToReadVotingKeystorePassword)? + } else { + let keystore = open_keystore(voting_keystore_path)?; + unlock_keystore_via_stdin_password(&keystore, &voting_keystore_path)? + .0 + .as_ref() + .to_vec() + .into() + } + } + }; + passwords.push(pw); + public_keys.push(def.voting_public_key.clone()); + } + + //decrypt + tokio::task::spawn_blocking(move || match cache.decrypt(passwords, public_keys) { + Ok(_) | Err(key_cache::Error::AlreadyDecrypted) => cache, + _ => KeyCache::new(), + }) + .await + .map_err(Error::TokioJoin) + } + /// Scans `self.definitions` and attempts to initialize and validators which are not already /// initialized. /// @@ -405,31 +528,48 @@ impl InitializedValidators { /// I.e., if there are two different definitions with the same public key then the second will /// be ignored. async fn update_validators(&mut self) -> Result<(), Error> { + //use key cache if available + let mut key_stores = HashMap::new(); + + // Create a lock file for the cache + let key_cache_path = KeyCache::cache_file_path(&self.validators_dir); + let cache_lockfile_path = get_lockfile_path(&key_cache_path) + .ok_or_else(|| Error::BadKeyCachePath(key_cache_path))?; + create_lock_file(&cache_lockfile_path, self.delete_lockfiles, &self.log)?; + + let mut key_cache = self + .decrypt_key_cache( + KeyCache::open_or_create(&self.validators_dir) + .map_err(Error::UnableToOpenKeyCache)?, + &mut key_stores, + ) + .await?; + + let mut disabled_uuids = HashSet::new(); for def in self.definitions.as_slice() { if def.enabled { match &def.signing_definition { - SigningDefinition::LocalKeystore { .. } => { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => { if self.validators.contains_key(&def.voting_public_key) { continue; } - // Decoding a local keystore can take several seconds, therefore it's best - // to keep if off the core executor. This also has the fortunate effect of - // interrupting the potentially long-running task during shut down. - let inner_def = def.clone(); - let strict_lockfiles = self.strict_lockfiles; - let inner_log = self.log.clone(); - let result = tokio::task::spawn_blocking(move || { - InitializedValidator::from_definition( - inner_def, - strict_lockfiles, - &inner_log, - ) - }) - .await - .map_err(Error::TokioJoin)?; + if let Some(key_store) = key_stores.get(voting_keystore_path) { + disabled_uuids.remove(key_store.uuid()); + } - match result { + match InitializedValidator::from_definition( + def.clone(), + self.delete_lockfiles, + &self.log, + &mut key_cache, + &mut key_stores, + ) + .await + { Ok(init) => { self.validators .insert(init.voting_public_key().clone(), init); @@ -455,6 +595,17 @@ impl InitializedValidators { } } else { self.validators.remove(&def.voting_public_key); + match &def.signing_definition { + SigningDefinition::LocalKeystore { + voting_keystore_path, + .. + } => { + if let Some(key_store) = key_stores.get(voting_keystore_path) { + disabled_uuids.insert(*key_store.uuid()); + } + } + } + info!( self.log, "Disabled validator"; @@ -462,6 +613,31 @@ impl InitializedValidators { ); } } + for uuid in disabled_uuids { + key_cache.remove(&uuid); + } + + let validators_dir = self.validators_dir.clone(); + let log = self.log.clone(); + if key_cache.is_modified() { + tokio::task::spawn_blocking(move || { + match key_cache.save(validators_dir) { + Err(e) => warn!( + log, + "Error during saving of key_cache"; + "err" => format!("{:?}", e) + ), + Ok(true) => info!(log, "Modified key_cache saved successfully"), + _ => {} + }; + remove_lock(&cache_lockfile_path); + }) + .await + .map_err(Error::TokioJoin)?; + } else { + debug!(log, "Key cache not modified"); + remove_lock(&cache_lockfile_path); + } Ok(()) } } diff --git a/validator_client/src/key_cache.rs b/validator_client/src/key_cache.rs new file mode 100644 index 0000000000..6da06aaa1b --- /dev/null +++ b/validator_client/src/key_cache.rs @@ -0,0 +1,347 @@ +use account_utils::create_with_600_perms; +use bls::{Keypair, PublicKey}; +use eth2_keystore::json_keystore::{ + Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, KdfModule, + Sha256Checksum, +}; +use eth2_keystore::{ + decrypt, default_kdf, encrypt, keypair_from_secret, Error as KeystoreError, PlainText, Uuid, + ZeroizeHash, IV_SIZE, SALT_SIZE, +}; +use rand::prelude::*; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::OpenOptions; +use std::path::{Path, PathBuf}; +use std::{fs, io}; + +/// The file name for the serialized `KeyCache` struct. +pub const CACHE_FILENAME: &str = "validator_key_cache.json"; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum State { + NotDecrypted, + DecryptedAndSaved, + DecryptedWithUnsavedUpdates, +} + +fn not_decrypted() -> State { + State::NotDecrypted +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct KeyCache { + crypto: Crypto, + uuids: Vec, + #[serde(skip)] + pairs: HashMap, //maps public keystore uuids to their corresponding Keypair + #[serde(skip)] + passwords: Vec, + #[serde(skip)] + #[serde(default = "not_decrypted")] + state: State, +} + +type SerializedKeyMap = HashMap<Uuid, ZeroizeHash>; + +impl KeyCache { + pub fn new() -> Self { + KeyCache { + uuids: Vec::new(), + crypto: Self::init_crypto(), + pairs: HashMap::new(), + passwords: Vec::new(), + state: State::DecryptedWithUnsavedUpdates, + } + } + + pub fn init_crypto() -> Crypto { + let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>(); + let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into(); + + let kdf = default_kdf(salt.to_vec()); + let cipher = Cipher::Aes128Ctr(Aes128Ctr { iv }); + + Crypto { + kdf: KdfModule { + function: kdf.function(), + params: kdf, + message: EmptyString, + }, + checksum: ChecksumModule { + function: Sha256Checksum::function(), + params: EmptyMap, + message: Vec::new().into(), + }, + cipher: CipherModule { + function: cipher.function(), + params: cipher, + message: Vec::new().into(), + }, + } + } + + pub fn cache_file_path<P: AsRef<Path>>(validators_dir: P) -> PathBuf { + validators_dir.as_ref().join(CACHE_FILENAME) + } + + /// Open an existing file or create a new, empty one if it does not exist. + pub fn open_or_create<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> { + let cache_path = Self::cache_file_path(validators_dir.as_ref()); + if !cache_path.exists() { + Ok(Self::new()) + } else { + Self::open(validators_dir) + } + } + + /// Open an existing file, returning an error if the file does not exist. + pub fn open<P: AsRef<Path>>(validators_dir: P) -> Result<Self, Error> { + let cache_path = validators_dir.as_ref().join(CACHE_FILENAME); + let file = OpenOptions::new() + .read(true) + .create_new(false) + .open(&cache_path) + .map_err(Error::UnableToOpenFile)?; + serde_json::from_reader(file).map_err(Error::UnableToParseFile) + } + + fn encrypt(&mut self) -> Result<(), Error> { + self.crypto = Self::init_crypto(); + let secret_map: SerializedKeyMap = self + .pairs + .iter() + .map(|(k, v)| (*k, v.sk.serialize())) + .collect(); + + let raw = PlainText::from( + bincode::serialize(&secret_map).map_err(Error::UnableToSerializeKeyMap)?, + ); + let (cipher_text, checksum) = encrypt( + raw.as_ref(), + Self::password(&self.passwords).as_ref(), + &self.crypto.kdf.params, + &self.crypto.cipher.params, + ) + .map_err(Error::UnableToEncrypt)?; + + self.crypto.cipher.message = cipher_text.into(); + self.crypto.checksum.message = checksum.to_vec().into(); + Ok(()) + } + + /// Stores `Self` encrypted in json format. + /// + /// Will create a new file if it does not exist or over-write any existing file. + /// Returns false iff there are no unsaved changes + pub fn save<P: AsRef<Path>>(&mut self, validators_dir: P) -> Result<bool, Error> { + if self.is_modified() { + self.encrypt()?; + + let cache_path = validators_dir.as_ref().join(CACHE_FILENAME); + let bytes = serde_json::to_vec(self).map_err(Error::UnableToEncodeFile)?; + + let res = if cache_path.exists() { + fs::write(cache_path, &bytes).map_err(Error::UnableToWriteFile) + } else { + create_with_600_perms(&cache_path, &bytes).map_err(Error::UnableToWriteFile) + }; + if res.is_ok() { + self.state = State::DecryptedAndSaved; + } + res.map(|_| true) + } else { + Ok(false) + } + } + + pub fn is_modified(&self) -> bool { + self.state == State::DecryptedWithUnsavedUpdates + } + + pub fn uuids(&self) -> &Vec<Uuid> { + &self.uuids + } + + fn password(passwords: &[PlainText]) -> PlainText { + PlainText::from(passwords.iter().fold(Vec::new(), |mut v, p| { + v.extend(p.as_ref()); + v + })) + } + + pub fn decrypt( + &mut self, + passwords: Vec<PlainText>, + public_keys: Vec<PublicKey>, + ) -> Result<&HashMap<Uuid, Keypair>, Error> { + match self.state { + State::NotDecrypted => { + let password = Self::password(&passwords); + let text = + decrypt(password.as_ref(), &self.crypto).map_err(Error::UnableToDecrypt)?; + let key_map: SerializedKeyMap = + bincode::deserialize(text.as_bytes()).map_err(Error::UnableToParseKeyMap)?; + self.passwords = passwords; + self.pairs = HashMap::new(); + if public_keys.len() != self.uuids.len() { + return Err(Error::PublicKeyMismatch); + } + for (uuid, public_key) in self.uuids.iter().zip(public_keys.iter()) { + if let Some(secret) = key_map.get(uuid) { + let key_pair = keypair_from_secret(secret.as_ref()) + .map_err(Error::UnableToParseKeyPair)?; + if &key_pair.pk != public_key { + return Err(Error::PublicKeyMismatch); + } + self.pairs.insert(*uuid, key_pair); + } else { + return Err(Error::MissingUuidKey); + } + } + self.state = State::DecryptedAndSaved; + Ok(&self.pairs) + } + _ => Err(Error::AlreadyDecrypted), + } + } + + pub fn remove(&mut self, uuid: &Uuid) { + //do nothing in not decrypted state + if let State::NotDecrypted = self.state { + return; + } + self.pairs.remove(uuid); + if let Some(pos) = self.uuids.iter().position(|uuid2| uuid2 == uuid) { + self.uuids.remove(pos); + self.passwords.remove(pos); + } + self.state = State::DecryptedWithUnsavedUpdates; + } + + pub fn add(&mut self, keypair: Keypair, uuid: &Uuid, password: PlainText) { + //do nothing in not decrypted state + if let State::NotDecrypted = self.state { + return; + } + self.pairs.insert(*uuid, keypair); + self.uuids.push(*uuid); + self.passwords.push(password); + self.state = State::DecryptedWithUnsavedUpdates; + } + + pub fn get(&self, uuid: &Uuid) -> Option<Keypair> { + self.pairs.get(uuid).cloned() + } +} + +#[derive(Debug)] +pub enum Error { + /// The cache file could not be opened. + UnableToOpenFile(io::Error), + /// The cache file could not be parsed as JSON. + UnableToParseFile(serde_json::Error), + /// The cache file could not be serialized as YAML. + UnableToEncodeFile(serde_json::Error), + /// The cache file could not be written to the filesystem. + UnableToWriteFile(io::Error), + /// Couldn't decrypt the cache file + UnableToDecrypt(KeystoreError), + UnableToEncrypt(KeystoreError), + /// Couldn't decode the decrypted hashmap + UnableToParseKeyMap(bincode::Error), + UnableToParseKeyPair(KeystoreError), + UnableToSerializeKeyMap(bincode::Error), + PublicKeyMismatch, + MissingUuidKey, + /// Cache file is already decrypted + AlreadyDecrypted, +} + +#[cfg(test)] +mod tests { + use super::*; + use eth2_keystore::json_keystore::{HexBytes, Kdf}; + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct KeyCacheTest { + pub params: Kdf, + //pub checksum: ChecksumModule, + //pub cipher: CipherModule, + uuids: Vec<Uuid>, + } + + #[tokio::test] + async fn test_serialization() { + let mut key_cache = KeyCache::new(); + let key_pair = Keypair::random(); + let uuid = Uuid::from_u128(1); + let password = PlainText::from(vec![1, 2, 3, 4, 5, 6]); + key_cache.add(key_pair, &uuid, password); + + key_cache.crypto.cipher.message = HexBytes::from(vec![7, 8, 9]); + key_cache.crypto.checksum.message = HexBytes::from(vec![10, 11, 12]); + + let binary = serde_json::to_vec(&key_cache).unwrap(); + let clone: KeyCache = serde_json::from_slice(binary.as_ref()).unwrap(); + + assert_eq!(clone.crypto, key_cache.crypto); + assert_eq!(clone.uuids, key_cache.uuids); + } + + #[tokio::test] + async fn test_encryption() { + let mut key_cache = KeyCache::new(); + let keypairs = vec![Keypair::random(), Keypair::random()]; + let uuids = vec![Uuid::from_u128(1), Uuid::from_u128(2)]; + let passwords = vec![ + PlainText::from(vec![1, 2, 3, 4, 5, 6]), + PlainText::from(vec![7, 8, 9, 10, 11, 12]), + ]; + + for ((keypair, uuid), password) in keypairs.iter().zip(uuids.iter()).zip(passwords.iter()) { + key_cache.add(keypair.clone(), uuid, password.clone()); + } + + key_cache.encrypt().unwrap(); + key_cache.state = State::DecryptedAndSaved; + + assert_eq!(&key_cache.uuids, &uuids); + + let mut new_clone = KeyCache { + crypto: key_cache.crypto.clone(), + uuids: key_cache.uuids.clone(), + pairs: Default::default(), + passwords: vec![], + state: State::NotDecrypted, + }; + + new_clone + .decrypt(passwords, keypairs.iter().map(|p| p.pk.clone()).collect()) + .unwrap(); + + let passwords_to_plain = |cache: &KeyCache| -> Vec<Vec<u8>> { + cache + .passwords + .iter() + .map(|x| x.as_bytes().to_vec()) + .collect() + }; + + assert_eq!(key_cache.crypto, new_clone.crypto); + assert_eq!( + passwords_to_plain(&key_cache), + passwords_to_plain(&new_clone) + ); + assert_eq!(key_cache.uuids, new_clone.uuids); + assert_eq!(key_cache.state, new_clone.state); + assert_eq!(key_cache.pairs.len(), new_clone.pairs.len()); + for (key, value) in key_cache.pairs { + assert!(new_clone.pairs.contains_key(&key)); + assert_eq!( + format!("{:?}", value), + format!("{:?}", new_clone.pairs[&key]) + ); + } + } +} diff --git a/validator_client/src/lib.rs b/validator_client/src/lib.rs index 0342711990..3b7dc6b071 100644 --- a/validator_client/src/lib.rs +++ b/validator_client/src/lib.rs @@ -6,6 +6,7 @@ mod duties_service; mod fork_service; mod initialized_validators; mod is_synced; +mod key_cache; mod notifier; mod validator_duty; mod validator_store;