use crate::print::Print;
use ed25519_dalek::Signer;
use keyring::Entry;
use sep5::seed_phrase::SeedPhrase;
use zeroize::Zeroize;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[cfg(feature = "additional-libs")]
#[error(transparent)]
Keyring(#[from] keyring::Error),
#[error(transparent)]
Sep5(#[from] sep5::error::Error),
#[error("Secure Store keys are not allowed: additional-libs feature must be enabled")]
FeatureNotEnabled,
}
pub struct StellarEntry {
name: String,
#[cfg(feature = "additional-libs")]
keyring: Entry,
}
impl StellarEntry {
pub fn new(name: &str) -> Result<Self, Error> {
Ok(StellarEntry {
name: name.to_string(),
keyring: Entry::new(name, &whoami::username())?,
})
}
pub fn write(&self, seed_phrase: SeedPhrase, print: &Print) -> Result<(), Error> {
if let Ok(key) = self.get_public_key(None) {
print.warnln(format!(
"A key for {0} already exists in your operating system's secure store: {1}",
self.name, key
));
} else {
print.infoln(format!(
"Saving a new key to your operating system's secure store: {0}",
self.name
));
self.set_seed_phrase(seed_phrase)?;
}
Ok(())
}
fn set_seed_phrase(&self, seed_phrase: SeedPhrase) -> Result<(), Error> {
let mut data = seed_phrase.seed_phrase.into_phrase();
self.keyring.set_password(&data)?;
data.zeroize();
Ok(())
}
pub fn delete_seed_phrase(&self, print: &Print) -> Result<(), Error> {
match self.keyring.delete_credential() {
Ok(()) => Ok(()),
Err(e) => match e {
keyring::Error::NoEntry => {
print.infoln("This key was already removed from the secure store.");
Ok(())
}
_ => Err(Error::Keyring(e)),
},
}
}
fn get_seed_phrase(&self) -> Result<SeedPhrase, Error> {
Ok(self.keyring.get_password()?.parse()?)
}
fn use_key<T>(
&self,
f: impl FnOnce(ed25519_dalek::SigningKey) -> Result<T, Error>,
hd_path: Option<usize>,
) -> Result<T, Error> {
let mut key_bytes: [u8; 32] = {
self.get_seed_phrase()?
.from_path_index(hd_path.unwrap_or_default(), None)?
.private()
.0
};
let result = {
let keypair = ed25519_dalek::SigningKey::from_bytes(&key_bytes);
f(keypair)?
};
key_bytes.zeroize();
Ok(result)
}
pub fn get_public_key(
&self,
hd_path: Option<usize>,
) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
self.use_key(
|keypair| {
Ok(stellar_strkey::ed25519::PublicKey(
*keypair.verifying_key().as_bytes(),
))
},
hd_path,
)
}
pub fn sign_data(&self, data: &[u8], hd_path: Option<usize>) -> Result<Vec<u8>, Error> {
self.use_key(
|keypair| {
let signature = keypair.sign(data);
Ok(signature.to_bytes().to_vec())
},
hd_path,
)
}
}
#[cfg(feature = "additional-libs")]
#[cfg(test)]
mod test {
use crate::print;
use super::*;
use keyring::{mock, set_default_credential_builder};
#[test]
fn test_get_password() {
set_default_credential_builder(mock::default_credential_builder());
let seed_phrase = crate::config::secret::seed_phrase_from_seed(None).unwrap();
let seed_phrase_clone = seed_phrase.clone();
let entry = StellarEntry::new("test").unwrap();
let set_seed_phrase_result = entry.set_seed_phrase(seed_phrase);
assert!(set_seed_phrase_result.is_ok());
let get_seed_phrase_result = entry.get_seed_phrase();
assert!(get_seed_phrase_result.is_ok());
assert_eq!(
seed_phrase_clone.phrase(),
get_seed_phrase_result.unwrap().phrase()
);
}
#[test]
fn test_get_public_key() {
set_default_credential_builder(mock::default_credential_builder());
let seed_phrase = crate::config::secret::seed_phrase_from_seed(None).unwrap();
let public_key = seed_phrase.from_path_index(0, None).unwrap().public().0;
let entry = StellarEntry::new("test").unwrap();
let set_seed_phrase_result = entry.set_seed_phrase(seed_phrase);
assert!(set_seed_phrase_result.is_ok());
let get_public_key_result = entry.get_public_key(None);
assert!(get_public_key_result.is_ok());
assert_eq!(public_key, get_public_key_result.unwrap().0);
}
#[test]
fn test_sign_data() {
set_default_credential_builder(mock::default_credential_builder());
let seed_phrase = crate::config::secret::seed_phrase_from_seed(None).unwrap();
let entry = StellarEntry::new("test").unwrap();
entry.set_seed_phrase(seed_phrase).unwrap();
let tx_xdr = r"AAAAAgAAAADh6eOnZEq1xQgKioffuH7/8D8x8+OdGFEkiYC6QKMWzQAAAGQAAACuAAAAAQAAAAAAAAAAAAAAAQAAAAAAAAAYAAAAAQAAAAAAAAAAAAAAAOHp46dkSrXFCAqKh9+4fv/wPzHz450YUSSJgLpAoxbNoFT1s8jZPCv9IJ2DsqGTA8pOtavv58JF53aDycpRPcEAAAAA+N2m5zc3EfWUmLvigYPOHKXhSy8OrWfVibc6y6PrQoYAAAAAAAAAAAAAAAA";
let sign_tx_env_result = entry.sign_data(tx_xdr.as_bytes(), None);
assert!(sign_tx_env_result.is_ok());
}
#[test]
fn test_delete_seed_phrase() {
set_default_credential_builder(mock::default_credential_builder());
let seed_phrase = crate::config::secret::seed_phrase_from_seed(None).unwrap();
let entry = StellarEntry::new("test").unwrap();
entry.set_seed_phrase(seed_phrase).unwrap();
let get_seed_phrase_result = entry.get_seed_phrase();
assert!(get_seed_phrase_result.is_ok());
let print = print::Print::new(true);
let delete_seed_phrase_result = entry.delete_seed_phrase(&print);
assert!(delete_seed_phrase_result.is_ok());
let get_password_result = entry.get_seed_phrase();
assert!(get_password_result.is_err());
assert!(matches!(
get_password_result.unwrap_err(),
Error::Keyring(_)
));
}
}