#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use aes::cipher::{KeyIvInit, StreamCipher};
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 thiserror::Error;
use uuid::Uuid;
#[derive(Error, Debug)]
pub enum KeyStoreError {
#[error("Mac Mismatch")]
MacMismatch,
#[error("scrypt {0:?}")]
ScryptInvalidParams(scrypt::errors::InvalidParams),
#[error("scrypt {0:?}")]
ScryptInvalidOuputLen(scrypt::errors::InvalidOutputLen),
#[error("aes {0:?}")]
AesInvalidKeyNonceLength(aes::cipher::InvalidLength),
}
impl From<scrypt::errors::InvalidParams> for KeyStoreError {
fn from(e: scrypt::errors::InvalidParams) -> Self {
Self::ScryptInvalidParams(e)
}
}
impl From<scrypt::errors::InvalidOutputLen> for KeyStoreError {
fn from(e: scrypt::errors::InvalidOutputLen) -> Self {
Self::ScryptInvalidOuputLen(e)
}
}
impl From<aes::cipher::InvalidLength> for KeyStoreError {
fn from(e: aes::cipher::InvalidLength) -> Self {
Self::AesInvalidKeyNonceLength(e)
}
}
mod keystore;
use keystore::{CipherParams, CryptoData, KdfParamsType, KdfType};
pub use keystore::KeyStore;
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
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_random<R, S>(
rng: &mut R,
password: S,
) -> Result<(KeyStore, Vec<u8>), KeyStoreError>
where
R: Rng + CryptoRng,
S: AsRef<[u8]>,
{
let pk: [u8; DEFAULT_KEY_SIZE] = rng.gen();
Ok((encrypt(rng, &pk, password, None, None)?, pk.to_vec()))
}
pub fn decrypt<S>(
keystore: &KeyStore,
password: S,
) -> Result<Vec<u8>, KeyStoreError>
where
S: AsRef<[u8]>,
{
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().ceil() 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 mut decryptor = Aes128Ctr::new(
(&key[..16]).into(),
(&keystore.crypto.cipherparams.iv[..16]).into(),
);
let mut pk = keystore.crypto.ciphertext.clone();
decryptor.apply_keystream(&mut pk);
Ok(pk)
}
pub fn encrypt<R, B, S>(
rng: &mut R,
pk: B,
password: S,
address: Option<String>,
label: Option<String>,
) -> Result<KeyStore, KeyStoreError>
where
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 mut encryptor = Aes128Ctr::new((&key[..16]).into(), (&iv[..16]).into());
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 keystore = KeyStore {
id,
address,
label,
version: 3,
crypto: CryptoData {
cipher: String::from(DEFAULT_CIPHER),
cipherparams: CipherParams { 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(),
},
};
Ok(keystore)
}