eth_keystore/lib.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275
#![cfg_attr(docsrs, feature(doc_cfg))]
//! A minimalist library to interact with encrypted JSON keystores as per the
//! [Web3 Secret Storage Definition](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition).
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;
/// 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<dyn std::error::Error>> {
/// 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<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]>,
{
// 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.
///
/// # Example
///
/// ```no_run
/// use eth_keystore::decrypt_key;
/// use std::path::Path;
///
/// # async fn foobar() -> Result<(), Box<dyn std::error::Error>> {
/// let keypath = Path::new("./keys/my-key");
/// let private_key = decrypt_key(&keypath, "password_to_keystore")?;
/// # Ok(())
/// # }
/// ```
pub fn decrypt_key<P, S>(path: P, password: S) -> Result<Vec<u8>, KeystoreError>
where
P: AsRef<Path>,
S: AsRef<[u8]>,
{
// Read the file contents as string and deserialize it.
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.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
}
};
// Derive the MAC from the derived key and ciphertext.
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);
}
// Decrypt the private key bytes using AES-128-CTR
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)
}
/// 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.
///
/// # Example
///
/// ```no_run
/// use eth_keystore::encrypt_key;
/// use rand::RngCore;
/// use std::path::Path;
///
/// # async fn foobar() -> Result<(), Box<dyn std::error::Error>> {
/// let dir = Path::new("./keys");
/// let mut rng = rand::thread_rng();
///
/// // Construct a 32-byte random private key.
/// let mut private_key = vec![0u8; 32];
/// rng.fill_bytes(private_key.as_mut_slice());
///
/// // Since we specify a custom filename for the keystore, it will be stored in `$dir/my-key`
/// let name = encrypt_key(&dir, &mut rng, &private_key, "password_to_keystore", Some("my-key"))?;
/// # Ok(())
/// # }
/// ```
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]>,
{
// Generate a random salt.
let mut salt = vec![0u8; DEFAULT_KEY_SIZE];
rng.fill_bytes(salt.as_mut_slice());
// Derive the key.
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())?;
// Encrypt the private key using AES-128-CTR.
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);
// Calculate the MAC.
let mac = Keccak256::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 name = if let Some(name) = name {
name.to_string()
} else {
id.to_string()
};
// 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,
},
mac: mac.to_vec(),
},
#[cfg(feature = "geth-compat")]
address: address_from_pk(&pk)?,
};
let contents = serde_json::to_string(&keystore)?;
// Create a file in write-only mode, to store the encrypted JSON 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());
}
}