#![deny(missing_docs)]
#![deny(unsafe_code)]
use std::path::PathBuf;
use std::{fmt::Display, str::FromStr};
use anyhow::bail;
use clap::ValueEnum;
use cryptex::KeyRing;
use essential_signer::Key;
use essential_signer::PublicKey;
use essential_types::contract::Contract;
use essential_types::{Hash, Word};
use rand::SeedableRng;
use rusqlite::OptionalExtension;
use serde::Serialize;
pub use essential_signer::ed25519_dalek;
pub use essential_signer::secp256k1;
pub use essential_signer::Padding;
pub use essential_signer::Signature;
const NAME: &str = "essential-wallet";
#[derive(ValueEnum, Clone, Copy, Debug)]
pub enum Scheme {
Secp256k1,
Ed25519,
}
pub struct Wallet {
store: cryptex::sqlcipher::SqlCipherKeyring,
names: rusqlite::Connection,
#[cfg(feature = "test-utils")]
dir: Option<tempfile::TempDir>,
}
impl Wallet {
pub fn new(password: &str, path: PathBuf) -> anyhow::Result<Self> {
let mut path = db_dir(Some(path.clone()))?;
let mut params = cryptex::sqlcipher::ConnectionParams::default();
params.key(password.as_bytes());
let store = cryptex::sqlcipher::SqlCipherKeyring::with_params(¶ms, Some(path.clone()))
.map_err(|e| {
if let cryptex::error::KeyRingError::GeneralError { msg } = &e {
if msg.contains("file is not a database") {
return anyhow::anyhow!("Incorrect password.",);
}
}
e.into()
})?;
path.push("names.db3");
let names = rusqlite::Connection::open(path)?;
create_table(&names)?;
#[cfg(not(feature = "test-utils"))]
let r = Ok(Self { store, names });
#[cfg(feature = "test-utils")]
let r = Ok(Self {
store,
names,
dir: None,
});
r
}
pub fn with_default_path(password: &str) -> anyhow::Result<Self> {
let path = db_dir(None)?;
Self::new(password, path)
}
#[cfg(feature = "test-utils")]
pub fn temp() -> anyhow::Result<Self> {
let dir = tempfile::tempdir()?;
let path = db_dir(Some(dir.path().to_path_buf()))?;
let mut s = Self::new("password", path)?;
s.dir = Some(dir);
Ok(s)
}
#[cfg(feature = "test-utils")]
pub fn insert_key(&mut self, name: &str, key: Key) -> anyhow::Result<()> {
let scheme = match key {
Key::Secp256k1(_) => Scheme::Secp256k1,
Key::Ed25519(_) => Scheme::Ed25519,
};
self.insert_name(name, scheme)?;
match key {
Key::Secp256k1(private_key) => {
match self.store.set_secret(name, private_key.as_ref().as_slice()) {
Ok(_) => Ok(()),
Err(e) => {
self.delete_name(name)?;
Err(e.into())
}
}
}
Key::Ed25519(_) => todo!("Not supported yet"),
}
}
#[cfg(feature = "test-utils")]
pub fn generate_private_key(&mut self, scheme: Scheme) -> anyhow::Result<Key> {
match scheme {
Scheme::Secp256k1 => {
let mut rng = rand::rngs::StdRng::from_entropy();
let (private_key, _) = secp256k1::generate_keypair(&mut rng);
Ok(Key::Secp256k1(private_key))
}
Scheme::Ed25519 => todo!("Not supported yet"),
}
}
pub fn new_key_pair(&mut self, name: &str, scheme: Scheme) -> anyhow::Result<()> {
self.insert_name(name, scheme)?;
match scheme {
Scheme::Secp256k1 => {
let mut rng = rand::rngs::StdRng::from_entropy();
let (private_key, _) = secp256k1::generate_keypair(&mut rng);
match self.store.set_secret(name, private_key.as_ref().as_slice()) {
Ok(_) => Ok(()),
Err(e) => {
self.delete_name(name)?;
Err(e.into())
}
}
}
Scheme::Ed25519 => todo!("Not supported yet"),
}
}
pub fn delete_key_pair(&mut self, name: &str) -> anyhow::Result<()> {
let Some(scheme) = self.scheme(name)? else {
bail!("Name not found: {}", name)
};
self.delete_name(name)?;
match self
.store
.delete_secret(name)
.map_err(|_| anyhow::anyhow!("The name '{}' does not exist", name))
{
Ok(_) => Ok(()),
Err(e) => {
self.insert_name(name, scheme)?;
Err(e)
}
}
}
pub fn list_names(&mut self) -> anyhow::Result<Vec<String>> {
Ok(self.list()?.into_iter().map(|(n, _)| n).collect())
}
pub fn get_public_key(&mut self, name: &str) -> anyhow::Result<PublicKey> {
let key = self.name_to_key(name)?;
Ok(essential_signer::public_key(&key))
}
pub fn sign_contract(
&mut self,
data: Contract,
name: &str,
) -> anyhow::Result<essential_types::contract::SignedContract> {
let key = self.name_to_key(name)?;
match key {
Key::Secp256k1(key) => Ok(essential_sign::contract::sign(data, &key)),
Key::Ed25519(_) => Err(anyhow::anyhow!(
"Ed25519 not supported for signing contracts. Please use a Secp256k1 key"
))?,
}
}
pub fn sign_postcard<T: Serialize>(
&mut self,
data: &T,
name: &str,
) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_postcard(data, &key)
}
pub fn sign_postcard_with_padding<T: Serialize>(
&mut self,
data: &T,
padding: Padding,
name: &str,
) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_postcard_with_padding(data, padding, &key)
}
pub fn sign_hash(&mut self, data: Hash, name: &str) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_hash(data, &key)
}
pub fn sign_words(&mut self, data: &[Word], name: &str) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_words(data, &key)
}
pub fn sign_bytes_with_padding(
&mut self,
data: Vec<u8>,
padding: Padding,
name: &str,
) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_bytes_with_padding(data, padding, &key)
}
pub fn sign_aligned_bytes(&mut self, data: &[u8], name: &str) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_aligned_bytes(data, &key)
}
pub fn sign_bytes_unchecked(&mut self, data: &[u8], name: &str) -> anyhow::Result<Signature> {
let key = self.name_to_key(name)?;
essential_signer::sign_bytes_unchecked(data, &key)
}
fn insert_name(&mut self, name: &str, scheme: Scheme) -> anyhow::Result<()> {
self.names.execute(
"INSERT OR REPLACE INTO names (name, scheme) VALUES (?, ?)",
[name, &scheme.to_string()],
)?;
Ok(())
}
fn delete_name(&mut self, name: &str) -> anyhow::Result<()> {
self.names
.execute("DELETE FROM names WHERE name = ?", [name])?;
Ok(())
}
fn name_to_key(&mut self, name: &str) -> anyhow::Result<Key> {
let Some(scheme) = self.scheme(name)? else {
bail!("Name not found: {}. Maybe you need to create it?", name)
};
let private_key = self.store.get_secret(name)?;
match scheme {
Scheme::Secp256k1 => {
let private_key = secp256k1::SecretKey::from_slice(private_key.as_slice())?;
Ok(Key::Secp256k1(private_key))
}
Scheme::Ed25519 => todo!("Not supported yet"),
}
}
fn scheme(&mut self, name: &str) -> anyhow::Result<Option<Scheme>> {
self.names
.query_row(
"SELECT scheme FROM names WHERE name = ? LIMIT 1",
[name],
|row| {
let scheme = row.get::<_, String>(0)?;
Ok(scheme)
},
)
.optional()?
.map(|scheme| {
let scheme = <Scheme as FromStr>::from_str(&scheme)?;
Ok(scheme)
})
.transpose()
}
fn list(&mut self) -> anyhow::Result<Vec<(String, Scheme)>> {
self.names
.prepare_cached("SELECT name, scheme FROM names")?
.query_and_then([], |row| {
let name = row.get::<_, String>(0)?;
let scheme = row.get::<_, String>(1)?;
let scheme = <Scheme as FromStr>::from_str(&scheme)?;
Ok((name, scheme))
})?
.collect::<anyhow::Result<Vec<_>>>()
}
}
impl Display for Scheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Scheme::Secp256k1 => write!(f, "secp256k1"),
Scheme::Ed25519 => write!(f, "ed25519"),
}
}
}
impl FromStr for Scheme {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"secp256k1" => Ok(Scheme::Secp256k1),
"ed25519" => Ok(Scheme::Ed25519),
_ => Err(anyhow::anyhow!("Unknown scheme: {}", s)),
}
}
}
fn create_table(names: &rusqlite::Connection) -> anyhow::Result<()> {
names.execute(
"CREATE TABLE IF NOT EXISTS names (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
scheme TEXT NOT NULL
)",
[],
)?;
Ok(())
}
fn db_dir(in_path: Option<PathBuf>) -> anyhow::Result<PathBuf> {
let path = match in_path {
None => {
let mut path = dirs::home_dir().unwrap_or_else(|| {
dirs::document_dir().unwrap_or_else(|| {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR").to_string()))
})
});
path.push(format!(".{}", NAME));
path
}
Some(path) => path,
};
if !path.is_dir() {
std::fs::create_dir_all(&path)?;
}
Ok(path)
}