diff --git a/Cargo.lock b/Cargo.lock index c17c29a3d2a..ad7148eb9dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1296,6 +1296,23 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "eth2_keystore" +version = "0.1.0" +dependencies = [ + "bls", + "eth2_ssz", + "hex 0.3.2", + "rand 0.7.3", + "rust-crypto", + "serde", + "serde_json", + "serde_repr", + "tempfile", + "uuid 0.8.1", + "zeroize 1.1.0", +] + [[package]] name = "eth2_ssz" version = "0.1.2" @@ -3550,7 +3567,7 @@ dependencies = [ "tokio-threadpool", "tokio-timer 0.2.13", "url 1.7.2", - "uuid", + "uuid 0.7.4", "winreg", ] @@ -3650,6 +3667,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time", +] + [[package]] name = "rustc-demangle" version = "0.1.16" @@ -3662,6 +3692,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + [[package]] name = "rustc_version" version = "0.2.3" @@ -4199,9 +4235,9 @@ dependencies = [ name = "state_transition_vectors" version = "0.1.0" dependencies = [ - "eth2_ssz 0.1.2", - "state_processing 0.2.0", - "types 0.2.0", + "eth2_ssz", + "state_processing", + "types", ] [[package]] @@ -4983,6 +5019,16 @@ dependencies = [ "rand 0.6.5", ] +[[package]] +name = "uuid" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11" +dependencies = [ + "rand 0.7.3", + "serde", +] + [[package]] name = "validator_client" version = "0.2.0" @@ -5424,7 +5470,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e68403b858b6af538b11614e62dfe9ab2facba9f13a0cafb974855cfb495ec95" dependencies = [ - "zeroize_derive", + "zeroize_derive 0.1.0", ] [[package]] @@ -5432,6 +5478,9 @@ name = "zeroize" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +dependencies = [ + "zeroize_derive 1.0.0", +] [[package]] name = "zeroize_derive" @@ -5443,3 +5492,15 @@ dependencies = [ "quote 0.6.13", "syn 0.15.44", ] + +[[package]] +name = "zeroize_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" +dependencies = [ + "proc-macro2 1.0.12", + "quote 1.0.4", + "syn 1.0.19", + "synstructure", +] diff --git a/Cargo.toml b/Cargo.toml index 6de08c372b8..04c81a16b3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "eth2/utils/deposit_contract", "eth2/utils/eth2_config", "eth2/utils/eth2_interop_keypairs", + "eth2/utils/eth2_keystore", "eth2/utils/eth2_testnet_config", "eth2/utils/logging", "eth2/utils/eth2_hashing", diff --git a/eth2/utils/eth2_keystore/Cargo.toml b/eth2/utils/eth2_keystore/Cargo.toml new file mode 100644 index 00000000000..917a3f63ba5 --- /dev/null +++ b/eth2/utils/eth2_keystore/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "eth2_keystore" +version = "0.1.0" +authors = ["Pawan Dhananjay "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.7.2" +rust-crypto = "0.2.36" +uuid = { version = "0.8", features = ["serde", "v4"] } +zeroize = { version = "1.0.0", features = ["zeroize_derive"] } +serde = "1.0.102" +serde_repr = "0.1" +hex = "0.3" +bls = { path = "../bls" } +eth2_ssz = { path = "../ssz" } +serde_json = "1.0.41" + +[dev-dependencies] +tempfile = "3.1.0" diff --git a/eth2/utils/eth2_keystore/src/derived_key.rs b/eth2/utils/eth2_keystore/src/derived_key.rs new file mode 100644 index 00000000000..c61d9329297 --- /dev/null +++ b/eth2/utils/eth2_keystore/src/derived_key.rs @@ -0,0 +1,24 @@ +use crate::keystore::DKLEN; +use zeroize::Zeroize; + +/// Provides wrapper around `[u8; DKLEN]` that implements `Zeroize`. +#[derive(Zeroize)] +#[zeroize(drop)] +pub struct DerivedKey([u8; DKLEN as usize]); + +impl DerivedKey { + /// Instantiates `Self` with an all-zeros byte array. + pub fn zero() -> Self { + Self([0; DKLEN as usize]) + } + + /// Returns a mutable reference to the underlying byte array. + pub fn as_mut_bytes(&mut self) -> &mut [u8] { + &mut self.0 + } + + /// Returns a reference to the underlying byte array. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } +} diff --git a/eth2/utils/eth2_keystore/src/json_keystore/checksum_module.rs b/eth2/utils/eth2_keystore/src/json_keystore/checksum_module.rs new file mode 100644 index 00000000000..bbcc418185d --- /dev/null +++ b/eth2/utils/eth2_keystore/src/json_keystore/checksum_module.rs @@ -0,0 +1,75 @@ +//! Defines the JSON representation of the "checksum" module. +//! +//! This file **MUST NOT** contain any logic beyond what is required to serialize/deserialize the +//! data structures. Specifically, there should not be any actual crypto logic in this file. + +use super::hex_bytes::HexBytes; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; +use std::convert::TryFrom; + +/// Used for ensuring that serde only decodes valid checksum functions. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub enum ChecksumFunction { + Sha256, +} + +impl Into for ChecksumFunction { + fn into(self) -> String { + match self { + ChecksumFunction::Sha256 => "sha256".into(), + } + } +} + +impl TryFrom for ChecksumFunction { + type Error = String; + + fn try_from(s: String) -> Result { + match s.as_ref() { + "sha256" => Ok(ChecksumFunction::Sha256), + other => Err(format!("Unsupported checksum function: {}", other)), + } + } +} + +/// Used for ensuring serde only decodes an empty map. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(try_from = "Value", into = "Value")] +pub struct EmptyMap; + +impl Into for EmptyMap { + fn into(self) -> Value { + Value::Object(Map::default()) + } +} + +impl TryFrom for EmptyMap { + type Error = &'static str; + + fn try_from(v: Value) -> Result { + match v { + Value::Object(map) if map.is_empty() => Ok(Self), + _ => Err("Checksum params must be an empty map"), + } + } +} + +/// Checksum module for `Keystore`. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ChecksumModule { + pub function: ChecksumFunction, + pub params: EmptyMap, + pub message: HexBytes, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct Sha256Checksum(String); + +impl Sha256Checksum { + pub fn function() -> ChecksumFunction { + ChecksumFunction::Sha256 + } +} diff --git a/eth2/utils/eth2_keystore/src/json_keystore/cipher_module.rs b/eth2/utils/eth2_keystore/src/json_keystore/cipher_module.rs new file mode 100644 index 00000000000..5300b2f8b28 --- /dev/null +++ b/eth2/utils/eth2_keystore/src/json_keystore/cipher_module.rs @@ -0,0 +1,64 @@ +//! Defines the JSON representation of the "cipher" module. +//! +//! This file **MUST NOT** contain any logic beyond what is required to serialize/deserialize the +//! data structures. Specifically, there should not be any actual crypto logic in this file. + +use super::hex_bytes::HexBytes; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// Used for ensuring that serde only decodes valid cipher functions. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub enum CipherFunction { + Aes128Ctr, +} + +impl Into for CipherFunction { + fn into(self) -> String { + match self { + CipherFunction::Aes128Ctr => "aes-128-ctr".into(), + } + } +} + +impl TryFrom for CipherFunction { + type Error = String; + + fn try_from(s: String) -> Result { + match s.as_ref() { + "aes-128-ctr" => Ok(CipherFunction::Aes128Ctr), + other => Err(format!("Unsupported cipher function: {}", other)), + } + } +} + +/// Cipher module representation. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct CipherModule { + pub function: CipherFunction, + pub params: Cipher, + pub message: HexBytes, +} + +/// Parameters for AES128 with ctr mode. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Aes128Ctr { + pub iv: HexBytes, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +pub enum Cipher { + Aes128Ctr(Aes128Ctr), +} + +impl Cipher { + pub fn function(&self) -> CipherFunction { + match &self { + Cipher::Aes128Ctr(_) => CipherFunction::Aes128Ctr, + } + } +} diff --git a/eth2/utils/eth2_keystore/src/json_keystore/hex_bytes.rs b/eth2/utils/eth2_keystore/src/json_keystore/hex_bytes.rs new file mode 100644 index 00000000000..92d8d16b29b --- /dev/null +++ b/eth2/utils/eth2_keystore/src/json_keystore/hex_bytes.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// To allow serde to encode/decode byte arrays from HEX ASCII strings. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub struct HexBytes(Vec); + +impl HexBytes { + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn len(&self) -> usize { + self.0.len() + } +} + +impl From> for HexBytes { + fn from(vec: Vec) -> Self { + Self(vec) + } +} + +impl Into for HexBytes { + fn into(self) -> String { + hex::encode(self.0) + } +} + +impl TryFrom for HexBytes { + type Error = String; + + fn try_from(s: String) -> Result { + // Left-pad with a zero if there is not an even number of hex digits to ensure + // `hex::decode` doesn't return an error. + let s = if s.len() % 2 != 0 { + format!("0{}", s) + } else { + s + }; + + hex::decode(s) + .map(Self) + .map_err(|e| format!("Invalid hex: {}", e)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn decode(json: &str) -> Vec { + serde_json::from_str::(&format!("\"{}\"", json)) + .expect("should decode json") + .as_bytes() + .to_vec() + } + + #[test] + fn odd_hex_bytes() { + let empty: Vec = vec![]; + + assert_eq!(decode(""), empty, "should decode nothing"); + assert_eq!(decode("00"), vec![0], "should decode 00"); + assert_eq!(decode("0"), vec![0], "should decode 0"); + assert_eq!(decode("01"), vec![1], "should decode 01"); + assert_eq!(decode("1"), vec![1], "should decode 1"); + assert_eq!(decode("0101"), vec![1, 1], "should decode 0101"); + assert_eq!(decode("101"), vec![1, 1], "should decode 101"); + } +} diff --git a/eth2/utils/eth2_keystore/src/json_keystore/kdf_module.rs b/eth2/utils/eth2_keystore/src/json_keystore/kdf_module.rs new file mode 100644 index 00000000000..1a595c45cfb --- /dev/null +++ b/eth2/utils/eth2_keystore/src/json_keystore/kdf_module.rs @@ -0,0 +1,128 @@ +//! Defines the JSON representation of the "kdf" module. +//! +//! This file **MUST NOT** contain any logic beyond what is required to serialize/deserialize the +//! data structures. Specifically, there should not be any actual crypto logic in this file. + +use super::hex_bytes::HexBytes; +use crypto::sha2::Sha256; +use crypto::{hmac::Hmac, mac::Mac}; +use serde::{Deserialize, Serialize}; +use std::convert::TryFrom; + +/// KDF module representation. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KdfModule { + pub function: KdfFunction, + pub params: Kdf, + pub message: EmptyString, +} + +/// Used for ensuring serde only decodes an empty string. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub struct EmptyString; + +impl Into for EmptyString { + fn into(self) -> String { + "".into() + } +} + +impl TryFrom for EmptyString { + type Error = &'static str; + + fn try_from(s: String) -> Result { + match s.as_ref() { + "" => Ok(Self), + _ => Err("kdf message must be empty"), + } + } +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(untagged, deny_unknown_fields)] +pub enum Kdf { + Scrypt(Scrypt), + Pbkdf2(Pbkdf2), +} + +impl Kdf { + pub fn function(&self) -> KdfFunction { + match &self { + Kdf::Pbkdf2(_) => KdfFunction::Pbkdf2, + Kdf::Scrypt(_) => KdfFunction::Scrypt, + } + } +} + +/// PRF for use in `pbkdf2`. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum Prf { + #[serde(rename = "hmac-sha256")] + HmacSha256, +} + +impl Prf { + pub fn mac(&self, password: &[u8]) -> impl Mac { + match &self { + _hmac_sha256 => Hmac::new(Sha256::new(), password), + } + } +} + +impl Default for Prf { + fn default() -> Self { + Prf::HmacSha256 + } +} + +/// Parameters for `pbkdf2` key derivation. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Pbkdf2 { + pub c: u32, + pub dklen: u32, + pub prf: Prf, + pub salt: HexBytes, +} + +/// Used for ensuring that serde only decodes valid KDF functions. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(try_from = "String", into = "String")] +pub enum KdfFunction { + Scrypt, + Pbkdf2, +} + +impl Into for KdfFunction { + fn into(self) -> String { + match self { + KdfFunction::Scrypt => "scrypt".into(), + KdfFunction::Pbkdf2 => "pbkdf2".into(), + } + } +} + +impl TryFrom for KdfFunction { + type Error = String; + + fn try_from(s: String) -> Result { + match s.as_ref() { + "scrypt" => Ok(KdfFunction::Scrypt), + "pbkdf2" => Ok(KdfFunction::Pbkdf2), + other => Err(format!("Unsupported kdf function: {}", other)), + } + } +} + +/// Parameters for `scrypt` key derivation. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Scrypt { + pub dklen: u32, + pub n: u32, + pub r: u32, + pub p: u32, + pub salt: HexBytes, +} diff --git a/eth2/utils/eth2_keystore/src/json_keystore/mod.rs b/eth2/utils/eth2_keystore/src/json_keystore/mod.rs new file mode 100644 index 00000000000..3706082eaa6 --- /dev/null +++ b/eth2/utils/eth2_keystore/src/json_keystore/mod.rs @@ -0,0 +1,52 @@ +//! This module intends to separate the JSON representation of the keystore from the actual crypto +//! logic. +//! +//! This module **MUST NOT** contain any logic beyond what is required to serialize/deserialize the +//! data structures. Specifically, there should not be any actual crypto logic in this file. + +mod checksum_module; +mod cipher_module; +mod hex_bytes; +mod kdf_module; + +pub use checksum_module::{ChecksumModule, EmptyMap, Sha256Checksum}; +pub use cipher_module::{Aes128Ctr, Cipher, CipherModule}; +pub use hex_bytes::HexBytes; +pub use kdf_module::{EmptyString, Kdf, KdfModule, Pbkdf2, Prf, Scrypt}; +pub use uuid::Uuid; + +use serde::{Deserialize, Serialize}; +use serde_repr::*; + +/// JSON representation of [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335) keystore. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct JsonKeystore { + pub crypto: Crypto, + pub uuid: Uuid, + pub path: String, + pub pubkey: String, + pub version: Version, +} + +/// Version for `JsonKeystore`. +#[derive(Debug, Clone, PartialEq, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +pub enum Version { + V4 = 4, +} + +impl Version { + pub fn four() -> Self { + Version::V4 + } +} + +/// Crypto module for keystore. +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Crypto { + pub kdf: KdfModule, + pub checksum: ChecksumModule, + pub cipher: CipherModule, +} diff --git a/eth2/utils/eth2_keystore/src/keystore.rs b/eth2/utils/eth2_keystore/src/keystore.rs new file mode 100644 index 00000000000..2adb7a31d3d --- /dev/null +++ b/eth2/utils/eth2_keystore/src/keystore.rs @@ -0,0 +1,399 @@ +//! Provides a JSON keystore for a BLS keypair, as specified by +//! [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335). + +use crate::derived_key::DerivedKey; +use crate::json_keystore::{ + Aes128Ctr, ChecksumModule, Cipher, CipherModule, Crypto, EmptyMap, EmptyString, JsonKeystore, + Kdf, KdfModule, Scrypt, Sha256Checksum, Version, +}; +use crate::plain_text::PlainText; +use crate::Uuid; +use bls::{Keypair, PublicKey, SecretKey}; +use crypto::{digest::Digest, sha2::Sha256}; +use rand::prelude::*; +use serde::{Deserialize, Serialize}; +use ssz::DecodeError; +use std::io::{Read, Write}; + +/// The byte-length of a BLS secret key. +const SECRET_KEY_LEN: usize = 32; +/// The default byte length of the salt used to seed the KDF. +/// +/// NOTE: there is no clear guidance in EIP-2335 regarding the size of this salt. Neither +/// [pbkdf2](https://www.ietf.org/rfc/rfc2898.txt) or [scrypt](https://tools.ietf.org/html/rfc7914) +/// make a clear statement about what size it should be, however 32-bytes certainly seems +/// reasonable and larger than the EITF examples. +pub const SALT_SIZE: usize = 32; +/// The length of the derived key. +pub const DKLEN: u32 = 32; +/// Size of the IV (initialization vector) used for aes-128-ctr encryption of private key material. +/// +/// NOTE: the EIP-2335 test vectors use a 16-byte IV whilst RFC3868 uses an 8-byte IV. Reference: +/// +/// - https://tools.ietf.org/html/rfc3686 +/// - https://github.com/ethereum/EIPs/issues/2339#issuecomment-623865023 +/// +/// Comment from Carl B, author of EIP-2335: +/// +/// AES CTR IV's should be the same length as the internal blocks in my understanding. (The IV is +/// the first block input.) +/// +/// As far as I know, AES-128-CTR is not defined by the IETF, but by NIST in SP800-38A. +/// (https://csrc.nist.gov/publications/detail/sp/800-38a/final) The test vectors in this standard +/// are 16 bytes. +pub const IV_SIZE: usize = 16; +/// The byte size of a SHA256 hash. +pub const HASH_SIZE: usize = 32; + +#[derive(Debug, PartialEq)] +pub enum Error { + InvalidSecretKeyLen { len: usize, expected: usize }, + InvalidPassword, + InvalidSecretKeyBytes(DecodeError), + PublicKeyMismatch, + EmptyPassword, + UnableToSerialize(String), + InvalidJson(String), + WriteError(String), + ReadError(String), + InvalidPbkdf2Param, + InvalidScryptParam, + IncorrectIvSize { expected: usize, len: usize }, +} + +/// Constructs a `Keystore`. +pub struct KeystoreBuilder<'a> { + keypair: &'a Keypair, + password: &'a [u8], + kdf: Kdf, + cipher: Cipher, + uuid: Uuid, + path: String, +} + +impl<'a> KeystoreBuilder<'a> { + /// Creates a new builder. + /// + /// Generates the KDF `salt` and AES `IV` using `rand::thread_rng()`. + /// + /// ## Errors + /// + /// Returns `Error::EmptyPassword` if `password == ""`. + pub fn new(keypair: &'a Keypair, password: &'a [u8], path: String) -> Result { + if password.is_empty() { + Err(Error::EmptyPassword) + } else { + let salt = rand::thread_rng().gen::<[u8; SALT_SIZE]>(); + let iv = rand::thread_rng().gen::<[u8; IV_SIZE]>().to_vec().into(); + + Ok(Self { + keypair, + password, + kdf: default_kdf(salt.to_vec()), + cipher: Cipher::Aes128Ctr(Aes128Ctr { iv }), + uuid: Uuid::new_v4(), + path, + }) + } + } + + /// Consumes `self`, returning a `Keystore`. + pub fn build(self) -> Result { + Keystore::encrypt( + self.keypair, + self.password, + self.kdf, + self.cipher, + self.uuid, + self.path, + ) + } +} + +/// Provides a BLS keystore as defined in [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335). +/// +/// Use `KeystoreBuilder` to create a new keystore. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(transparent)] +pub struct Keystore { + json: JsonKeystore, +} + +impl Keystore { + /// Generate `Keystore` object for a BLS12-381 secret key from a + /// keypair and password. + fn encrypt( + keypair: &Keypair, + password: &[u8], + kdf: Kdf, + cipher: Cipher, + uuid: Uuid, + path: String, + ) -> Result { + let secret = PlainText::from(keypair.sk.as_raw().as_bytes()); + + let (cipher_text, checksum) = encrypt(secret.as_bytes(), password, &kdf, &cipher)?; + + Ok(Keystore { + json: JsonKeystore { + 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, + path, + pubkey: keypair.pk.as_hex_string()[2..].to_string(), + version: Version::four(), + }, + }) + } + + /// Regenerate a BLS12-381 `Keypair` from `self` and the correct password. + /// + /// ## Errors + /// + /// - The provided password is incorrect. + /// - The keystore is badly formed. + /// + /// ## Panics + /// + /// May panic if provided unreasonable crypto parameters. + pub fn decrypt_keypair(&self, password: &[u8]) -> Result { + let plain_text = decrypt(password, &self.json.crypto)?; + + // Verify that secret key material is correct length. + if plain_text.len() != SECRET_KEY_LEN { + return Err(Error::InvalidSecretKeyLen { + len: plain_text.len(), + expected: SECRET_KEY_LEN, + }); + } + + // Instantiate a `SecretKey`. + let sk = + SecretKey::from_bytes(plain_text.as_bytes()).map_err(Error::InvalidSecretKeyBytes)?; + + // Derive a `PublicKey` from `SecretKey`. + let pk = PublicKey::from_secret_key(&sk); + + // Verify that the derived `PublicKey` matches `self`. + if pk.as_hex_string()[2..].to_string() != self.json.pubkey { + return Err(Error::PublicKeyMismatch); + } + + Ok(Keypair { sk, pk }) + } + + /// Returns the UUID for the keystore. + pub fn uuid(&self) -> &Uuid { + &self.json.uuid + } + + /// Returns the path for the keystore. + /// + /// Note: the path is not validated, it is simply whatever string the keystore provided. + pub fn path(&self) -> &str { + &self.json.path + } + + /// Encodes `self` as a JSON object. + pub fn to_json_string(&self) -> Result { + serde_json::to_string(self).map_err(|e| Error::UnableToSerialize(format!("{}", e))) + } + + /// Returns `self` from an encoded JSON object. + pub fn from_json_str(json_string: &str) -> Result { + serde_json::from_str(json_string).map_err(|e| Error::InvalidJson(format!("{}", e))) + } + + /// Encodes self as a JSON object to the given `writer`. + pub fn to_json_writer(&self, writer: W) -> Result<(), Error> { + serde_json::to_writer(writer, self).map_err(|e| Error::WriteError(format!("{}", e))) + } + + /// Instantiates `self` from a JSON `reader`. + pub fn from_json_reader(reader: R) -> Result { + serde_json::from_reader(reader).map_err(|e| Error::ReadError(format!("{}", e))) + } +} + +/// Returns `Kdf` used by default when creating keystores. +/// +/// Currently this is set to scrypt due to its memory hardness properties. +pub fn default_kdf(salt: Vec) -> Kdf { + Kdf::Scrypt(Scrypt { + dklen: DKLEN, + n: 262144, + p: 1, + r: 8, + salt: salt.into(), + }) +} + +/// Returns `(cipher_text, checksum)` for the given `plain_text` encrypted with `Cipher` using a +/// key derived from `password` via the `Kdf` (key derivation function). +/// +/// ## Errors +/// +/// - The `kdf` is badly formed (e.g., has some values set to zero). +pub fn encrypt( + plain_text: &[u8], + password: &[u8], + kdf: &Kdf, + cipher: &Cipher, +) -> Result<(Vec, [u8; HASH_SIZE]), Error> { + let derived_key = derive_key(&password, &kdf)?; + + // Encrypt secret. + let mut cipher_text = vec![0; plain_text.len()]; + match &cipher { + Cipher::Aes128Ctr(params) => { + crypto::aes::ctr( + crypto::aes::KeySize::KeySize128, + &derived_key.as_bytes()[0..16], + params.iv.as_bytes(), + ) + .process(plain_text, &mut cipher_text); + } + }; + + let checksum = generate_checksum(&derived_key, &cipher_text); + + Ok((cipher_text, checksum)) +} + +/// Regenerate some `plain_text` from the given `password` and `crypto`. +/// +/// ## Errors +/// +/// - The provided password is incorrect. +/// - The `crypto.kdf` is badly formed (e.g., has some values set to zero). +pub fn decrypt(password: &[u8], crypto: &Crypto) -> Result { + let cipher_message = &crypto.cipher.message; + + // Generate derived key + let derived_key = derive_key(password, &crypto.kdf.params)?; + + // Mismatching checksum indicates an invalid password. + if &generate_checksum(&derived_key, cipher_message.as_bytes())[..] + != crypto.checksum.message.as_bytes() + { + return Err(Error::InvalidPassword); + } + + let mut plain_text = PlainText::zero(cipher_message.len()); + match &crypto.cipher.params { + Cipher::Aes128Ctr(params) => { + crypto::aes::ctr( + crypto::aes::KeySize::KeySize128, + &derived_key.as_bytes()[0..16], + // NOTE: we do not check the size of the `iv` as there is no guidance about + // this on EIP-2335. + // + // Reference: + // + // - https://github.com/ethereum/EIPs/issues/2339#issuecomment-623865023 + params.iv.as_bytes(), + ) + .process(cipher_message.as_bytes(), plain_text.as_mut_bytes()); + } + }; + Ok(plain_text) +} + +/// Generates a checksum to indicate that the `derived_key` is associated with the +/// `cipher_message`. +fn generate_checksum(derived_key: &DerivedKey, cipher_message: &[u8]) -> [u8; HASH_SIZE] { + let mut hasher = Sha256::new(); + hasher.input(&derived_key.as_bytes()[16..32]); + hasher.input(cipher_message); + + let mut digest = [0; HASH_SIZE]; + hasher.result(&mut digest); + digest +} + +/// Derive a private key from the given `password` using the given `kdf` (key derivation function). +fn derive_key(password: &[u8], kdf: &Kdf) -> Result { + let mut dk = DerivedKey::zero(); + + match &kdf { + Kdf::Pbkdf2(params) => { + let mut mac = params.prf.mac(password); + + // RFC2898 declares that `c` must be a "positive integer" and the `crypto` crate panics + // if it is `0`. + // + // Both of these seem fairly convincing that it shouldn't be 0. + // + // Reference: + // + // https://www.ietf.org/rfc/rfc2898.txt + // + // Additionally, we always compute a derived key of 32 bytes so reject anything that + // says otherwise. + if params.c == 0 || params.dklen != DKLEN { + return Err(Error::InvalidPbkdf2Param); + } + + crypto::pbkdf2::pbkdf2( + &mut mac, + params.salt.as_bytes(), + params.c, + dk.as_mut_bytes(), + ); + } + Kdf::Scrypt(params) => { + // RFC7914 declares that all these parameters must be greater than 1: + // + // - `N`: costParameter. + // - `r`: blockSize. + // - `p`: parallelizationParameter + // + // Reference: + // + // https://tools.ietf.org/html/rfc7914 + // + // Additionally, we always compute a derived key of 32 bytes so reject anything that + // says otherwise. + if params.n <= 1 || params.r == 0 || params.p == 0 || params.dklen != DKLEN { + return Err(Error::InvalidScryptParam); + } + + // Ensure that `n` is power of 2. + if params.n != 2u32.pow(log2_int(params.n)) { + return Err(Error::InvalidScryptParam); + } + + crypto::scrypt::scrypt( + password, + params.salt.as_bytes(), + &crypto::scrypt::ScryptParams::new(log2_int(params.n) as u8, params.r, params.p), + dk.as_mut_bytes(), + ); + } + } + + Ok(dk) +} + +/// Compute floor of log2 of a u32. +fn log2_int(x: u32) -> u32 { + if x == 0 { + return 0; + } + 31 - x.leading_zeros() +} diff --git a/eth2/utils/eth2_keystore/src/lib.rs b/eth2/utils/eth2_keystore/src/lib.rs new file mode 100644 index 00000000000..ad536685310 --- /dev/null +++ b/eth2/utils/eth2_keystore/src/lib.rs @@ -0,0 +1,15 @@ +//! Provides a JSON keystore for a BLS keypair, as specified by +//! [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335). + +mod derived_key; +mod keystore; +mod plain_text; + +pub mod json_keystore; + +pub use keystore::{ + decrypt, default_kdf, encrypt, Error, Keystore, KeystoreBuilder, DKLEN, HASH_SIZE, IV_SIZE, + SALT_SIZE, +}; +pub use plain_text::PlainText; +pub use uuid::Uuid; diff --git a/eth2/utils/eth2_keystore/src/plain_text.rs b/eth2/utils/eth2_keystore/src/plain_text.rs new file mode 100644 index 00000000000..a643cdfb1fa --- /dev/null +++ b/eth2/utils/eth2_keystore/src/plain_text.rs @@ -0,0 +1,34 @@ +use zeroize::Zeroize; + +/// Provides wrapper around `Vec` that implements `Zeroize`. +#[derive(Zeroize, Clone, PartialEq)] +#[zeroize(drop)] +pub struct PlainText(Vec); + +impl PlainText { + /// Instantiate self with `len` zeros. + pub fn zero(len: usize) -> Self { + Self(vec![0; len]) + } + + /// The byte-length of `self` + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns a reference to the underlying bytes. + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// Returns a mutable reference to the underlying bytes. + pub fn as_mut_bytes(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl From> for PlainText { + fn from(vec: Vec) -> Self { + Self(vec) + } +} diff --git a/eth2/utils/eth2_keystore/tests/eip2335_vectors.rs b/eth2/utils/eth2_keystore/tests/eip2335_vectors.rs new file mode 100644 index 00000000000..fee3c37b72a --- /dev/null +++ b/eth2/utils/eth2_keystore/tests/eip2335_vectors.rs @@ -0,0 +1,107 @@ +//! Test cases taken from: +//! +//! https://eips.ethereum.org/EIPS/eip-2335 + +#![cfg(test)] + +use eth2_keystore::{Keystore, Uuid}; + +const EXPECTED_SECRET: &str = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; +const PASSWORD: &str = "testpassword"; + +pub fn decode_and_check_sk(json: &str) -> Keystore { + let keystore = Keystore::from_json_str(json).expect("should decode keystore json"); + let expected_sk = hex::decode(EXPECTED_SECRET).unwrap(); + let keypair = keystore.decrypt_keypair(PASSWORD.as_bytes()).unwrap(); + assert_eq!(keypair.sk.as_raw().as_bytes(), expected_sk); + keystore +} + +#[test] +fn eip2335_test_vector_scrypt() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + let keystore = decode_and_check_sk(&vector); + assert_eq!( + *keystore.uuid(), + Uuid::parse_str("1d85ae20-35c5-4611-98e8-aa14a633906f").unwrap(), + "uuid" + ); + assert_eq!(keystore.path(), "", "path"); +} + +#[test] +fn eip2335_test_vector_pbkdf() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + let keystore = decode_and_check_sk(&vector); + assert_eq!( + *keystore.uuid(), + Uuid::parse_str("64625def-3331-4eea-ab6f-782f3ed16a83").unwrap(), + "uuid" + ); + assert_eq!(keystore.path(), "m/12381/60/0/0", "path"); +} diff --git a/eth2/utils/eth2_keystore/tests/json.rs b/eth2/utils/eth2_keystore/tests/json.rs new file mode 100644 index 00000000000..4484c425453 --- /dev/null +++ b/eth2/utils/eth2_keystore/tests/json.rs @@ -0,0 +1,1011 @@ +#![cfg(test)] + +use eth2_keystore::{Error, Keystore}; + +/// A valid keystore we can mutate to ensure our JSON encoding is strict. +/// +/// If this test doesn't pass then it all previous tests are unreliable. +#[test] +fn scrypt_reference() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert!(Keystore::from_json_str(&vector).is_ok()); +} + +#[test] +fn pbkdf2_reference() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + assert!(Keystore::from_json_str(&vector).is_ok()); +} + +#[test] +fn additional_top_level_key() { + let vector = r#" + { + "cats": 42, + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn additional_cipher_key() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "cats": 42, + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn additional_checksum_key() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "cats": 42, + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn additional_kdf_key() { + let vector = r#" + { + "crypto": { + "kdf": { + "cats": 42, + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn additional_crypto_key() { + let vector = r#" + { + "crypto": { + "cats": 42, + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn bad_version() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 5 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn json_bad_checksum() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cd" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert_eq!( + Keystore::from_json_str(&vector) + .unwrap() + .decrypt_keypair("testpassword".as_bytes()) + .err() + .unwrap(), + Error::InvalidPassword + ); +} + +#[test] +fn kdf_function() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "not-scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn missing_scrypt_param() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn additional_scrypt_param() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "cats": 42 + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn checksum_function() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "not-sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn checksum_params() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": { + "cats": "lol" + }, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn kdf_message() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "1" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn cipher_function() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "not-aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn additional_cipher_param() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6", + "cat": 42 + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn missing_cipher_param() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": {}, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn missing_pubkey() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn missing_path() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn missing_version() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "" + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn pbkdf2_bad_hmac() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "bad-hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn pbkdf2_additional_parameter() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", + "cats": 42 + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} + +#[test] +fn pbkdf2_missing_parameter() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + match Keystore::from_json_str(&vector) { + Err(Error::InvalidJson(_)) => {} + _ => panic!("expected invalid json error"), + } +} diff --git a/eth2/utils/eth2_keystore/tests/params.rs b/eth2/utils/eth2_keystore/tests/params.rs new file mode 100644 index 00000000000..71e37a1ed55 --- /dev/null +++ b/eth2/utils/eth2_keystore/tests/params.rs @@ -0,0 +1,322 @@ +#![cfg(test)] + +use eth2_keystore::{Error, Keystore}; + +const PASSWORD: &str = "testpassword"; + +fn decrypt_error(vector: &str) -> Error { + Keystore::from_json_str(&vector) + .unwrap() + .decrypt_keypair(PASSWORD.as_bytes()) + .err() + .unwrap() +} + +#[test] +fn scrypt_zero_n() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 0, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidScryptParam); +} + +#[test] +fn scrypt_dklen_not_32() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 33, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidScryptParam); +} + +#[test] +fn scrypt_zero_p() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 0, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidScryptParam); +} + +#[test] +fn scrypt_zero_r() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 0, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidScryptParam); +} + +#[test] +fn scrypt_zero_dklen() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 0, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "path": "", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidScryptParam); +} + +#[test] +fn pbkdf2_zero_c() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 0, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidPbkdf2Param); +} + +#[test] +fn pbkdf2_zero_dken() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 0, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidPbkdf2Param); +} + +#[test] +fn pbkdf2_dklen_not_32() { + let vector = r#" + { + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 33, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48" + } + }, + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 + } + "#; + + assert_eq!(decrypt_error(vector), Error::InvalidPbkdf2Param); +} diff --git a/eth2/utils/eth2_keystore/tests/tests.rs b/eth2/utils/eth2_keystore/tests/tests.rs new file mode 100644 index 00000000000..8c94a9f5e0b --- /dev/null +++ b/eth2/utils/eth2_keystore/tests/tests.rs @@ -0,0 +1,108 @@ +#![cfg(test)] + +use bls::Keypair; +use eth2_keystore::{Error, Keystore, KeystoreBuilder}; +use std::fs::OpenOptions; +use tempfile::tempdir; + +const GOOD_PASSWORD: &[u8] = &[42, 42, 42]; +const BAD_PASSWORD: &[u8] = &[43, 43, 43]; + +#[test] +fn empty_password() { + assert_eq!( + KeystoreBuilder::new(&Keypair::random(), "".as_bytes(), "".into()) + .err() + .unwrap(), + Error::EmptyPassword + ); +} + +#[test] +fn string_round_trip() { + let keypair = Keypair::random(); + + let keystore = KeystoreBuilder::new(&keypair, GOOD_PASSWORD, "".into()) + .unwrap() + .build() + .unwrap(); + + let json = keystore.to_json_string().unwrap(); + let decoded = Keystore::from_json_str(&json).unwrap(); + + assert_eq!( + decoded.decrypt_keypair(BAD_PASSWORD).err().unwrap(), + Error::InvalidPassword, + "should not decrypt with bad password" + ); + + assert_eq!( + decoded.decrypt_keypair(GOOD_PASSWORD).unwrap(), + keypair, + "should decrypt with good password" + ); +} + +#[test] +fn file() { + let keypair = Keypair::random(); + let dir = tempdir().unwrap(); + let path = dir.path().join("keystore.json"); + + let get_file = || { + OpenOptions::new() + .write(true) + .read(true) + .create(true) + .open(path.clone()) + .expect("should create file") + }; + + let keystore = KeystoreBuilder::new(&keypair, GOOD_PASSWORD, "".into()) + .unwrap() + .build() + .unwrap(); + + keystore + .to_json_writer(&mut get_file()) + .expect("should write to file"); + + let decoded = Keystore::from_json_reader(&mut get_file()).expect("should read from file"); + + assert_eq!( + decoded.decrypt_keypair(BAD_PASSWORD).err().unwrap(), + Error::InvalidPassword, + "should not decrypt with bad password" + ); + + assert_eq!( + decoded.decrypt_keypair(GOOD_PASSWORD).unwrap(), + keypair, + "should decrypt with good password" + ); +} + +#[test] +fn scrypt_params() { + let keypair = Keypair::random(); + + let keystore = KeystoreBuilder::new(&keypair, GOOD_PASSWORD, "".into()) + .unwrap() + .build() + .unwrap(); + + let json = keystore.to_json_string().unwrap(); + let decoded = Keystore::from_json_str(&json).unwrap(); + + assert_eq!( + decoded.decrypt_keypair(BAD_PASSWORD).err().unwrap(), + Error::InvalidPassword, + "should not decrypt with bad password" + ); + + assert_eq!( + decoded.decrypt_keypair(GOOD_PASSWORD).unwrap(), + keypair, + "should decrypt with good password" + ); +}