use crate::base58;
use crate::error::{Error, Result};
use crate::keys::PrivateKey;
use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use zeroize::Zeroizing;
const SCRYPT_LOG_N: u8 = 14; const SCRYPT_R: u32 = 8;
const SCRYPT_P: u32 = 8;
const PREFIX: [u8; 2] = [0x01, 0x42];
const FLAG_BYTE: u8 = 0xc0;
const PAYLOAD_LEN: usize = 2 + 1 + 4 + 16 + 16;
fn bitcoin_address(key: &PrivateKey) -> String {
let uncompressed = key.public_key().to_uncompressed_bytes();
let sha = Sha256::digest(uncompressed);
let ripe = <ripemd::Ripemd160 as Digest>::digest(sha);
let mut payload = Vec::with_capacity(21);
payload.push(0x00);
payload.extend_from_slice(&ripe);
base58::encode_check(&payload)
}
fn stretch(passphrase: &str, salt: &[u8; 4]) -> Result<Zeroizing<[u8; 64]>> {
let params = scrypt::Params::new(SCRYPT_LOG_N, SCRYPT_R, SCRYPT_P, 64)
.map_err(|e| Error::key(format!("bad scrypt parameters: {e}")))?;
let mut out = Zeroizing::new([0u8; 64]);
scrypt::scrypt(passphrase.as_bytes(), salt, ¶ms, &mut *out)
.map_err(|e| Error::key(format!("scrypt failed: {e}")))?;
Ok(out)
}
fn salt_for(key: &PrivateKey) -> [u8; 4] {
let address = bitcoin_address(key);
let first = Sha256::digest(address.as_bytes());
let second = Sha256::digest(first);
[second[0], second[1], second[2], second[3]]
}
pub fn encrypt(key: &PrivateKey, passphrase: &str) -> Result<Zeroizing<String>> {
if passphrase.is_empty() {
return Err(Error::key("BIP-38 passphrase is empty"));
}
let salt = salt_for(key);
let stretched = stretch(passphrase, &salt)?;
let (half1, half2) = stretched.split_at(32);
let secret = key.expose_secret();
let cipher = aes::Aes256::new_from_slice(half2)
.map_err(|e| Error::key(format!("AES init failed: {e}")))?;
let mut encrypted = Zeroizing::new([0u8; 32]);
for block in 0..2 {
let mut buf = Zeroizing::new([0u8; 16]);
for i in 0..16 {
buf[i] = secret[block * 16 + i] ^ half1[block * 16 + i];
}
cipher.encrypt_block((&mut *buf).into());
encrypted[block * 16..(block + 1) * 16].copy_from_slice(&*buf);
}
let mut payload = Zeroizing::new(Vec::with_capacity(PAYLOAD_LEN));
payload.extend_from_slice(&PREFIX);
payload.push(FLAG_BYTE);
payload.extend_from_slice(&salt);
payload.extend_from_slice(&*encrypted);
Ok(Zeroizing::new(base58::encode_check(&payload)))
}
pub fn decrypt(encrypted_key: &str, passphrase: &str) -> Result<PrivateKey> {
let payload = Zeroizing::new(base58::decode_check(encrypted_key.trim())?);
if payload.len() != PAYLOAD_LEN {
return Err(Error::key(format!(
"BIP-38 payload must be {PAYLOAD_LEN} bytes, got {}",
payload.len()
)));
}
if payload[0..2] != PREFIX {
return Err(Error::key(format!(
"not a BIP-38 key: prefix is 0x{:02x}{:02x}, expected 0x0142",
payload[0], payload[1]
)));
}
if payload[2] != FLAG_BYTE {
return Err(Error::key(format!(
"unsupported BIP-38 flag byte 0x{:02x}; Graphene keys use 0x{FLAG_BYTE:02x}",
payload[2]
)));
}
let mut salt = [0u8; 4];
salt.copy_from_slice(&payload[3..7]);
let stretched = stretch(passphrase, &salt)?;
let (half1, half2) = stretched.split_at(32);
let cipher = aes::Aes256::new_from_slice(half2)
.map_err(|e| Error::key(format!("AES init failed: {e}")))?;
let mut secret = Zeroizing::new([0u8; 32]);
for block in 0..2 {
let mut buf = Zeroizing::new([0u8; 16]);
buf.copy_from_slice(&payload[7 + block * 16..7 + (block + 1) * 16]);
cipher.decrypt_block((&mut *buf).into());
for i in 0..16 {
secret[block * 16 + i] = buf[i] ^ half1[block * 16 + i];
}
}
let key = PrivateKey::from_bytes(&*secret)?;
if salt_for(&key).ct_eq(&salt).unwrap_u8() != 1 {
return Err(Error::key("BIP-38 salt mismatch: wrong passphrase"));
}
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
const TEST_WIF: &str = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3";
fn key() -> PrivateKey {
PrivateKey::from_wif(TEST_WIF).unwrap()
}
#[test]
fn round_trips() {
let encrypted = encrypt(&key(), "correct horse battery staple").unwrap();
assert!(encrypted.starts_with("6P"), "got {}", *encrypted);
let back = decrypt(&encrypted, "correct horse battery staple").unwrap();
assert_eq!(back, key());
}
#[test]
fn matches_beems_output_byte_for_byte() {
let encrypted = encrypt(&key(), "correct horse battery staple").unwrap();
assert_eq!(
&*encrypted,
"6PRWaUZmruY6rjNSJZ8G9yzdeU72VZmLgxMjADM7wuDaYknZCjot2JNmAc"
);
assert_eq!(
decrypt(
"6PRWaUZmruY6rjNSJZ8G9yzdeU72VZmLgxMjADM7wuDaYknZCjot2JNmAc",
"correct horse battery staple"
)
.unwrap(),
key()
);
}
#[test]
fn the_wrong_passphrase_is_refused() {
let encrypted = encrypt(&key(), "right").unwrap();
let err = decrypt(&encrypted, "wrong").unwrap_err();
assert!(format!("{err}").contains("wrong passphrase"));
}
#[test]
fn a_mistyped_key_fails_the_checksum() {
let encrypted = encrypt(&key(), "pass").unwrap();
let mut chars: Vec<char> = encrypted.chars().collect();
let i = chars.len() - 5;
chars[i] = if chars[i] == 'a' { 'b' } else { 'a' };
let broken: String = chars.into_iter().collect();
assert!(matches!(decrypt(&broken, "pass"), Err(Error::Checksum(_))));
}
#[test]
fn a_foreign_prefix_is_refused() {
let mut payload = vec![0x02, 0x43, FLAG_BYTE];
payload.extend_from_slice(&[0u8; 4]);
payload.extend_from_slice(&[0u8; 32]);
let encoded = base58::encode_check(&payload);
let err = decrypt(&encoded, "pass").unwrap_err();
assert!(format!("{err}").contains("prefix"));
}
#[test]
fn an_unsupported_flag_byte_is_refused() {
let mut payload = vec![PREFIX[0], PREFIX[1], 0xe0];
payload.extend_from_slice(&[0u8; 4]);
payload.extend_from_slice(&[0u8; 32]);
let encoded = base58::encode_check(&payload);
let err = decrypt(&encoded, "pass").unwrap_err();
assert!(format!("{err}").contains("flag byte"));
}
#[test]
fn a_short_payload_is_refused_before_slicing() {
let encoded = base58::encode_check(&[PREFIX[0], PREFIX[1], FLAG_BYTE, 0, 0]);
assert!(decrypt(&encoded, "pass").is_err());
}
#[test]
fn an_empty_passphrase_is_refused() {
assert!(encrypt(&key(), "").is_err());
}
#[test]
fn encryption_is_deterministic_for_a_given_key_and_passphrase() {
let a = encrypt(&key(), "same").unwrap();
let b = encrypt(&key(), "same").unwrap();
assert_eq!(&*a, &*b);
}
#[test]
fn distinct_keys_encrypt_differently() {
let other = PrivateKey::generate();
assert_ne!(
&*encrypt(&key(), "p").unwrap(),
&*encrypt(&other, "p").unwrap()
);
}
#[test]
fn the_encrypted_form_does_not_contain_the_key() {
let encrypted = encrypt(&key(), "passphrase").unwrap();
assert!(!encrypted.contains(TEST_WIF));
}
}