#![cfg_attr(docsrs, feature(doc_cfg))]
use aes::{
cipher::{self, InnerIvInit, KeyInit, StreamCipherCore},
Aes128,
};
use digest::{Digest, Update};
use hmac::Hmac;
use pbkdf2::pbkdf2;
use rand::{CryptoRng, Rng};
use scrypt::{scrypt, Params as ScryptParams};
use sha2::Sha256;
use sha3::Keccak256;
use uuid::Uuid;
use std::{
fs::File,
io::{Read, Write},
path::Path,
};
mod error;
mod keystore;
mod utils;
#[cfg(feature = "geth-compat")]
use utils::geth_compat::address_from_pk;
pub use error::KeystoreError;
pub use keystore::{CipherparamsJson, CryptoJson, EthKeystore, KdfType, KdfparamsType};
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_R: u32 = 8u32;
const DEFAULT_KDF_PARAMS_P: u32 = 1u32;
pub fn new<P, R, S>(
dir: P,
rng: &mut R,
password: S,
name: Option<&str>,
) -> Result<(Vec<u8>, String), KeystoreError>
where
P: AsRef<Path>,
R: Rng + CryptoRng,
S: AsRef<[u8]>,
{
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))
}
pub fn decrypt_key<P, S>(path: P, password: S) -> Result<Vec<u8>, KeystoreError>
where
P: AsRef<Path>,
S: AsRef<[u8]>,
{
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)?;
let key = match keystore.crypto.kdfparams {
KdfparamsType::Pbkdf2 {
c,
dklen,
prf: _,
salt,
} => {
let mut key = vec![0u8; dklen as usize];
pbkdf2::<Hmac<Sha256>>(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
}
};
let derived_mac = Keccak256::new()
.chain(&key[16..32])
.chain(&keystore.crypto.ciphertext)
.finalize();
if derived_mac.as_slice() != keystore.crypto.mac.as_slice() {
return Err(KeystoreError::MacMismatch);
}
let decryptor =
Aes128Ctr::new(&key[..16], &keystore.crypto.cipherparams.iv[..16]).expect("invalid length");
let mut pk = keystore.crypto.ciphertext;
decryptor.apply_keystream(&mut pk);
Ok(pk)
}
pub fn encrypt_key<P, R, B, S>(
dir: P,
rng: &mut R,
pk: B,
password: S,
name: Option<&str>,
) -> Result<String, KeystoreError>
where
P: AsRef<Path>,
R: Rng + CryptoRng,
B: AsRef<[u8]>,
S: AsRef<[u8]>,
{
let mut salt = vec![0u8; DEFAULT_KEY_SIZE];
rng.fill_bytes(salt.as_mut_slice());
let mut key = vec![0u8; DEFAULT_KDF_PARAMS_DKLEN as usize];
let scrypt_params = ScryptParams::new(
DEFAULT_KDF_PARAMS_LOG_N,
DEFAULT_KDF_PARAMS_R,
DEFAULT_KDF_PARAMS_P,
)?;
scrypt(password.as_ref(), &salt, &scrypt_params, key.as_mut_slice())?;
let mut iv = vec![0u8; DEFAULT_IV_SIZE];
rng.fill_bytes(iv.as_mut_slice());
let encryptor = Aes128Ctr::new(&key[..16], &iv[..16]).expect("invalid length");
let mut ciphertext = pk.as_ref().to_vec();
encryptor.apply_keystream(&mut ciphertext);
let mac = Keccak256::new()
.chain(&key[16..32])
.chain(&ciphertext)
.finalize();
let id = Uuid::new_v4();
let name = if let Some(name) = name {
name.to_string()
} else {
id.to_string()
};
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,
},
mac: mac.to_vec(),
},
#[cfg(feature = "geth-compat")]
address: address_from_pk(&pk)?,
};
let contents = serde_json::to_string(&keystore)?;
let mut file = File::create(dir.as_ref().join(&name))?;
file.write_all(contents.as_bytes())?;
Ok(id.to_string())
}
struct Aes128Ctr {
inner: ctr::CtrCore<Aes128, ctr::flavors::Ctr128BE>,
}
impl Aes128Ctr {
fn new(key: &[u8], iv: &[u8]) -> Result<Self, cipher::InvalidLength> {
let cipher = aes::Aes128::new_from_slice(key).unwrap();
let inner = ctr::CtrCore::inner_iv_slice_init(cipher, iv).unwrap();
Ok(Self { inner })
}
fn apply_keystream(self, buf: &mut [u8]) {
self.inner.apply_keystream_partial(buf.into());
}
}