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:
Paul Hauner 2020-05-11 17:45:06 +10:00 committed by GitHub
parent 294d007f64
commit f30271ee9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 2500 additions and 5 deletions

71
Cargo.lock generated
View File

@ -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",
]

View File

@ -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",

View 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"

View 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
}
}

View File

@ -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
}
}

View 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,
}
}
}

View 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");
}
}

View 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,
}

View 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,
}

View 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()
}

View 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;

View 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)
}
}

View 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");
}

File diff suppressed because it is too large Load Diff

View 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);
}

View 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"
);
}