emerald-vault-core 0.26.6

Ethereum secure account management core libary
Documentation
/*
Copyright 2019 ETCDEV GmbH

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
extern crate emerald_rs as emerald;
extern crate hex;
extern crate serde_json;
extern crate tempdir;
extern crate uuid;

use crate::emerald::keystore::{
    Cipher, CoreCrypto, CryptoType, HdwalletCrypto, Iv, Kdf, KdfDepthLevel, KeyFile, Mac, Prf,
    Salt, CIPHER_IV_BYTES, KDF_SALT_BYTES,
};
use crate::emerald::storage::{DbStorage, FsStorage, KeyfileStorage};
use crate::emerald::{Address, KECCAK256_BYTES};
use hex::FromHex;

use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tempdir::TempDir;
use uuid::Uuid;

const PRJ_DIR: Option<&'static str> = option_env!("CARGO_MANIFEST_DIR");

macro_rules! arr {
    ($bytes:expr, $num:expr) => {{
        let mut arr = [0u8; $num];
        arr.copy_from_slice($bytes);
        arr
    }};
}

pub fn temp_dir() -> PathBuf {
    let dir = TempDir::new("emerald").unwrap();
    File::create(dir.path()).ok();
    dir.into_path()
}

pub fn file_content<P: AsRef<Path>>(path: P) -> String {
    let mut text = String::new();

    File::open(path)
        .expect("Expect read file content")
        .read_to_string(&mut text)
        .ok();

    text
}

pub fn keyfile_path(name: &str) -> PathBuf {
    let mut path = keystore_path();
    path.push(name);
    path
}

pub fn keystore_path() -> PathBuf {
    let mut buf = PathBuf::from(PRJ_DIR.expect("Expect project directory"));
    buf.push("tests/keystore/serialize");
    buf
}

#[test]
fn should_decrypt_private_key_protected_by_scrypt() {
    let path =
        keyfile_path("UTC--2017-03-17T10-52-08.229Z--0047201aed0b69875b24b614dda0270bcd9f11cc");

    let keyfile = KeyFile::decode(&file_content(path)).unwrap();

    assert!(keyfile.decrypt_key("_").is_err());
    assert_eq!(
        keyfile.decrypt_key("1234567890").unwrap().to_string(),
        "0xfa384e6fe915747cd13faa1022044b0def5e6bec4238bec53166487a5cca569f"
    );
}

#[test]
fn should_decrypt_private_key_protected_by_pbkdf2() {
    let path = keyfile_path("UTC--2017-03-20T17-03-12Z--37e0d14f-7269-7ca0-4419-d7b13abfeea9");

    let keyfile = KeyFile::decode(&file_content(path)).unwrap();

    assert!(keyfile.decrypt_key("_").is_err());
    assert_eq!(
        keyfile.decrypt_key("1234567890").unwrap().to_string(),
        "0x00b413b37c71bfb92719d16e28d7329dea5befa0d0b8190742f89e55617991cf"
    );
}

#[test]
fn should_decode_keyfile_without_address() {
    let path = keyfile_path("UTC--2017-03-20T17-03-12Z--37e0d14f-7269-7ca0-4419-d7b13abfeea9");

    let mut crypto = CoreCrypto::default();
    crypto.kdf_params.dklen = 32;
    crypto.kdf_params.kdf = Kdf::Pbkdf2 {
        prf: Prf::default(),
        c: 10240,
    };
    crypto.kdf_params.salt = Salt::from(arr!(
        &Vec::from_hex("095a4028fa2474bb2191f9fc1d876c79a9ff76ed029aa7150d37da785a00175b",)
            .unwrap(),
        KDF_SALT_BYTES
    ));
    crypto.cipher = Cipher::default();
    crypto.cipher_text =
        Vec::from_hex("9c9e3ebbf01a512f3bea41ac6fe7676344c0da77236b38847c02718ec9b66126").unwrap();

    crypto.cipher_params.iv = Iv::from(arr!(
        &Vec::from_hex("58d54158c3e27131b0a0f2b91201aedc").unwrap(),
        CIPHER_IV_BYTES
    ));

    crypto.mac = Mac::from(arr!(
        &Vec::from_hex("83c175d2ef1229ab10eb6726500a4303ab729e6e44dfaac274fe75c870b23a63",)
            .unwrap(),
        KECCAK256_BYTES
    ));

    let exp = KeyFile {
        visible: None,
        name: Some("".to_string()),
        description: None,
        address: Address::from_str("0x4c4cfc6470a1dc26916585ef03dfec42deb936ff").unwrap(),
        uuid: Uuid::from_str("37e0d14f-7269-7ca0-4419-d7b13abfeea9").unwrap(),
        crypto: CryptoType::Core(crypto),
    };

    // just first encoding
    let key = KeyFile::decode(&file_content(path)).unwrap();

    // verify encoding & decoding full cycle logic
    let key = KeyFile::decode(&serde_json::to_string(&key).unwrap()).unwrap();

    if let CryptoType::Core(ref exp_core) = exp.crypto {
        if let CryptoType::Core(ref recv_core) = key.crypto {
            assert_eq!(key, exp);
            assert_eq!(key.visible, exp.visible);
            assert_eq!(recv_core.kdf_params, exp_core.kdf_params);
            assert_eq!(recv_core.cipher_text, exp_core.cipher_text);
            assert_eq!(recv_core.cipher_params.iv, exp_core.cipher_params.iv);
            assert_eq!(recv_core.mac, exp_core.mac);
        } else {
            assert!(false, "Invalid Crypto type")
        }
    }
}

#[test]
fn should_decode_keyfile_with_address() {
    let path =
        keyfile_path("UTC--2017-03-17T10-52-08.229Z--0047201aed0b69875b24b614dda0270bcd9f11cc");

    let mut crypto = CoreCrypto::default();
    crypto.kdf_params.kdf = Kdf::Scrypt {
        n: 1024,
        r: 8,
        p: 1,
    };
    crypto.kdf_params.dklen = 32;
    crypto.kdf_params.salt = Salt::from(arr!(
        &Vec::from_hex("fd4acb81182a2c8fa959d180967b374277f2ccf2f7f401cb08d042cc785464b4",)
            .unwrap(),
        KDF_SALT_BYTES
    ));
    crypto.cipher = Cipher::default();
    crypto.cipher_text =
        Vec::from_hex("c3dfc95ca91dce73fe8fc4ddbaed33bad522e04a6aa1af62bba2a0bb90092fa1").unwrap();

    crypto.cipher_params.iv = Iv::from(arr!(
        &Vec::from_hex("9df1649dd1c50f2153917e3b9e7164e9").unwrap(),
        CIPHER_IV_BYTES
    ));

    crypto.mac = Mac::from(arr!(
        &Vec::from_hex("9f8a85347fd1a81f14b99f69e2b401d68fb48904efe6a66b357d8d1d61ab14e5",)
            .unwrap(),
        KECCAK256_BYTES
    ));

    let exp = KeyFile {
        visible: None,
        name: None,
        description: None,
        address: Address::from_str("0x0047201aed0b69875b24b614dda0270bcd9f11cc").unwrap(),
        uuid: Uuid::from_str("f7ab2bfa-e336-4f45-a31f-beb3dd0689f3").unwrap(),
        crypto: CryptoType::Core(crypto),
    };

    // just first encoding
    let key = KeyFile::decode(&file_content(path)).unwrap();

    // verify encoding & decoding full cycle logic
    let key = KeyFile::decode(&serde_json::to_string(&key).unwrap()).unwrap();

    if let CryptoType::Core(ref exp_core) = exp.crypto {
        if let CryptoType::Core(ref recv_core) = key.crypto {
            assert_eq!(key, exp);
            assert_eq!(key.visible, exp.visible);
            assert_eq!(recv_core.kdf_params, exp_core.kdf_params);
            assert_eq!(recv_core.cipher_text, exp_core.cipher_text);
            assert_eq!(recv_core.cipher_params.iv, exp_core.cipher_params.iv);
            assert_eq!(recv_core.mac, exp_core.mac);
        } else {
            assert!(false, "Invalid Crypto type")
        }
    } else {
        assert!(false, "Invalid Crypto type")
    }
}

#[test]
fn should_decode_hd_wallet_keyfile() {
    let path = keyfile_path("UTC--2017-05-30T06-16-46Z--a928d7c2-b37b-464c-a70b-b9979d59fac5");

    let mut crypto = HdwalletCrypto::default();
    crypto.cipher = "hardware".to_string();
    crypto.hardware = "ledger-nano-s:v1".to_string();
    crypto.hd_path = "44'/61'/0'/0/0".to_string();

    let exp = KeyFile {
        visible: None,
        name: None,
        description: None,
        address: Address::from_str("01234567890abcdef1234567890abcdef1234567").unwrap(),
        uuid: Uuid::from_str("a928d7c2-b37b-464c-a70b-b9979d59fac5").unwrap(),
        crypto: CryptoType::HdWallet(crypto),
    };

    // just first encoding
    let key = KeyFile::decode(&file_content(path)).unwrap();

    // verify encoding & decoding full cycle logic
    let key = KeyFile::decode(&serde_json::to_string(&key).unwrap()).unwrap();

    if let CryptoType::HdWallet(ref exp_hd) = exp.crypto {
        if let CryptoType::HdWallet(ref recv_hd) = key.crypto {
            assert_eq!(key, exp);
            assert_eq!(key.visible, exp.visible);
            assert_eq!(recv_hd.cipher, exp_hd.cipher);
            assert_eq!(recv_hd.hardware, exp_hd.hardware);
            assert_eq!(recv_hd.hd_path, exp_hd.hd_path);
        } else {
            assert!(false, "Invalid Crypto type")
        }
    }
}

#[test]
//TODO:1 remove condition after fix for `scrypt` on Windows
#[cfg(not(target_os = "windows"))]
fn should_use_security_level() {
    let sec = KdfDepthLevel::Normal;
    let kf = KeyFile::new("1234567890", &sec, None, None).unwrap();
    if let CryptoType::Core(ref core) = kf.crypto {
        assert_eq!(core.kdf_params.kdf, Kdf::from(sec));
    } else {
        assert!(false, "Invalid Crypto type")
    }

    let sec = KdfDepthLevel::High;
    let kf = KeyFile::new("1234567890", &sec, Some("s".to_string()), None).unwrap();
    if let CryptoType::Core(ref core) = kf.crypto {
        assert_eq!(core.kdf_params.kdf, Kdf::from(sec));
    } else {
        assert!(false, "Invalid Crypto type")
    }
}

#[test]
fn should_flush_to_file() {
    let kf = KeyFile::new("1234567890", &KdfDepthLevel::Normal, None, None).unwrap();

    let storage = FsStorage::new(&temp_dir().as_path());

    assert!(storage.put(&kf).is_ok());
}

#[test]
fn should_search_by_address_filesystem() {
    let addr = "0xc0de379b51d582e1600c76dd1efee8ed024b844a"
        .parse::<Address>()
        .unwrap();

    let storage = FsStorage::new(&keystore_path());
    let (_, kf) = storage.search_by_address(&addr).unwrap();

    assert_eq!(
        kf.uuid,
        "a928d7c2-b37b-464c-a70b-b9979d59fac4".parse().unwrap()
    );
}

#[test]
fn should_search_by_address_db() {
    let addr = "0xc0de379b51d582e1600c76dd1efee8ed024b844a"
        .parse::<Address>()
        .unwrap();

    let path = keyfile_path("UTC--2017-05-30T06-16-46Z--a928d7c2-b37b-464c-a70b-b9979d59fac4");
    let key = KeyFile::decode(&file_content(path)).unwrap();

    let storage = DbStorage::new(temp_dir().as_path()).unwrap();
    storage.put(&key).unwrap();

    let (_, kf) = storage.search_by_address(&addr).unwrap();

    assert_eq!(
        kf.uuid,
        "a928d7c2-b37b-464c-a70b-b9979d59fac4".parse().unwrap()
    );
}

#[test]
fn should_update_existing_addresses() {
    let path = keyfile_path("UTC--2017-05-30T06-16-46Z--a928d7c2-b37b-464c-a70b-b9979d59fac4");
    let mut key = KeyFile::decode(&file_content(path)).unwrap();

    let storage = DbStorage::new(temp_dir().as_path()).unwrap();
    assert!(key.name.is_none());
    storage.put(&key).unwrap();

    let updated_name = Some("updated name".to_string());
    key.name = updated_name.clone();
    assert!(storage.put(&key).is_ok());

    let (_, kf) = storage.search_by_address(&key.address).unwrap();
    assert_eq!(kf.name, updated_name)
}