use crate::keys::bls::{BlsKeypair, BlsSecretKey};
use aes::cipher::{KeyIvInit, StreamCipher};
use pbkdf2::pbkdf2_hmac;
use scrypt::{scrypt, Params as ScryptParams};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::Path;
use thiserror::Error;
use uuid::Uuid;
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Keystore {
pub crypto: KeystoreCrypto,
#[serde(default)]
pub description: Option<String>,
pub pubkey: String,
pub path: String,
pub uuid: Uuid,
pub version: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeystoreCrypto {
pub kdf: KeystoreKdf,
pub checksum: KeystoreChecksum,
pub cipher: KeystoreCipher,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeystoreKdf {
pub function: String,
pub params: KdfParams,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum KdfParams {
Scrypt(ScryptKdfParams),
Pbkdf2(Pbkdf2KdfParams),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScryptKdfParams {
pub dklen: u32,
pub n: u32,
pub p: u32,
pub r: u32,
pub salt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pbkdf2KdfParams {
pub dklen: u32,
pub c: u32,
pub prf: String,
pub salt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeystoreChecksum {
pub function: String,
pub params: serde_json::Value,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KeystoreCipher {
pub function: String,
pub params: CipherParams,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CipherParams {
pub iv: String,
}
impl Keystore {
pub fn load(path: impl AsRef<Path>) -> Result<Self, KeystoreError> {
let contents = fs::read_to_string(path.as_ref())
.map_err(|e| KeystoreError::Io(e.to_string()))?;
serde_json::from_str(&contents)
.map_err(|e| KeystoreError::Parse(e.to_string()))
}
pub fn decrypt(&self, password: &str) -> Result<BlsSecretKey, KeystoreError> {
let decryption_key = self.derive_key(password)?;
self.verify_checksum(&decryption_key)?;
let secret_key_bytes = self.decrypt_secret(&decryption_key)?;
BlsSecretKey::from_bytes(&secret_key_bytes)
.map_err(|e| KeystoreError::InvalidKey(e.to_string()))
}
pub fn decrypt_keypair(&self, password: &str) -> Result<BlsKeypair, KeystoreError> {
let secret = self.decrypt(password)?;
Ok(BlsKeypair::from_secret(secret))
}
fn derive_key(&self, password: &str) -> Result<[u8; 32], KeystoreError> {
let kdf = &self.crypto.kdf;
match kdf.function.as_str() {
"scrypt" => {
let params = match &kdf.params {
KdfParams::Scrypt(p) => p,
_ => return Err(KeystoreError::InvalidKdf("Expected scrypt params".into())),
};
let salt = hex::decode(¶ms.salt)
.map_err(|e| KeystoreError::Parse(format!("Invalid salt hex: {}", e)))?;
let log_n = (params.n as f64).log2() as u8;
let scrypt_params = ScryptParams::new(log_n, params.r, params.p, params.dklen as usize)
.map_err(|e| KeystoreError::InvalidKdf(format!("Invalid scrypt params: {:?}", e)))?;
let mut dk = [0u8; 32];
scrypt(password.as_bytes(), &salt, &scrypt_params, &mut dk)
.map_err(|e| KeystoreError::InvalidKdf(format!("Scrypt failed: {:?}", e)))?;
Ok(dk)
}
"pbkdf2" => {
let params = match &kdf.params {
KdfParams::Pbkdf2(p) => p,
_ => return Err(KeystoreError::InvalidKdf("Expected pbkdf2 params".into())),
};
let salt = hex::decode(¶ms.salt)
.map_err(|e| KeystoreError::Parse(format!("Invalid salt hex: {}", e)))?;
let mut dk = [0u8; 32];
pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, params.c, &mut dk);
Ok(dk)
}
other => Err(KeystoreError::UnsupportedKdf(other.to_string())),
}
}
fn verify_checksum(&self, decryption_key: &[u8; 32]) -> Result<(), KeystoreError> {
let cipher_message = hex::decode(&self.crypto.cipher.message)
.map_err(|e| KeystoreError::Parse(format!("Invalid cipher message hex: {}", e)))?;
let mut hasher = Sha256::new();
hasher.update(&decryption_key[16..32]);
hasher.update(&cipher_message);
let computed_checksum = hasher.finalize();
let expected_checksum = hex::decode(&self.crypto.checksum.message)
.map_err(|e| KeystoreError::Parse(format!("Invalid checksum hex: {}", e)))?;
if computed_checksum.as_slice() != expected_checksum.as_slice() {
return Err(KeystoreError::InvalidPassword);
}
Ok(())
}
fn decrypt_secret(&self, decryption_key: &[u8; 32]) -> Result<[u8; 32], KeystoreError> {
let cipher = &self.crypto.cipher;
if cipher.function != "aes-128-ctr" {
return Err(KeystoreError::UnsupportedCipher(cipher.function.clone()));
}
let iv = hex::decode(&cipher.params.iv)
.map_err(|e| KeystoreError::Parse(format!("Invalid IV hex: {}", e)))?;
let ciphertext = hex::decode(&cipher.message)
.map_err(|e| KeystoreError::Parse(format!("Invalid ciphertext hex: {}", e)))?;
let aes_key: [u8; 16] = decryption_key[0..16].try_into().unwrap();
let iv_array: [u8; 16] = iv.try_into()
.map_err(|_| KeystoreError::Parse("Invalid IV length".into()))?;
let mut cipher = Aes128Ctr::new(&aes_key.into(), &iv_array.into());
let mut plaintext = ciphertext;
cipher.apply_keystream(&mut plaintext);
if plaintext.len() != 32 {
return Err(KeystoreError::InvalidKey(format!(
"Expected 32 bytes, got {}",
plaintext.len()
)));
}
let mut secret = [0u8; 32];
secret.copy_from_slice(&plaintext);
Ok(secret)
}
pub fn public_key_bytes(&self) -> Result<[u8; 48], KeystoreError> {
let pubkey = self.pubkey.strip_prefix("0x").unwrap_or(&self.pubkey);
let bytes = hex::decode(pubkey)
.map_err(|e| KeystoreError::Parse(format!("Invalid pubkey hex: {}", e)))?;
if bytes.len() != 48 {
return Err(KeystoreError::Parse(format!(
"Expected 48 bytes for pubkey, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 48];
arr.copy_from_slice(&bytes);
Ok(arr)
}
}
pub fn load_keystores_from_dir(
dir: impl AsRef<Path>,
password: &str,
) -> Result<Vec<BlsKeypair>, KeystoreError> {
let dir = dir.as_ref();
if !dir.exists() {
return Err(KeystoreError::Io(format!(
"Directory does not exist: {}",
dir.display()
)));
}
let mut keypairs = Vec::new();
for entry in fs::read_dir(dir).map_err(|e| KeystoreError::Io(e.to_string()))? {
let entry = entry.map_err(|e| KeystoreError::Io(e.to_string()))?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
match Keystore::load(&path) {
Ok(keystore) => {
match keystore.decrypt_keypair(password) {
Ok(keypair) => {
tracing::info!(
pubkey = %keystore.pubkey,
path = %path.display(),
"Loaded validator key"
);
keypairs.push(keypair);
}
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"Failed to decrypt keystore"
);
}
}
}
Err(e) => {
tracing::debug!(
path = %path.display(),
error = %e,
"Failed to load keystore"
);
}
}
}
}
Ok(keypairs)
}
#[derive(Debug, Error)]
pub enum KeystoreError {
#[error("I/O error: {0}")]
Io(String),
#[error("Parse error: {0}")]
Parse(String),
#[error("Invalid password")]
InvalidPassword,
#[error("Unsupported KDF: {0}")]
UnsupportedKdf(String),
#[error("Invalid KDF parameters: {0}")]
InvalidKdf(String),
#[error("Unsupported cipher: {0}")]
UnsupportedCipher(String),
#[error("Invalid key: {0}")]
InvalidKey(String),
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
const TEST_KEYSTORE_SCRYPT: &str = r#"{
"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": "Test keystore",
"pubkey": "0x9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
"path": "m/12381/3600/0/0/0",
"uuid": "1d85ae20-35c5-4611-8e4c-00a8c6c5fb55",
"version": 4
}"#;
#[test]
fn test_parse_keystore() {
let keystore: Keystore = serde_json::from_str(TEST_KEYSTORE_SCRYPT).unwrap();
assert_eq!(keystore.version, 4);
assert!(keystore.pubkey.starts_with("0x"));
}
#[test]
fn test_load_keystore_from_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("keystore.json");
let mut file = fs::File::create(&path).unwrap();
file.write_all(TEST_KEYSTORE_SCRYPT.as_bytes()).unwrap();
let keystore = Keystore::load(&path).unwrap();
assert_eq!(keystore.version, 4);
}
#[test]
fn test_public_key_bytes() {
let keystore: Keystore = serde_json::from_str(TEST_KEYSTORE_SCRYPT).unwrap();
let pubkey = keystore.public_key_bytes().unwrap();
assert_eq!(pubkey.len(), 48);
}
}