EIP-2335: Keystore (#1071)
* 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 * Split out encrypt/decrypt * Replace some password usage with slice * Expose PlainText struct * Expose consts, remove Password * Expose SALT_SIZE * Move dbg assert statement * Fix dodgy json test * Protect against n == 1 * Return error if n is not power of 2 * Add dklen checks * Add note about panics Co-authored-by: pawan <pawandhananjay@gmail.com>
This commit is contained in:
parent
294d007f64
commit
f30271ee9e
71
Cargo.lock
generated
71
Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
@ -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",
|
||||
|
22
eth2/utils/eth2_keystore/Cargo.toml
Normal file
22
eth2/utils/eth2_keystore/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "eth2_keystore"
|
||||
version = "0.1.0"
|
||||
authors = ["Pawan Dhananjay <pawan@sigmaprime.io", "Paul Hauner <paul@paulhauner.com>"]
|
||||
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"
|
24
eth2/utils/eth2_keystore/src/derived_key.rs
Normal file
24
eth2/utils/eth2_keystore/src/derived_key.rs
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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<String> for ChecksumFunction {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
ChecksumFunction::Sha256 => "sha256".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for ChecksumFunction {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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<Value> for EmptyMap {
|
||||
fn into(self) -> Value {
|
||||
Value::Object(Map::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Value> for EmptyMap {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(v: Value) -> Result<Self, Self::Error> {
|
||||
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
|
||||
}
|
||||
}
|
64
eth2/utils/eth2_keystore/src/json_keystore/cipher_module.rs
Normal file
64
eth2/utils/eth2_keystore/src/json_keystore/cipher_module.rs
Normal file
@ -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<String> for CipherFunction {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
CipherFunction::Aes128Ctr => "aes-128-ctr".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for CipherFunction {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
72
eth2/utils/eth2_keystore/src/json_keystore/hex_bytes.rs
Normal file
72
eth2/utils/eth2_keystore/src/json_keystore/hex_bytes.rs
Normal file
@ -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<u8>);
|
||||
|
||||
impl HexBytes {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for HexBytes {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<String> for HexBytes {
|
||||
fn into(self) -> String {
|
||||
hex::encode(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for HexBytes {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
// 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<u8> {
|
||||
serde_json::from_str::<HexBytes>(&format!("\"{}\"", json))
|
||||
.expect("should decode json")
|
||||
.as_bytes()
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_hex_bytes() {
|
||||
let empty: Vec<u8> = 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");
|
||||
}
|
||||
}
|
128
eth2/utils/eth2_keystore/src/json_keystore/kdf_module.rs
Normal file
128
eth2/utils/eth2_keystore/src/json_keystore/kdf_module.rs
Normal file
@ -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<String> for EmptyString {
|
||||
fn into(self) -> String {
|
||||
"".into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for EmptyString {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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<String> for KdfFunction {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
KdfFunction::Scrypt => "scrypt".into(),
|
||||
KdfFunction::Pbkdf2 => "pbkdf2".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for KdfFunction {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
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,
|
||||
}
|
52
eth2/utils/eth2_keystore/src/json_keystore/mod.rs
Normal file
52
eth2/utils/eth2_keystore/src/json_keystore/mod.rs
Normal file
@ -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,
|
||||
}
|
399
eth2/utils/eth2_keystore/src/keystore.rs
Normal file
399
eth2/utils/eth2_keystore/src/keystore.rs
Normal file
@ -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<Self, Error> {
|
||||
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, Error> {
|
||||
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<Self, Error> {
|
||||
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<Keypair, Error> {
|
||||
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<String, Error> {
|
||||
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<Self, Error> {
|
||||
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<W: Write>(&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<R: Read>(reader: R) -> Result<Self, Error> {
|
||||
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<u8>) -> 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>, [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<PlainText, Error> {
|
||||
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<DerivedKey, Error> {
|
||||
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()
|
||||
}
|
15
eth2/utils/eth2_keystore/src/lib.rs
Normal file
15
eth2/utils/eth2_keystore/src/lib.rs
Normal file
@ -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;
|
34
eth2/utils/eth2_keystore/src/plain_text.rs
Normal file
34
eth2/utils/eth2_keystore/src/plain_text.rs
Normal file
@ -0,0 +1,34 @@
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Provides wrapper around `Vec<u8>` that implements `Zeroize`.
|
||||
#[derive(Zeroize, Clone, PartialEq)]
|
||||
#[zeroize(drop)]
|
||||
pub struct PlainText(Vec<u8>);
|
||||
|
||||
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<Vec<u8>> for PlainText {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self(vec)
|
||||
}
|
||||
}
|
107
eth2/utils/eth2_keystore/tests/eip2335_vectors.rs
Normal file
107
eth2/utils/eth2_keystore/tests/eip2335_vectors.rs
Normal file
@ -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");
|
||||
}
|
1011
eth2/utils/eth2_keystore/tests/json.rs
Normal file
1011
eth2/utils/eth2_keystore/tests/json.rs
Normal file
File diff suppressed because it is too large
Load Diff
322
eth2/utils/eth2_keystore/tests/params.rs
Normal file
322
eth2/utils/eth2_keystore/tests/params.rs
Normal file
@ -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);
|
||||
}
|
108
eth2/utils/eth2_keystore/tests/tests.rs
Normal file
108
eth2/utils/eth2_keystore/tests/tests.rs
Normal file
@ -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"
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user