From 8a0e41379a7600ea04e5f2ee402bab7297ae0045 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 12 Apr 2023 11:21:03 -0700 Subject: [PATCH 1/4] Bump to version 4 following test vecs here: https://eips.ethereum.org/EIPS/eip-2335 --- src/keystore.rs | 303 +++++++++++++++++++++----------- src/lib.rs | 68 ++++--- tests/mod.rs | 15 +- tests/test-keys/key-pbkdf2.json | 46 +++-- tests/test-keys/key-scrypt.json | 50 ++++-- 5 files changed, 312 insertions(+), 170 deletions(-) diff --git a/src/keystore.rs b/src/keystore.rs index 4a77804..9fbbbeb 100644 --- a/src/keystore.rs +++ b/src/keystore.rs @@ -7,34 +7,31 @@ use ethereum_types::H160 as Address; #[derive(Debug, Deserialize, Serialize)] /// This struct represents the deserialized form of an encrypted JSON keystore based on the -/// [Web3 Secret Storage Definition](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition). +/// [eip-2335](https://eips.ethereum.org/EIPS/eip-2335). pub struct EthKeystore { - #[cfg(feature = "geth-compat")] - pub address: Address, - pub crypto: CryptoJson, - pub id: Uuid, + pub description: String, + pub pubkey: String, + pub path: String, + pub uuid: String, pub version: u8, } #[derive(Debug, Deserialize, Serialize)] /// Represents the "crypto" part of an encrypted JSON keystore. pub struct CryptoJson { - pub cipher: String, - pub cipherparams: CipherparamsJson, - #[serde(serialize_with = "buffer_to_hex", deserialize_with = "hex_to_buffer")] - pub ciphertext: Vec, - pub kdf: KdfType, - pub kdfparams: KdfparamsType, - #[serde(serialize_with = "buffer_to_hex", deserialize_with = "hex_to_buffer")] - pub mac: Vec, + pub kdf: Kdf, + pub checksum: Checksum, + pub cipher: Cipher, } #[derive(Debug, Deserialize, Serialize)] -/// Represents the "cipherparams" part of an encrypted JSON keystore. -pub struct CipherparamsJson { +/// Represents the "crypto" part of an encrypted JSON keystore. +pub struct Kdf { + pub function: KdfType, + pub params: KdfparamsType, #[serde(serialize_with = "buffer_to_hex", deserialize_with = "hex_to_buffer")] - pub iv: Vec, + pub message: Vec, } #[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -66,6 +63,39 @@ pub enum KdfparamsType { }, } +#[derive(Debug, Deserialize, Serialize)] +pub struct Checksum { + pub function: HashFunction, + pub params: ChecksumParams, + #[serde(serialize_with = "buffer_to_hex", deserialize_with = "hex_to_buffer")] + pub message: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ChecksumParams {} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Cipher { + pub function: String, + pub params: CipherparamsJson, + #[serde(serialize_with = "buffer_to_hex", deserialize_with = "hex_to_buffer")] + pub message: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +/// Represents the "cipherparams" part of an encrypted JSON keystore. +pub struct CipherparamsJson { + #[serde(serialize_with = "buffer_to_hex", deserialize_with = "hex_to_buffer")] + pub iv: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +/// Types of key derivition functions supported by the Web3 Secret Storage. +pub enum HashFunction { + Sha256, +} + fn buffer_to_hex(buffer: &T, serializer: S) -> Result where T: AsRef<[u8]>, @@ -87,155 +117,220 @@ where mod tests { use super::*; - #[cfg(feature = "geth-compat")] + #[cfg(not(feature = "geth-compat"))] #[test] - fn deserialize_geth_compat_keystore() { + fn test_deserialize_pbkdf2() { + // Test vec from: https://eips.ethereum.org/EIPS/eip-2335 let data = r#" { - "address": "00000398232e2064f896018496b4b44b3d62751f", "crypto": { - "cipher": "aes-128-ctr", - "ciphertext": "4f784cd629a7caf34b488e36fb96aad8a8f943a6ce31c7deab950c5e3a5b1c43", - "cipherparams": { - "iv": "76f07196b3c94f25b8f34d869493f640" + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" }, - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "n": 262144, - "p": 1, - "r": 8, - "salt": "1e7be4ce8351dd1710b0885438414b1748a81f1af510eda11e4d1f99c8d43975" + "checksum": { + "function": "sha256", + "params": {}, + "message": "8a9f5d9912ed7e75ea794bc5a89bca5f193721d30868ade6f73043c6ea6febf1" }, - "mac": "5b5433575a2418c1c813337a88b4099baa2f534e5dabeba86979d538c1f594d8" + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "cee03fde2af33149775b7223e7845e4fb2c8ae1792e5f99fe9ecf474cc8c16ad" + } }, - "id": "6c4485f3-3cc0-4081-848e-8bf489f2c262", - "version": 3 + "description": "This is a test keystore that uses PBKDF2 to secure the secret.", + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 }"#; let keystore: EthKeystore = serde_json::from_str(data).unwrap(); + + // Check outer level + assert_eq!(keystore.version, 4); assert_eq!( - keystore.address.as_bytes().to_vec(), - hex::decode("00000398232e2064f896018496b4b44b3d62751f").unwrap() + keystore.uuid, + Uuid::parse_str("64625def-3331-4eea-ab6f-782f3ed16a83") + .unwrap() + .to_string() ); - } - - #[cfg(not(feature = "geth-compat"))] - #[test] - fn test_deserialize_pbkdf2() { - let data = r#" - { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" - }, - "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", - "kdf" : "pbkdf2", - "kdfparams" : { - "c" : 262144, - "dklen" : 32, - "prf" : "hmac-sha256", - "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" - }, - "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - }"#; - let keystore: EthKeystore = serde_json::from_str(data).unwrap(); - assert_eq!(keystore.version, 3); assert_eq!( - keystore.id, - Uuid::parse_str("3198bc9c-6672-5ab3-d995-4942343ae5b6").unwrap() + keystore.description, + "This is a test keystore that uses PBKDF2 to secure the secret.".to_string() ); - assert_eq!(keystore.crypto.cipher, "aes-128-ctr"); assert_eq!( - keystore.crypto.cipherparams.iv, - Vec::from_hex("6087dab2f9fdbbfaddc31a909735c1e6").unwrap() + keystore.pubkey, + "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07".to_string() ); + assert_eq!(keystore.path, "m/12381/60/0/0".to_string()); + + // Check Cipher + assert_eq!(keystore.crypto.cipher.function, "aes-128-ctr"); assert_eq!( - keystore.crypto.ciphertext, - Vec::from_hex("5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46") + keystore.crypto.cipher.params.iv, + Vec::from_hex("264daa3f303d7259501c93d997d84fe6").unwrap() + ); + assert_eq!( + keystore.crypto.cipher.message, + Vec::from_hex("cee03fde2af33149775b7223e7845e4fb2c8ae1792e5f99fe9ecf474cc8c16ad") .unwrap() ); - assert_eq!(keystore.crypto.kdf, KdfType::Pbkdf2); + + // Check KDF + assert_eq!(keystore.crypto.kdf.function, KdfType::Pbkdf2); assert_eq!( - keystore.crypto.kdfparams, + keystore.crypto.kdf.params, KdfparamsType::Pbkdf2 { c: 262144, dklen: 32, prf: String::from("hmac-sha256"), salt: Vec::from_hex( - "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" + "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" ) .unwrap(), } ); + assert_eq!(keystore.crypto.kdf.message, Vec::from_hex("").unwrap()); + + // Test Checksum assert_eq!( - keystore.crypto.mac, - Vec::from_hex("517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2") + keystore.crypto.checksum.message, + Vec::from_hex("8a9f5d9912ed7e75ea794bc5a89bca5f193721d30868ade6f73043c6ea6febf1") .unwrap() ); + + assert_eq!(keystore.crypto.checksum.function, HashFunction::Sha256); + } + + #[test] + fn test_deserialize_kdf() { + let data = r#" + { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }"#; + let _kdf: Kdf = serde_json::from_str(data).unwrap(); + } + + #[test] + fn test_deserialize_checksum() { + let data = r#" + { + "function": "sha256", + "params": {}, + "message": "d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484" + }"#; + let _kdf: Checksum = serde_json::from_str(data).unwrap(); } #[cfg(not(feature = "geth-compat"))] #[test] fn test_deserialize_scrypt() { + // Test vec from: https://eips.ethereum.org/EIPS/eip-2335 let data = r#" { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "p" : 8, - "r" : 1, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + "checksum": { + "function": "sha256", + "params": {}, + "message": "d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484" }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "06ae90d55fe0a6e9c5c3bc5b170827b2e5cce3929ed3f116c2811e6366dfe20f" + } }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 + "description": "This is a test keystore that uses scrypt to secure the secret.", + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/3141592653/589793238", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "version": 4 }"#; let keystore: EthKeystore = serde_json::from_str(data).unwrap(); - assert_eq!(keystore.version, 3); + + // Check outer level + assert_eq!(keystore.version, 4); assert_eq!( - keystore.id, - Uuid::parse_str("3198bc9c-6672-5ab3-d995-4942343ae5b6").unwrap() + keystore.uuid, + Uuid::parse_str("1d85ae20-35c5-4611-98e8-aa14a633906f") + .unwrap() + .to_string() ); - assert_eq!(keystore.crypto.cipher, "aes-128-ctr"); assert_eq!( - keystore.crypto.cipherparams.iv, - Vec::from_hex("83dbcc02d8ccb40e466191a123791e0e").unwrap() + keystore.description, + "This is a test keystore that uses scrypt to secure the secret.".to_string() ); assert_eq!( - keystore.crypto.ciphertext, - Vec::from_hex("d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c") + keystore.pubkey, + "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07".to_string() + ); + assert_eq!(keystore.path, "m/12381/60/3141592653/589793238".to_string()); + + // Check Cipher + assert_eq!(keystore.crypto.cipher.function, "aes-128-ctr"); + assert_eq!( + keystore.crypto.cipher.params.iv, + Vec::from_hex("264daa3f303d7259501c93d997d84fe6").unwrap() + ); + assert_eq!( + keystore.crypto.cipher.message, + Vec::from_hex("06ae90d55fe0a6e9c5c3bc5b170827b2e5cce3929ed3f116c2811e6366dfe20f") .unwrap() ); - assert_eq!(keystore.crypto.kdf, KdfType::Scrypt); + // Check KDF + assert_eq!(keystore.crypto.kdf.function, KdfType::Scrypt); assert_eq!( - keystore.crypto.kdfparams, + keystore.crypto.kdf.params, KdfparamsType::Scrypt { dklen: 32, n: 262144, - p: 8, - r: 1, + p: 1, + r: 8, salt: Vec::from_hex( - "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" ) .unwrap(), } ); + assert_eq!(keystore.crypto.kdf.message, Vec::from_hex("").unwrap()); + + // Test Checksum assert_eq!( - keystore.crypto.mac, - Vec::from_hex("2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097") + keystore.crypto.checksum.message, + Vec::from_hex("d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484") .unwrap() ); + + assert_eq!(keystore.crypto.checksum.function, HashFunction::Sha256); } } diff --git a/src/lib.rs b/src/lib.rs index be8d59d..63cbda9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use aes::{ }; use digest::{Digest, Update}; use hmac::Hmac; +use keystore::{Checksum, ChecksumParams, Cipher, HashFunction, Kdf}; use pbkdf2::pbkdf2; use rand::{CryptoRng, Rng}; use scrypt::{scrypt, Params as ScryptParams}; @@ -109,7 +110,7 @@ where let keystore: EthKeystore = serde_json::from_str(&contents)?; // Derive the key. - let key = match keystore.crypto.kdfparams { + let key = match keystore.crypto.kdf.params { KdfparamsType::Pbkdf2 { c, dklen, @@ -136,20 +137,20 @@ where }; // Derive the MAC from the derived key and ciphertext. - let derived_mac = Keccak256::new() + let derived_mac = Sha256::new() .chain(&key[16..32]) - .chain(&keystore.crypto.ciphertext) + .chain(&keystore.crypto.cipher.message) .finalize(); - if derived_mac.as_slice() != keystore.crypto.mac.as_slice() { + if derived_mac.as_slice() != keystore.crypto.checksum.message.as_slice() { return Err(KeystoreError::MacMismatch); } // Decrypt the private key bytes using AES-128-CTR - let decryptor = - Aes128Ctr::new(&key[..16], &keystore.crypto.cipherparams.iv[..16]).expect("invalid length"); + let decryptor = Aes128Ctr::new(&key[..16], &keystore.crypto.cipher.params.iv[..16]) + .expect("invalid length"); - let mut pk = keystore.crypto.ciphertext; + let mut pk = keystore.crypto.cipher.message; decryptor.apply_keystream(&mut pk); Ok(pk) @@ -215,39 +216,54 @@ where encryptor.apply_keystream(&mut ciphertext); // Calculate the MAC. - let mac = Keccak256::new() + let mac = Sha256::new() .chain(&key[16..32]) .chain(&ciphertext) .finalize(); // If a file name is not specified for the keystore, simply use the strigified uuid. - let id = Uuid::new_v4(); + let uuid = Uuid::new_v4(); let name = if let Some(name) = name { name.to_string() } else { - id.to_string() + uuid.to_string() }; + let version = 4; + let pubkey = String::from("123123"); + let path = String::from("path"); + let description = String::from("asdf"); + // Construct and serialize the encrypted JSON keystore. let keystore = EthKeystore { - id, - version: 3, crypto: CryptoJson { - cipher: String::from(DEFAULT_CIPHER), - cipherparams: CipherparamsJson { iv }, - ciphertext: ciphertext.to_vec(), - kdf: KdfType::Scrypt, - kdfparams: KdfparamsType::Scrypt { - dklen: DEFAULT_KDF_PARAMS_DKLEN, - n: 2u32.pow(DEFAULT_KDF_PARAMS_LOG_N as u32), - p: DEFAULT_KDF_PARAMS_P, - r: DEFAULT_KDF_PARAMS_R, - salt, + kdf: Kdf { + function: KdfType::Scrypt, + params: KdfparamsType::Scrypt { + dklen: DEFAULT_KDF_PARAMS_DKLEN, + n: 2u32.pow(DEFAULT_KDF_PARAMS_LOG_N as u32), + p: DEFAULT_KDF_PARAMS_P, + r: DEFAULT_KDF_PARAMS_R, + salt, + }, + message: vec![], + }, + checksum: Checksum { + function: HashFunction::Sha256, + params: ChecksumParams {}, + message: mac.to_vec(), + }, + cipher: Cipher { + function: String::from(DEFAULT_CIPHER), + params: CipherparamsJson { iv }, + message: ciphertext.to_vec(), }, - mac: mac.to_vec(), }, - #[cfg(feature = "geth-compat")] - address: address_from_pk(&pk)?, + description, + pubkey, + path, + uuid: name.clone(), + version, }; let contents = serde_json::to_string(&keystore)?; @@ -255,7 +271,7 @@ where let mut file = File::create(dir.as_ref().join(&name))?; file.write_all(contents.as_bytes())?; - Ok(id.to_string()) + Ok(uuid.to_string()) } struct Aes128Ctr { diff --git a/tests/mod.rs b/tests/mod.rs index 35ea51d..cb40594 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -40,22 +40,29 @@ mod tests { #[cfg(not(feature = "geth-compat"))] #[test] fn test_decrypt_pbkdf2() { + // Test vec from: https://eips.ethereum.org/EIPS/eip-2335 let secret = - Vec::from_hex("7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d") + Vec::from_hex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") .unwrap(); + + let encoded_pw = hex::decode("7465737470617373776f7264f09f9491").unwrap(); + let pw = String::from_utf8(encoded_pw).unwrap(); let keypath = Path::new("./tests/test-keys/key-pbkdf2.json"); - assert_eq!(decrypt_key(&keypath, "testpassword").unwrap(), secret); + assert_eq!(decrypt_key(&keypath, pw).unwrap(), secret); assert!(decrypt_key(&keypath, "wrongtestpassword").is_err()); } #[cfg(not(feature = "geth-compat"))] #[test] fn test_decrypt_scrypt() { + // Test vec from: https://eips.ethereum.org/EIPS/eip-2335 let secret = - Vec::from_hex("80d3a6ed7b24dcd652949bc2f3827d2f883b3722e3120b15a93a2e0790f03829") + Vec::from_hex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") .unwrap(); + let encoded_pw = hex::decode("7465737470617373776f7264f09f9491").unwrap(); + let pw = String::from_utf8(encoded_pw).unwrap(); let keypath = Path::new("./tests/test-keys/key-scrypt.json"); - assert_eq!(decrypt_key(&keypath, "grOQ8QDnGHvpYJf").unwrap(), secret); + assert_eq!(decrypt_key(&keypath, pw).unwrap(), secret); assert!(decrypt_key(&keypath, "thisisnotrandom").is_err()); } diff --git a/tests/test-keys/key-pbkdf2.json b/tests/test-keys/key-pbkdf2.json index 84cf8cc..53aa140 100644 --- a/tests/test-keys/key-pbkdf2.json +++ b/tests/test-keys/key-pbkdf2.json @@ -1,19 +1,31 @@ { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" - }, - "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", - "kdf" : "pbkdf2", - "kdfparams" : { - "c" : 262144, - "dklen" : 32, - "prf" : "hmac-sha256", - "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" - }, - "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" + "crypto": { + "kdf": { + "function": "pbkdf2", + "params": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "8a9f5d9912ed7e75ea794bc5a89bca5f193721d30868ade6f73043c6ea6febf1" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "cee03fde2af33149775b7223e7845e4fb2c8ae1792e5f99fe9ecf474cc8c16ad" + } }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 -} + "description": "This is a test keystore that uses PBKDF2 to secure the secret.", + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/0/0", + "uuid": "64625def-3331-4eea-ab6f-782f3ed16a83", + "version": 4 +} \ No newline at end of file diff --git a/tests/test-keys/key-scrypt.json b/tests/test-keys/key-scrypt.json index d86ed34..a3523a5 100644 --- a/tests/test-keys/key-scrypt.json +++ b/tests/test-keys/key-scrypt.json @@ -1,20 +1,32 @@ { - "version": 3, - "id": "23fb6c51-79c8-4ce5-a02a-0f4f169a4168", - "crypto": { - "ciphertext": "eb7302cc8669683e49fc40f3445464020568a1d82e71c977264f286a3099be58", - "cipherparams": { - "iv": "2a9fc5d709ab79e7f015111b93872f6c" - }, - "cipher": "aes-128-ctr", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "salt": "19a1b0af7a3f4f04a4e8d1cf722550e905da18b369029b0460b12765f19d8a99", - "n": 8192, - "r": 8, - "p": 1 - }, - "mac": "b2987a6adb597af26705042977c55ea132ca1391220455769eba2bc0b239089b" - } -} + "crypto": { + "kdf": { + "function": "scrypt", + "params": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" + }, + "message": "" + }, + "checksum": { + "function": "sha256", + "params": {}, + "message": "d2217fe5f3e9a1e34581ef8a78f7c9928e436d36dacc5e846690a5581e8ea484" + }, + "cipher": { + "function": "aes-128-ctr", + "params": { + "iv": "264daa3f303d7259501c93d997d84fe6" + }, + "message": "06ae90d55fe0a6e9c5c3bc5b170827b2e5cce3929ed3f116c2811e6366dfe20f" + } + }, + "description": "This is a test keystore that uses scrypt to secure the secret.", + "pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07", + "path": "m/12381/60/3141592653/589793238", + "uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f", + "version": 4 +} \ No newline at end of file From 02ad4e9b3f16246f0a946e23a7e026c74509e8e5 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Wed, 12 Apr 2023 14:00:39 -0700 Subject: [PATCH 2/4] Add decrypt_keystore function to decrypt a JSON encoded string rather than a file --- src/lib.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 63cbda9..8310275 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -156,6 +156,60 @@ where Ok(pk) } +pub fn decrypt_keystore(keystore_s: &String, password: S) -> Result, KeystoreError> +where + S: AsRef<[u8]>, +{ + // Deserialize keystore string + let keystore: EthKeystore = serde_json::from_str(&keystore_s)?; + + // Derive the key. + let key = match keystore.crypto.kdf.params { + KdfparamsType::Pbkdf2 { + c, + dklen, + prf: _, + salt, + } => { + let mut key = vec![0u8; dklen as usize]; + pbkdf2::>(password.as_ref(), &salt, c, key.as_mut_slice()); + key + } + KdfparamsType::Scrypt { + dklen, + n, + p, + r, + salt, + } => { + let mut key = vec![0u8; dklen as usize]; + let log_n = (n as f32).log2() as u8; + let scrypt_params = ScryptParams::new(log_n, r, p)?; + scrypt(password.as_ref(), &salt, &scrypt_params, key.as_mut_slice())?; + key + } + }; + + // Derive the MAC from the derived key and ciphertext. + let derived_mac = Sha256::new() + .chain(&key[16..32]) + .chain(&keystore.crypto.cipher.message) + .finalize(); + + if derived_mac.as_slice() != keystore.crypto.checksum.message.as_slice() { + return Err(KeystoreError::MacMismatch); + } + + // Decrypt the private key bytes using AES-128-CTR + let decryptor = Aes128Ctr::new(&key[..16], &keystore.crypto.cipher.params.iv[..16]) + .expect("invalid length"); + + let mut pk = keystore.crypto.cipher.message; + decryptor.apply_keystream(&mut pk); + + Ok(pk) +} + /// Encrypts the given private key using the [Scrypt](https://tools.ietf.org/html/rfc7914.html) /// password-based key derivation function, and stores it in the provided directory. On success, it /// returns the `id` (Uuid) generated for this keystore. From 59c066298e262b3d41283e327eb497c10f96aa59 Mon Sep 17 00:00:00 2001 From: Jason Vranek Date: Fri, 14 Apr 2023 13:12:20 -0700 Subject: [PATCH 3/4] encrypt_key() now derives the bls public key and includes as part of the v4 keystore --- Cargo.lock | 143 +++++++++++++++++++++++++++++++++++++++++---------- Cargo.toml | 2 +- src/error.rs | 3 ++ src/lib.rs | 105 ++++--------------------------------- tests/mod.rs | 36 +------------ 5 files changed, 134 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d46f62..8bbafb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blst" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a30d0edd9dd1c60ddb42b80341c7852f6f985279a5c1a83659dcb65899dec99" +dependencies = [ + "cc", + "glob", + "threadpool", + "which", + "zeroize", +] + [[package]] name = "byte-slice-cast" version = "1.2.0" @@ -70,6 +83,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -171,6 +190,12 @@ dependencies = [ "signature", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "elliptic-curve" version = "0.12.3" @@ -196,6 +221,7 @@ name = "eth-keystore" version = "0.5.0" dependencies = [ "aes", + "blst", "ctr", "digest", "ethereum-types", @@ -208,7 +234,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "sha3", "thiserror", "uuid", ] @@ -289,6 +314,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.12.0" @@ -300,6 +331,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "hex" version = "0.4.3" @@ -350,7 +390,7 @@ checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.86", ] [[package]] @@ -381,16 +421,26 @@ dependencies = [ ] [[package]] -name = "keccak" -version = "0.1.0" +name = "libc" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] -name = "libc" -version = "0.2.117" +name = "num_cpus" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "parity-scale-codec" @@ -415,7 +465,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", + "syn 1.0.86", ] [[package]] @@ -468,18 +518,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -615,7 +665,7 @@ checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.86", ] [[package]] @@ -640,16 +690,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha3" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f935e31cf406e8c0e96c2815a5516181b7004ae8c5f296293221e9b1e356bd" -dependencies = [ - "digest", - "keccak", -] - [[package]] name = "signature" version = "1.6.0" @@ -693,6 +733,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "tap" version = "1.0.1" @@ -716,7 +767,16 @@ checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.86", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", ] [[package]] @@ -755,6 +815,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + [[package]] name = "unicode-xid" version = "0.2.2" @@ -783,6 +849,17 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "wyz" version = "0.5.0" @@ -797,3 +874,17 @@ name = "zeroize" version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c88870063c39ee00ec285a2f8d6a966e5b6fb2becc4e8dac77ed0d370ed6006" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] diff --git a/Cargo.toml b/Cargo.toml index 56e5272..9d09771 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,9 @@ scrypt = { version = "0.10.0", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10.1" -sha3 = "0.10.0" thiserror = { version = "1.0.22", default-features = false } uuid = { version = "0.8", features = ["serde", "v4"] } +blst = "0.3.10" # feature = "geth-compat" ethereum-types = { version = "0.13.1", optional = true } diff --git a/src/error.rs b/src/error.rs index 664dfd3..7b846e9 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,9 @@ use thiserror::Error; #[derive(Error, Debug)] /// An error thrown when interacting with the eth-keystore crate. pub enum KeystoreError { + // An error when the input BLS private key is not parsed correctly + #[error("blst {0:?}")] + BLSError(blst::BLST_ERROR), /// An error thrown while decrypting an encrypted JSON keystore if the calculated MAC does not /// match the MAC declared in the keystore. #[error("Mac Mismatch")] diff --git a/src/lib.rs b/src/lib.rs index 8310275..16cb038 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,7 +13,6 @@ use pbkdf2::pbkdf2; use rand::{CryptoRng, Rng}; use scrypt::{scrypt, Params as ScryptParams}; use sha2::Sha256; -use sha3::Keccak256; use uuid::Uuid; use std::{ @@ -36,52 +35,10 @@ const DEFAULT_CIPHER: &str = "aes-128-ctr"; const DEFAULT_KEY_SIZE: usize = 32usize; const DEFAULT_IV_SIZE: usize = 16usize; const DEFAULT_KDF_PARAMS_DKLEN: u8 = 32u8; -const DEFAULT_KDF_PARAMS_LOG_N: u8 = 13u8; +const DEFAULT_KDF_PARAMS_LOG_N: u8 = 18u8; const DEFAULT_KDF_PARAMS_R: u32 = 8u32; const DEFAULT_KDF_PARAMS_P: u32 = 1u32; -/// Creates a new JSON keystore using the [Scrypt](https://tools.ietf.org/html/rfc7914.html) -/// key derivation function. The keystore is encrypted by a key derived from the provided `password` -/// and stored in the provided directory with either the user-provided filename, or a generated -/// Uuid `id`. -/// -/// # Example -/// -/// ```no_run -/// use eth_keystore::new; -/// use std::path::Path; -/// -/// # async fn foobar() -> Result<(), Box> { -/// let dir = Path::new("./keys"); -/// let mut rng = rand::thread_rng(); -/// // here `None` signifies we don't specify a filename for the keystore. -/// // the default filename is a generated Uuid for the keystore. -/// let (private_key, name) = new(&dir, &mut rng, "password_to_keystore", None)?; -/// -/// // here `Some("my_key")` denotes a custom filename passed by the caller. -/// let (private_key, name) = new(&dir, &mut rng, "password_to_keystore", Some("my_key"))?; -/// # Ok(()) -/// # } -/// ``` -pub fn new( - dir: P, - rng: &mut R, - password: S, - name: Option<&str>, -) -> Result<(Vec, String), KeystoreError> -where - P: AsRef, - R: Rng + CryptoRng, - S: AsRef<[u8]>, -{ - // Generate a random private key. - let mut pk = vec![0u8; DEFAULT_KEY_SIZE]; - rng.fill_bytes(pk.as_mut_slice()); - - let name = encrypt_key(dir, rng, &pk, password, name)?; - Ok((pk, name)) -} - /// Decrypts an encrypted JSON keystore at the provided `path` using the provided `password`. /// Decryption supports the [Scrypt](https://tools.ietf.org/html/rfc7914.html) and /// [PBKDF2](https://ietf.org/rfc/rfc2898.txt) key derivation functions. @@ -107,53 +64,7 @@ where let mut file = File::open(path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - let keystore: EthKeystore = serde_json::from_str(&contents)?; - - // Derive the key. - let key = match keystore.crypto.kdf.params { - KdfparamsType::Pbkdf2 { - c, - dklen, - prf: _, - salt, - } => { - let mut key = vec![0u8; dklen as usize]; - pbkdf2::>(password.as_ref(), &salt, c, key.as_mut_slice()); - key - } - KdfparamsType::Scrypt { - dklen, - n, - p, - r, - salt, - } => { - let mut key = vec![0u8; dklen as usize]; - let log_n = (n as f32).log2() as u8; - let scrypt_params = ScryptParams::new(log_n, r, p)?; - scrypt(password.as_ref(), &salt, &scrypt_params, key.as_mut_slice())?; - key - } - }; - - // Derive the MAC from the derived key and ciphertext. - let derived_mac = Sha256::new() - .chain(&key[16..32]) - .chain(&keystore.crypto.cipher.message) - .finalize(); - - if derived_mac.as_slice() != keystore.crypto.checksum.message.as_slice() { - return Err(KeystoreError::MacMismatch); - } - - // Decrypt the private key bytes using AES-128-CTR - let decryptor = Aes128Ctr::new(&key[..16], &keystore.crypto.cipher.params.iv[..16]) - .expect("invalid length"); - - let mut pk = keystore.crypto.cipher.message; - decryptor.apply_keystream(&mut pk); - - Ok(pk) + decrypt_keystore(&contents, password) } pub fn decrypt_keystore(keystore_s: &String, password: S) -> Result, KeystoreError> @@ -247,6 +158,13 @@ where B: AsRef<[u8]>, S: AsRef<[u8]>, { + let bls_sk = match blst::min_pk::SecretKey::from_bytes(pk.as_ref()) { + Ok(sk) => sk, + Err(e) => return Err(KeystoreError::BLSError(e)), + }; + + let bls_pk = bls_sk.sk_to_pk().compress(); + let pubkey = hex::encode(&bls_pk); // Generate a random salt. let mut salt = vec![0u8; DEFAULT_KEY_SIZE]; rng.fill_bytes(salt.as_mut_slice()); @@ -284,9 +202,8 @@ where }; let version = 4; - let pubkey = String::from("123123"); - let path = String::from("path"); - let description = String::from("asdf"); + let path = String::from(""); // Path is not currently derived + let description = String::from("Version 4 BLS keystore"); // Construct and serialize the encrypted JSON keystore. let keystore = EthKeystore { diff --git a/tests/mod.rs b/tests/mod.rs index cb40594..f264e56 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -1,42 +1,10 @@ -use eth_keystore::{decrypt_key, encrypt_key, new}; +use eth_keystore::{decrypt_key, encrypt_key}; use hex::FromHex; use std::path::Path; mod tests { use super::*; - #[test] - fn test_new() { - let dir = Path::new("./tests/test-keys"); - let mut rng = rand::thread_rng(); - let (secret, id) = new(&dir, &mut rng, "thebestrandompassword", None).unwrap(); - - let keypath = dir.join(&id); - - assert_eq!( - decrypt_key(&keypath, "thebestrandompassword").unwrap(), - secret - ); - assert!(decrypt_key(&keypath, "notthebestrandompassword").is_err()); - assert!(std::fs::remove_file(&keypath).is_ok()); - } - - #[test] - fn test_new_with_name() { - let dir = Path::new("./tests/test-keys"); - let mut rng = rand::thread_rng(); - let name = "my_keystore"; - let (secret, _id) = new(&dir, &mut rng, "thebestrandompassword", Some(name)).unwrap(); - - let keypath = dir.join(&name); - - assert_eq!( - decrypt_key(&keypath, "thebestrandompassword").unwrap(), - secret - ); - assert!(std::fs::remove_file(&keypath).is_ok()); - } - #[cfg(not(feature = "geth-compat"))] #[test] fn test_decrypt_pbkdf2() { @@ -69,7 +37,7 @@ mod tests { #[test] fn test_encrypt_decrypt_key() { let secret = - Vec::from_hex("7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d") + Vec::from_hex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") .unwrap(); let dir = Path::new("./tests/test-keys"); let mut rng = rand::thread_rng(); From 67ece1a43931367f858cd8e573027e80f24de863 Mon Sep 17 00:00:00 2001 From: JasonVranek Date: Wed, 8 Nov 2023 20:30:51 -0800 Subject: [PATCH 4/4] Fix UUID and add the default path as described by EIP-2334. Fixes importing keystores to Teku validator client. --- src/lib.rs | 10 ++++++---- tests/mod.rs | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 16cb038..f36a0f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -194,7 +194,7 @@ where .finalize(); // If a file name is not specified for the keystore, simply use the strigified uuid. - let uuid = Uuid::new_v4(); + let uuid = Uuid::new_v4().to_string(); let name = if let Some(name) = name { name.to_string() } else { @@ -202,7 +202,9 @@ where }; let version = 4; - let path = String::from(""); // Path is not currently derived + + // https://eips.ethereum.org/EIPS/eip-2334 + let path = String::from("m/12381/3600/0/0/0"); let description = String::from("Version 4 BLS keystore"); // Construct and serialize the encrypted JSON keystore. @@ -233,7 +235,7 @@ where description, pubkey, path, - uuid: name.clone(), + uuid, version, }; let contents = serde_json::to_string(&keystore)?; @@ -242,7 +244,7 @@ where let mut file = File::create(dir.as_ref().join(&name))?; file.write_all(contents.as_bytes())?; - Ok(uuid.to_string()) + Ok(name) } struct Aes128Ctr { diff --git a/tests/mod.rs b/tests/mod.rs index f264e56..6909f6e 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -37,7 +37,7 @@ mod tests { #[test] fn test_encrypt_decrypt_key() { let secret = - Vec::from_hex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f") + Vec::from_hex("4c627588f8040116b75f14fdb55b552612a46a2cd91e65b516defe39d81fc08f") .unwrap(); let dir = Path::new("./tests/test-keys"); let mut rng = rand::thread_rng();