EIP-2386 (draft): Eth2 wallet (#1117)

* Add test to understand flow of key storage

* First commit

* Committing to save trait stuff

* Working naive design

* Add keystore struct

* Move keystore files into their own module

* Add serde (de)serialize_with magic

* Add keystore test

* Fix tests

* Add comments and minor fixes

* Pass optional params to `to_keystore` function

* Add `path` field to keystore

* Add function to read Keystore from file

* Add test vectors and fix Version serialization

* Checksum params is empty object

* Add public key to Keystore

* Add function for saving keystore into file

* Deleted account_manager main.rs

* Move keystore module to validator_client

* Add save_keystore method to validator_directory

* Add load_keystore function. Minor refactorings

* Fixed dependencies

* Address some review comments

* Add Password newtype; derive Zeroize

* Fix test

* Move keystore into own crate

* Remove padding

* Add error enum, zeroize more things

* Fix comment

* Add keystore builder

* Remove keystore stuff from val client

* Add more tests, comments

* Add more comments, test vectors

* Progress on improving JSON validation

* More JSON verification

* Start moving JSON into own mod

* Remove old code

* Add more tests, reader/writers

* Tidy

* Move keystore into own file

* Move more logic into keystore file

* Tidy

* Tidy

* Allow for odd-character hex

* Add more json missing field checks

* Use scrypt by default

* Tidy, address comments

* Test path and uuid in vectors

* Fix comment

* Add checks for kdf params

* Enforce empty kdf message

* Expose json_keystore mod

* First commits on path derivation

* Progress with implementation

* More progress

* Passing intermediate test vectors

* Tidy, add comments

* Add DerivedKey structs

* Move key derivation into own crate

* Add zeroize structs

* Return error for empty seed

* Add tests

* Tidy

* First commits on path derivation

* Progress with implementation

* Move key derivation into own crate

* Start defining JSON wallet

* Add progress

* Split out encrypt/decrypt

* First commits on path derivation

* Progress with implementation

* More progress

* Passing intermediate test vectors

* Tidy, add comments

* Add DerivedKey structs

* Move key derivation into own crate

* Add zeroize structs

* Return error for empty seed

* Add tests

* Tidy

* Add progress

* Replace some password usage with slice

* First commits on path derivation

* Progress with implementation

* More progress

* Passing intermediate test vectors

* Tidy, add comments

* Add DerivedKey structs

* Move key derivation into own crate

* Add zeroize structs

* Return error for empty seed

* Add tests

* Tidy

* Add progress

* Expose PlainText struct

* First commits on path derivation

* Progress with implementation

* More progress

* Passing intermediate test vectors

* Tidy, add comments

* Add DerivedKey structs

* Move key derivation into own crate

* Add zeroize structs

* Return error for empty seed

* Add tests

* Tidy

* Add builder

* Expose consts, remove Password

* Minor progress

* Expose SALT_SIZE

* First compiling version

* Add test vectors

* Move dbg assert statement

* Add mnemonic, tidy

* Tidy

* Add testing

* Fix broken test

* Address review comments

Co-authored-by: pawan <pawandhananjay@gmail.com>
This commit is contained in:
Paul Hauner
2020-05-12 08:54:59 +10:00
committed by GitHub
parent 5530bb195f
commit cab6c58923
13 changed files with 1775 additions and 13 deletions

View File

@@ -0,0 +1,65 @@
use serde::{Deserialize, Serialize};
use serde_repr::*;
use std::convert::TryFrom;
pub use eth2_keystore::json_keystore::{
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, Kdf, KdfModule,
Scrypt, Sha256Checksum,
};
pub use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct JsonWallet {
pub crypto: Crypto,
pub name: String,
// TODO: confirm if this field is optional or not.
//
// Reference:
//
// https://github.com/sigp/lighthouse/pull/1117#discussion_r422892396
pub nextaccount: u32,
pub uuid: Uuid,
pub version: Version,
#[serde(rename = "type")]
pub type_field: TypeField,
}
/// Version for `JsonWallet`.
#[derive(Debug, Clone, PartialEq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
pub enum Version {
V1 = 1,
}
impl Version {
pub fn one() -> Self {
Version::V1
}
}
/// Used for ensuring that serde only decodes valid checksum functions.
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub enum TypeField {
Hd,
}
impl Into<String> for TypeField {
fn into(self) -> String {
match self {
TypeField::Hd => "hierarchical deterministic".into(),
}
}
}
impl TryFrom<String> for TypeField {
type Error = String;
fn try_from(s: String) -> Result<Self, Self::Error> {
match s.as_ref() {
"hierarchical deterministic" => Ok(TypeField::Hd),
other => Err(format!("Unsupported type function: {}", other)),
}
}
}

View File

@@ -0,0 +1,11 @@
mod validator_path;
mod wallet;
pub mod json_wallet;
pub use bip39;
pub use validator_path::{KeyType, ValidatorPath, COIN_TYPE, PURPOSE};
pub use wallet::{
recover_validator_secret, DerivedKey, Error, KeystoreError, PlainText, ValidatorKeystores,
Wallet, WalletBuilder,
};

View File

@@ -0,0 +1,41 @@
use std::fmt;
use std::iter::Iterator;
pub const PURPOSE: u32 = 12381;
pub const COIN_TYPE: u32 = 3600;
pub enum KeyType {
Voting,
Withdrawal,
}
pub struct ValidatorPath(Vec<u32>);
impl ValidatorPath {
pub fn new(index: u32, key_type: KeyType) -> Self {
let mut vec = vec![PURPOSE, COIN_TYPE, index, 0];
match key_type {
KeyType::Voting => vec.push(0),
KeyType::Withdrawal => {}
}
Self(vec)
}
pub fn iter_nodes(&self) -> impl Iterator<Item = &u32> {
self.0.iter()
}
}
impl fmt::Display for ValidatorPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "m")?;
for node in self.iter_nodes() {
write!(f, "/{}", node)?;
}
Ok(())
}
}

View File

@@ -0,0 +1,287 @@
use crate::{
json_wallet::{
Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, JsonWallet,
Kdf, KdfModule, Sha256Checksum, TypeField, Version,
},
KeyType, ValidatorPath,
};
use eth2_keystore::{
decrypt, default_kdf, encrypt, keypair_from_secret, Keystore, KeystoreBuilder, IV_SIZE,
SALT_SIZE,
};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use uuid::Uuid;
pub use bip39::{Mnemonic, Seed as Bip39Seed};
pub use eth2_key_derivation::DerivedKey;
pub use eth2_keystore::{Error as KeystoreError, PlainText};
#[derive(Debug, PartialEq)]
pub enum Error {
KeystoreError(KeystoreError),
PathExhausted,
EmptyPassword,
EmptySeed,
}
impl From<KeystoreError> for Error {
fn from(e: KeystoreError) -> Error {
Error::KeystoreError(e)
}
}
/// Contains the two keystores required for an eth2 validator.
pub struct ValidatorKeystores {
/// Contains the secret key used for signing every-day consensus messages (blocks,
/// attestations, etc).
pub voting: Keystore,
/// Contains the secret key that should eventually be required for withdrawing stacked ETH.
pub withdrawal: Keystore,
}
/// Constructs a `Keystore`.
///
/// Generates the KDF `salt` and AES `IV` using `rand::thread_rng()`.
pub struct WalletBuilder<'a> {
seed: PlainText,
password: &'a [u8],
kdf: Kdf,
cipher: Cipher,
uuid: Uuid,
name: String,
nextaccount: u32,
}
impl<'a> WalletBuilder<'a> {
/// Creates a new builder for a seed specified as a BIP-39 `Mnemonic` (where the nmemonic itself does
/// not have a passphrase).
///
/// ## Errors
///
/// Returns `Error::EmptyPassword` if `password == ""`.
pub fn from_mnemonic(
mnemonic: &Mnemonic,
password: &'a [u8],
name: String,
) -> Result<Self, Error> {
// TODO: `bip39` does not use zeroize. Perhaps we should make a PR upstream?
let seed = Bip39Seed::new(mnemonic, "");
Self::from_seed_bytes(seed.as_bytes(), password, name)
}
/// Creates a new builder from a `seed` specified as a byte slice.
///
/// ## Errors
///
/// Returns `Error::EmptyPassword` if `password == ""`.
pub fn from_seed_bytes(seed: &[u8], password: &'a [u8], name: String) -> Result<Self, Error> {
if password.is_empty() {
Err(Error::EmptyPassword)
} else if seed.is_empty() {
Err(Error::EmptySeed)
} else {
let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>();
let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into();
Ok(Self {
seed: seed.to_vec().into(),
password,
kdf: default_kdf(salt.to_vec()),
cipher: Cipher::Aes128Ctr(Aes128Ctr { iv }),
uuid: Uuid::new_v4(),
nextaccount: 0,
name,
})
}
}
/// Consumes `self`, returning an encrypted `Wallet`.
pub fn build(self) -> Result<Wallet, Error> {
Wallet::encrypt(
self.seed.as_bytes(),
self.password,
self.kdf,
self.cipher,
self.uuid,
self.name,
self.nextaccount,
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Wallet {
json: JsonWallet,
}
impl Wallet {
/// Instantiates `Self`, encrypting the `seed` using `password` (via `kdf` and `cipher`).
///
/// The `uuid`, `name` and `nextaccount` are carried through into the created wallet.
fn encrypt(
seed: &[u8],
password: &[u8],
kdf: Kdf,
cipher: Cipher,
uuid: Uuid,
name: String,
nextaccount: u32,
) -> Result<Self, Error> {
let (cipher_text, checksum) = encrypt(&seed, &password, &kdf, &cipher)?;
Ok(Self {
json: JsonWallet {
crypto: Crypto {
kdf: KdfModule {
function: kdf.function(),
params: kdf,
message: EmptyString,
},
checksum: ChecksumModule {
function: Sha256Checksum::function(),
params: EmptyMap,
message: checksum.to_vec().into(),
},
cipher: CipherModule {
function: cipher.function(),
params: cipher,
message: cipher_text.into(),
},
},
uuid,
nextaccount,
version: Version::one(),
type_field: TypeField::Hd,
name,
},
})
}
/// Produces a `Keystore` (encrypted with `keystore_password`) for the validator at
/// `self.nextaccount`, incrementing `self.nextaccount` if the keystore was successfully
/// generated.
///
/// Uses the default encryption settings of `KeystoreBuilder`, not necessarily those that were
/// used to encrypt `self`.
///
/// ## Errors
///
/// - If `wallet_password` is unable to decrypt `self`.
/// - If `keystore_password.is_empty()`.
/// - If `self.nextaccount == u32::max_value()`.
pub fn next_validator(
&mut self,
wallet_password: &[u8],
voting_keystore_password: &[u8],
withdrawal_keystore_password: &[u8],
) -> Result<ValidatorKeystores, Error> {
// Helper closure to reduce code duplication when generating keys.
//
// It is not a function on `self` to help protect against generating keys without
// incrementing `nextaccount`.
let derive = |key_type: KeyType, password: &[u8]| -> Result<Keystore, Error> {
let (secret, path) =
recover_validator_secret(&self, wallet_password, self.json.nextaccount, key_type)?;
let keypair = keypair_from_secret(secret.as_bytes())?;
KeystoreBuilder::new(&keypair, password, format!("{}", path))?
.build()
.map_err(Into::into)
};
let keystores = ValidatorKeystores {
voting: derive(KeyType::Voting, voting_keystore_password)?,
withdrawal: derive(KeyType::Withdrawal, withdrawal_keystore_password)?,
};
self.json.nextaccount = self
.json
.nextaccount
.checked_add(1)
.ok_or_else(|| Error::PathExhausted)?;
Ok(keystores)
}
/// Returns the value of the JSON wallet `nextaccount` field.
///
/// This will be the index of the next wallet generated with `Self::next_validator`.
pub fn nextaccount(&self) -> u32 {
self.json.nextaccount
}
/// Returns the value of the JSON wallet `name` field.
pub fn name(&self) -> &str {
&self.json.name
}
/// Returns the value of the JSON wallet `uuid` field.
pub fn uuid(&self) -> &Uuid {
&self.json.uuid
}
/// Returns the value of the JSON wallet `type` field.
pub fn type_field(&self) -> String {
self.json.type_field.clone().into()
}
/// Returns the master seed of this wallet. Care should be taken not to leak this seed.
pub fn decrypt_seed(&self, password: &[u8]) -> Result<PlainText, Error> {
decrypt(password, &self.json.crypto).map_err(Into::into)
}
/// Encodes `self` as a JSON object.
pub fn to_json_string(&self) -> Result<String, Error> {
serde_json::to_string(self)
.map_err(|e| KeystoreError::UnableToSerialize(format!("{}", e)))
.map_err(Into::into)
}
/// Returns `self` from an encoded JSON object.
pub fn from_json_str(json_string: &str) -> Result<Self, Error> {
serde_json::from_str(json_string)
.map_err(|e| KeystoreError::InvalidJson(format!("{}", e)))
.map_err(Into::into)
}
/// Encodes self as a JSON object to the given `writer`.
pub fn to_json_writer<W: Write>(&self, writer: W) -> Result<(), Error> {
serde_json::to_writer(writer, self)
.map_err(|e| KeystoreError::WriteError(format!("{}", e)))
.map_err(Into::into)
}
/// Instantiates `self` from a JSON `reader`.
pub fn from_json_reader<R: Read>(reader: R) -> Result<Self, Error> {
serde_json::from_reader(reader)
.map_err(|e| KeystoreError::ReadError(format!("{}", e)))
.map_err(Into::into)
}
}
/// Returns `(secret, path)` for the `key_type` for the validator at `index`.
///
/// This function should only be used for recovering lost keys, not creating new ones because it
/// does not update `wallet.nextaccount`. Using this function to generate new keys can easily
/// result in the same key being unknowingly generated twice.
///
/// To generate consecutive keys safely, use `Wallet::next_voting_keystore`.
pub fn recover_validator_secret(
wallet: &Wallet,
wallet_password: &[u8],
index: u32,
key_type: KeyType,
) -> Result<(PlainText, ValidatorPath), Error> {
let path = ValidatorPath::new(index, key_type);
let secret = wallet.decrypt_seed(wallet_password)?;
let master = DerivedKey::from_seed(secret.as_bytes()).map_err(|()| Error::EmptyPassword)?;
let destination = path.iter_nodes().fold(master, |dk, i| dk.child(*i));
Ok((destination.secret().to_vec().into(), path))
}