use std::{
collections::{BTreeMap, HashSet},
fmt::{Display, Formatter, Write},
fs,
fs::File,
io::BufReader,
path::{Path, PathBuf},
};
use anyhow::{Context, anyhow, bail, ensure};
use bip32::DerivationPath;
use bip39::{Language, Mnemonic, Seed};
use iota_types::{
base_types::IotaAddress,
crypto::{
EncodeDecodeBase64, IotaKeyPair, PublicKey, Signature, SignatureScheme, enum_dispatch,
get_key_pair_from_rng,
},
};
use rand::{SeedableRng, rngs::StdRng};
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use shared_crypto::intent::{Intent, IntentMessage};
use crate::{
key_derive::{derive_key_pair_from_path, generate_new_key},
random_names::{random_name, random_names},
};
#[derive(Serialize, Deserialize)]
#[enum_dispatch(AccountKeystore)]
pub enum Keystore {
File(FileBasedKeystore),
InMem(InMemKeystore),
}
#[enum_dispatch]
pub trait AccountKeystore: Send + Sync {
fn add_key(&mut self, alias: Option<String>, keypair: IotaKeyPair)
-> Result<(), anyhow::Error>;
fn keys(&self) -> Vec<PublicKey>;
fn get_key(&self, address: &IotaAddress) -> Result<&IotaKeyPair, anyhow::Error>;
fn sign_hashed(&self, address: &IotaAddress, msg: &[u8])
-> Result<Signature, signature::Error>;
fn sign_secure<T>(
&self,
address: &IotaAddress,
msg: &T,
intent: Intent,
) -> Result<Signature, signature::Error>
where
T: Serialize;
fn addresses(&self) -> Vec<IotaAddress> {
self.keys().iter().map(|k| k.into()).collect()
}
fn addresses_with_alias(&self) -> Vec<(&IotaAddress, &Alias)>;
fn aliases(&self) -> Vec<&Alias>;
fn aliases_mut(&mut self) -> Vec<&mut Alias>;
fn alias_names(&self) -> Vec<&str> {
self.aliases()
.into_iter()
.map(|a| a.alias.as_str())
.collect()
}
fn get_alias_by_address(&self, address: &IotaAddress) -> Result<String, anyhow::Error>;
fn get_address_by_alias(&self, alias: String) -> Result<&IotaAddress, anyhow::Error>;
fn alias_exists(&self, alias: &str) -> bool {
self.alias_names().contains(&alias)
}
fn create_alias(&self, alias: Option<String>) -> Result<String, anyhow::Error>;
fn update_alias(
&mut self,
old_alias: &str,
new_alias: Option<&str>,
) -> Result<String, anyhow::Error>;
fn update_alias_value(
&mut self,
old_alias: &str,
new_alias: Option<&str>,
) -> Result<String, anyhow::Error> {
if !self.alias_exists(old_alias) {
bail!("The provided alias {old_alias} does not exist");
}
let new_alias_name = match new_alias {
Some(x) => validate_alias(x)?,
None => random_name(
&self
.alias_names()
.into_iter()
.map(|x| x.to_string())
.collect::<HashSet<_>>(),
),
};
for a in self.aliases_mut() {
if a.alias == old_alias {
let pk = &a.public_key_base64;
*a = Alias {
alias: new_alias_name.clone(),
public_key_base64: pk.clone(),
};
}
}
Ok(new_alias_name)
}
fn generate_and_add_new_key(
&mut self,
key_scheme: SignatureScheme,
alias: Option<String>,
derivation_path: Option<DerivationPath>,
word_length: Option<String>,
) -> Result<(IotaAddress, String, SignatureScheme), anyhow::Error> {
let (address, kp, scheme, phrase) =
generate_new_key(key_scheme, derivation_path, word_length)?;
self.add_key(alias, kp)?;
Ok((address, phrase, scheme))
}
fn import_from_mnemonic(
&mut self,
phrase: &str,
key_scheme: SignatureScheme,
derivation_path: Option<DerivationPath>,
alias: Option<String>,
) -> Result<IotaAddress, anyhow::Error> {
let mnemonic = Mnemonic::from_phrase(phrase, Language::English)
.map_err(|e| anyhow::anyhow!("Invalid mnemonic phrase: {:?}", e))?;
let seed = Seed::new(&mnemonic, "");
self.import_from_seed(seed.as_bytes(), key_scheme, derivation_path, alias)
}
fn import_from_seed(
&mut self,
seed: &[u8],
key_scheme: SignatureScheme,
derivation_path: Option<DerivationPath>,
alias: Option<String>,
) -> Result<IotaAddress, anyhow::Error> {
match derive_key_pair_from_path(seed, derivation_path, &key_scheme) {
Ok((address, kp)) => {
self.add_key(alias, kp)?;
Ok(address)
}
Err(e) => Err(anyhow!("error getting keypair {:?}", e)),
}
}
}
impl Display for Keystore {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut writer = String::new();
match self {
Keystore::File(file) => {
writeln!(writer, "Keystore Type: File")?;
write!(writer, "Keystore Path : {:?}", file.path)?;
write!(f, "{}", writer)
}
Keystore::InMem(_) => {
writeln!(writer, "Keystore Type: InMem")?;
write!(f, "{}", writer)
}
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Alias {
pub alias: String,
pub public_key_base64: String,
}
#[derive(Default)]
pub struct FileBasedKeystore {
keys: BTreeMap<IotaAddress, IotaKeyPair>,
aliases: BTreeMap<IotaAddress, Alias>,
path: PathBuf,
}
impl Serialize for FileBasedKeystore {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.path.to_str().unwrap_or(""))
}
}
impl<'de> Deserialize<'de> for FileBasedKeystore {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
FileBasedKeystore::new(&PathBuf::from(String::deserialize(deserializer)?))
.map_err(D::Error::custom)
}
}
impl AccountKeystore for FileBasedKeystore {
fn sign_hashed(
&self,
address: &IotaAddress,
msg: &[u8],
) -> Result<Signature, signature::Error> {
Ok(Signature::new_hashed(
msg,
self.keys.get(address).ok_or_else(|| {
signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
})?,
))
}
fn sign_secure<T>(
&self,
address: &IotaAddress,
msg: &T,
intent: Intent,
) -> Result<Signature, signature::Error>
where
T: Serialize,
{
Ok(Signature::new_secure(
&IntentMessage::new(intent, msg),
self.keys.get(address).ok_or_else(|| {
signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
})?,
))
}
fn add_key(
&mut self,
alias: Option<String>,
keypair: IotaKeyPair,
) -> Result<(), anyhow::Error> {
let address: IotaAddress = (&keypair.public()).into();
let alias = self.create_alias(alias)?;
self.aliases.insert(address, Alias {
alias,
public_key_base64: keypair.public().encode_base64(),
});
self.keys.insert(address, keypair);
self.save()?;
Ok(())
}
fn aliases(&self) -> Vec<&Alias> {
self.aliases.values().collect()
}
fn addresses_with_alias(&self) -> Vec<(&IotaAddress, &Alias)> {
self.aliases.iter().collect::<Vec<_>>()
}
fn aliases_mut(&mut self) -> Vec<&mut Alias> {
self.aliases.values_mut().collect()
}
fn keys(&self) -> Vec<PublicKey> {
self.keys.values().map(|key| key.public()).collect()
}
fn create_alias(&self, alias: Option<String>) -> Result<String, anyhow::Error> {
match alias {
Some(a) if self.alias_exists(&a) => {
bail!("Alias {a} already exists. Please choose another alias.")
}
Some(a) => validate_alias(&a),
None => Ok(random_name(
&self
.alias_names()
.into_iter()
.map(|x| x.to_string())
.collect::<HashSet<_>>(),
)),
}
}
fn get_address_by_alias(&self, alias: String) -> Result<&IotaAddress, anyhow::Error> {
self.addresses_with_alias()
.iter()
.find(|x| x.1.alias == alias)
.ok_or_else(|| anyhow!("Cannot resolve alias {alias} to an address"))
.map(|x| x.0)
}
fn get_alias_by_address(&self, address: &IotaAddress) -> Result<String, anyhow::Error> {
match self.aliases.get(address) {
Some(alias) => Ok(alias.alias.clone()),
None => bail!("Cannot find alias for address {address}"),
}
}
fn get_key(&self, address: &IotaAddress) -> Result<&IotaKeyPair, anyhow::Error> {
match self.keys.get(address) {
Some(key) => Ok(key),
None => Err(anyhow!("Cannot find key for address: [{address}]")),
}
}
fn update_alias(
&mut self,
old_alias: &str,
new_alias: Option<&str>,
) -> Result<String, anyhow::Error> {
let new_alias_name = self.update_alias_value(old_alias, new_alias)?;
self.save_aliases()?;
Ok(new_alias_name)
}
}
impl FileBasedKeystore {
pub fn new(path: &PathBuf) -> Result<Self, anyhow::Error> {
let keys = if path.exists() {
let reader =
BufReader::new(File::open(path).with_context(|| {
format!("Cannot open the keystore file: {}", path.display())
})?);
let kp_strings: Vec<String> = serde_json::from_reader(reader).with_context(|| {
format!("Cannot deserialize the keystore file: {}", path.display(),)
})?;
kp_strings
.iter()
.map(|kpstr| {
let key = IotaKeyPair::decode(kpstr);
key.map(|k| (IotaAddress::from(&k.public()), k))
})
.collect::<Result<BTreeMap<_, _>, _>>()
.map_err(|e| anyhow!("Invalid keystore file: {}. {}", path.display(), e))?
} else {
BTreeMap::new()
};
let mut aliases_path = path.clone();
aliases_path.set_extension("aliases");
let aliases = if aliases_path.exists() {
let reader = BufReader::new(File::open(&aliases_path).with_context(|| {
format!(
"Cannot open aliases file in keystore: {}",
aliases_path.display()
)
})?);
let aliases: Vec<Alias> = serde_json::from_reader(reader).with_context(|| {
format!(
"Cannot deserialize aliases file in keystore: {}",
aliases_path.display(),
)
})?;
aliases
.into_iter()
.map(|alias| {
let key = PublicKey::decode_base64(&alias.public_key_base64);
key.map(|k| (Into::<IotaAddress>::into(&k), alias))
})
.collect::<Result<BTreeMap<_, _>, _>>()
.map_err(|e| {
anyhow!(
"Invalid aliases file in keystore: {}. {}",
aliases_path.display(),
e
)
})?
} else if keys.is_empty() {
BTreeMap::new()
} else {
let names: Vec<String> = random_names(HashSet::new(), keys.len());
let aliases = keys
.iter()
.zip(names)
.map(|((iota_address, ikp), alias)| {
let public_key_base64 = ikp.public().encode_base64();
(*iota_address, Alias {
alias,
public_key_base64,
})
})
.collect::<BTreeMap<_, _>>();
let aliases_store = serde_json::to_string_pretty(&aliases.values().collect::<Vec<_>>())
.with_context(|| {
format!(
"Cannot serialize aliases to file in keystore: {}",
aliases_path.display()
)
})?;
fs::write(aliases_path, aliases_store)?;
aliases
};
Ok(Self {
keys,
aliases,
path: path.to_path_buf(),
})
}
pub fn set_path(&mut self, path: &Path) {
self.path = path.to_path_buf();
}
pub fn save_aliases(&self) -> Result<(), anyhow::Error> {
let aliases_store = serde_json::to_string_pretty(
&self.aliases.values().collect::<Vec<_>>(),
)
.with_context(|| {
format!(
"Cannot serialize aliases to file in keystore: {}",
self.path.display()
)
})?;
let mut aliases_path = self.path.clone();
aliases_path.set_extension("aliases");
fs::write(aliases_path, aliases_store)?;
Ok(())
}
pub fn save_keystore(&self) -> Result<(), anyhow::Error> {
let store = serde_json::to_string_pretty(
&self
.keys
.values()
.map(|k| k.encode())
.collect::<Result<Vec<_>, _>>()
.map_err(|e| anyhow!(e))?,
)
.with_context(|| format!("Cannot serialize keystore to file: {}", self.path.display()))?;
fs::write(&self.path, store)?;
println!("Keys saved as Bech32.");
Ok(())
}
pub fn save(&self) -> Result<(), anyhow::Error> {
self.save_aliases()?;
self.save_keystore()?;
Ok(())
}
pub fn key_pairs(&self) -> Vec<&IotaKeyPair> {
self.keys.values().collect()
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct InMemKeystore {
aliases: BTreeMap<IotaAddress, Alias>,
keys: BTreeMap<IotaAddress, IotaKeyPair>,
}
impl AccountKeystore for InMemKeystore {
fn sign_hashed(
&self,
address: &IotaAddress,
msg: &[u8],
) -> Result<Signature, signature::Error> {
Ok(Signature::new_hashed(
msg,
self.keys.get(address).ok_or_else(|| {
signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
})?,
))
}
fn sign_secure<T>(
&self,
address: &IotaAddress,
msg: &T,
intent: Intent,
) -> Result<Signature, signature::Error>
where
T: Serialize,
{
Ok(Signature::new_secure(
&IntentMessage::new(intent, msg),
self.keys.get(address).ok_or_else(|| {
signature::Error::from_source(format!("Cannot find key for address: [{address}]"))
})?,
))
}
fn add_key(
&mut self,
alias: Option<String>,
keypair: IotaKeyPair,
) -> Result<(), anyhow::Error> {
let address: IotaAddress = (&keypair.public()).into();
let alias = alias.unwrap_or_else(|| {
random_name(
&self
.aliases()
.iter()
.map(|x| x.alias.clone())
.collect::<HashSet<_>>(),
)
});
let public_key_base64 = keypair.public().encode_base64();
let alias = Alias {
alias,
public_key_base64,
};
self.aliases.insert(address, alias);
self.keys.insert(address, keypair);
Ok(())
}
fn aliases(&self) -> Vec<&Alias> {
self.aliases.values().collect()
}
fn addresses_with_alias(&self) -> Vec<(&IotaAddress, &Alias)> {
self.aliases.iter().collect::<Vec<_>>()
}
fn keys(&self) -> Vec<PublicKey> {
self.keys.values().map(|key| key.public()).collect()
}
fn get_key(&self, address: &IotaAddress) -> Result<&IotaKeyPair, anyhow::Error> {
match self.keys.get(address) {
Some(key) => Ok(key),
None => Err(anyhow!("Cannot find key for address: [{address}]")),
}
}
fn get_alias_by_address(&self, address: &IotaAddress) -> Result<String, anyhow::Error> {
match self.aliases.get(address) {
Some(alias) => Ok(alias.alias.clone()),
None => bail!("Cannot find alias for address {address}"),
}
}
fn get_address_by_alias(&self, alias: String) -> Result<&IotaAddress, anyhow::Error> {
self.addresses_with_alias()
.iter()
.find(|x| x.1.alias == alias)
.ok_or_else(|| anyhow!("Cannot resolve alias {alias} to an address"))
.map(|x| x.0)
}
fn create_alias(&self, alias: Option<String>) -> Result<String, anyhow::Error> {
match alias {
Some(a) if self.alias_exists(&a) => {
bail!("Alias {a} already exists. Please choose another alias.")
}
Some(a) => validate_alias(&a),
None => Ok(random_name(
&self
.alias_names()
.into_iter()
.map(|x| x.to_string())
.collect::<HashSet<_>>(),
)),
}
}
fn aliases_mut(&mut self) -> Vec<&mut Alias> {
self.aliases.values_mut().collect()
}
fn update_alias(
&mut self,
old_alias: &str,
new_alias: Option<&str>,
) -> Result<String, anyhow::Error> {
self.update_alias_value(old_alias, new_alias)
}
}
impl InMemKeystore {
pub fn new_insecure_for_tests(initial_key_number: usize) -> Self {
let mut rng = StdRng::from_seed([0; 32]);
let keys = (0..initial_key_number)
.map(|_| get_key_pair_from_rng(&mut rng))
.map(|(ad, k)| (ad, IotaKeyPair::Ed25519(k)))
.collect::<BTreeMap<IotaAddress, IotaKeyPair>>();
let aliases = keys
.iter()
.zip(random_names(HashSet::new(), keys.len()))
.map(|((iota_address, ikp), alias)| {
let public_key_base64 = ikp.public().encode_base64();
(*iota_address, Alias {
alias,
public_key_base64,
})
})
.collect::<BTreeMap<_, _>>();
Self { aliases, keys }
}
}
fn validate_alias(alias: &str) -> Result<String, anyhow::Error> {
let re = Regex::new(r"^[A-Za-z][A-Za-z0-9-_\.]*$")
.map_err(|_| anyhow!("Cannot build the regex needed to validate the alias naming"))?;
let alias = alias.trim();
ensure!(
re.is_match(alias),
"Invalid alias. A valid alias must start with a letter and can contain only letters, digits, hyphens (-), dots (.), or underscores (_)."
);
Ok(alias.to_string())
}
#[cfg(test)]
mod tests {
use crate::keystore::validate_alias;
#[test]
fn validate_alias_test() {
assert!(validate_alias("A.B_dash").is_ok());
assert!(validate_alias("A.B-C1_dash").is_ok());
assert!(validate_alias("abc_123.iota").is_ok());
assert!(validate_alias("A.B-C_dash!").is_err());
assert!(validate_alias(".B-C_dash!").is_err());
assert!(validate_alias("_test").is_err());
assert!(validate_alias("123").is_err());
assert!(validate_alias("@@123").is_err());
assert!(validate_alias("@_Ab").is_err());
assert!(validate_alias("_Ab").is_err());
assert!(validate_alias("^A").is_err());
assert!(validate_alias("-A").is_err());
}
}