mod address;
#[macro_use]
mod byte_array;
mod crypto;
mod error;
use self::address::try_extract_address;
pub use self::crypto::{CoreCrypto, Iv, Mac, Salt, decode_str};
use self::error::Error;
use super::{CIPHER_IV_BYTES, Cipher, CryptoType, KDF_SALT_BYTES, Kdf, KeyFile};
use super::HdwalletCrypto;
use super::core::{self, Address};
use super::util;
use rustc_serialize::{Encodable, Encoder, json};
use std::fs::{self, File, read_dir};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub const CURRENT_VERSION: u8 = 3;
pub const SUPPORTED_VERSIONS: &'static [u8] = &[CURRENT_VERSION];
#[derive(Clone, Debug, RustcDecodable, RustcEncodable)]
pub struct SerializableKeyFileCore {
version: u8,
id: Uuid,
address: Address,
name: Option<String>,
description: Option<String>,
visible: Option<bool>,
crypto: CoreCrypto,
}
impl SerializableKeyFileCore {
fn try_from(kf: KeyFile) -> Result<Self, Error> {
let cr = CoreCrypto::try_from(kf.clone())?;
Ok(SerializableKeyFileCore {
version: CURRENT_VERSION,
id: kf.uuid,
address: kf.address,
name: kf.name.clone(),
description: kf.description.clone(),
visible: kf.visible,
crypto: cr,
})
}
}
impl Into<KeyFile> for SerializableKeyFileCore {
fn into(self) -> KeyFile {
KeyFile {
name: self.name,
description: self.description,
address: self.address,
visible: self.visible,
uuid: self.id,
crypto: CryptoType::Core(self.crypto),
}
}
}
#[derive(Clone, Debug, RustcDecodable, RustcEncodable)]
pub struct SerializableKeyFileHD {
version: u8,
id: Uuid,
address: Address,
name: Option<String>,
description: Option<String>,
visible: Option<bool>,
crypto: HdwalletCrypto,
}
impl SerializableKeyFileHD {
fn try_from(kf: KeyFile) -> Result<Self, Error> {
let cr = HdwalletCrypto::try_from(kf.clone())?;
Ok(SerializableKeyFileHD {
version: CURRENT_VERSION,
id: kf.uuid,
address: kf.address,
name: kf.name.clone(),
description: kf.description.clone(),
visible: kf.visible,
crypto: cr,
})
}
}
impl Into<KeyFile> for SerializableKeyFileHD {
fn into(self) -> KeyFile {
KeyFile {
name: self.name,
description: self.description,
address: self.address,
visible: self.visible,
uuid: self.id,
crypto: CryptoType::HdWallet(self.crypto),
}
}
}
impl KeyFile {
pub fn decode(f: String) -> Result<KeyFile, Error> {
let buf1 = f.clone();
let buf2 = f.clone();
let mut ver = 0;
let kf = json::decode::<SerializableKeyFileCore>(&buf1)
.and_then(|core| {
ver = core.version;
Ok(core.into())
})
.or_else(|_| {
json::decode::<SerializableKeyFileHD>(&buf2).and_then(|hd| {
ver = hd.version;
Ok(hd.into())
})
})
.map_err(|e| Error::from(e))?;
if !SUPPORTED_VERSIONS.contains(&ver) {
return Err(Error::UnsupportedVersion(ver));
}
Ok(kf)
}
pub fn flush<P: AsRef<Path>>(&self, dir: P, filename: Option<&str>) -> Result<(), Error> {
let tmp;
let name = match filename {
Some(n) => n,
None => {
tmp = generate_filename(&self.uuid.to_string());
&tmp
}
};
let path = dir.as_ref().join(name);
Ok(write(self, path)?)
}
pub fn search_by_address<P: AsRef<Path>>(
addr: &Address,
path: P,
) -> Result<(PathBuf, KeyFile), Error> {
let entries = fs::read_dir(path)?;
for entry in entries {
let path = entry?.path();
if path.is_dir() {
continue;
}
let mut file = fs::File::open(&path)?;
let mut content = String::new();
if file.read_to_string(&mut content).is_err() {
continue;
}
match try_extract_address(&content) {
Some(a) if a == *addr => {
let kf = KeyFile::decode(content)?;
return Ok((path.to_owned(), kf));
}
_ => continue,
}
}
Err(Error::NotFound)
}
}
impl Encodable for KeyFile {
fn encode<S: Encoder>(&self, s: &mut S) -> Result<(), S::Error> {
match SerializableKeyFileCore::try_from(self.clone()) {
Ok(sf) => sf.encode(s),
Err(_) => {
match SerializableKeyFileHD::try_from(self.clone()) {
Ok(sf) => sf.encode(s),
Err(_) => Ok(()),
}
}
}
}
}
pub fn write<P: AsRef<Path>>(kf: &KeyFile, p: P) -> Result<(), Error> {
let json = match SerializableKeyFileCore::try_from(kf.clone()) {
Ok(sf) => json::encode(&sf)?,
Err(_) => {
match SerializableKeyFileHD::try_from(kf.clone()) {
Ok(sf) => json::encode(&sf)?,
Err(e) => return Err(Error::InvalidCrypto(e.to_string())),
}
}
};
let mut file = File::create(&p)?;
file.write_all(json.as_ref()).ok();
Ok(())
}
pub fn list_accounts<P: AsRef<Path>>(
path: P,
show_hidden: bool,
) -> Result<Vec<(String, String, String, bool)>, Error> {
let mut accounts: Vec<(String, String, String, bool)> = vec![];
for e in read_dir(&path)? {
if e.is_err() {
continue;
}
let entry = e.unwrap();
let mut content = String::new();
if let Ok(mut keyfile) = File::open(entry.path()) {
if keyfile.read_to_string(&mut content).is_err() {
continue;
}
match KeyFile::decode(content) {
Ok(kf) => {
let mut info = Vec::new();
if kf.visible.is_none() || kf.visible.unwrap() || show_hidden {
let is_hd = match kf.crypto {
CryptoType::Core(_) => false,
CryptoType::HdWallet(_) => true,
};
match kf.name {
Some(name) => info.push(name),
None => info.push("".to_string()),
}
match kf.description {
Some(desc) => info.push(desc),
None => info.push("".to_string()),
}
accounts.push((
info[0].clone(),
kf.address.to_string(),
info[1].clone(),
is_hd,
));
}
}
Err(_) => info!("Invalid keystore file format for: {:?}", entry.file_name()),
}
}
}
Ok(accounts)
}
pub fn hide<P: AsRef<Path>>(addr: &Address, path: P) -> Result<bool, Error> {
let (p, mut kf) = KeyFile::search_by_address(addr, &path)?;
kf.visible = Some(false);
write(&kf, &p)?;
Ok(true)
}
pub fn unhide<P: AsRef<Path>>(addr: &Address, path: P) -> Result<bool, Error> {
let (p, mut kf) = KeyFile::search_by_address(addr, &path)?;
kf.visible = Some(true);
write(&kf, &p)?;
Ok(true)
}
fn generate_filename(uuid: &str) -> String {
format!("UTC--{}Z--{}", &util::timestamp(), &uuid)
}
#[cfg(test)]
mod tests {
use super::*;
use tests::*;
#[test]
fn should_catch_unsupported_keyfile_version() {
let str = r#"{
"version": 2,
"id": "9bec4728-37f9-4444-9990-2ba70ee038e9"
}"#;
assert!(KeyFile::decode(str.to_string()).is_err());
}
#[test]
fn should_catch_keyfile_version_malformed() {
let str = r#"{
"version": "x",
"id": "9bec4728-37f9-4444-9990-2ba70ee038e9"
}"#;
assert!(KeyFile::decode(str.to_string()).is_err());
}
#[test]
fn should_catch_keyfile_uuid_malformed() {
let str = r#"{
"version": 3,
"id": "__ec4728-37f9-4444-9990-2ba70ee038e9"
}"#;
assert!(KeyFile::decode(str.to_string()).is_err());
}
#[test]
fn should_catch_absent_keyfile_uuid() {
let str = r#"{"version": 3}"#;
assert!(KeyFile::decode(str.to_string()).is_err());
}
#[test]
fn should_generate_filename() {
let re = Regex::new(r"^UTC--\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z--*").unwrap();
assert!(re.is_match(
&generate_filename("9bec4728-37f9-4444-9990-2ba70ee038e9"),
));
}
}