essential_wallet/
lib.rs

1//! # Essential Wallet
2//!
3//! ## Warning!
4//! This crate has not been audited for security.
5//! USE AT YOUR OWN RISK!
6//! This crate is intended for testing and educational purposes only.
7//! Never use this for production code or to store real funds.
8//!
9//! This crate can be used as a library in front ends for testing key management and signing.
10//! This crate also provides a binary cli tool that can be used to manage keys and sign data.
11
12#![deny(missing_docs)]
13#![deny(unsafe_code)]
14
15use std::path::PathBuf;
16use std::{fmt::Display, str::FromStr};
17
18use clap::ValueEnum;
19use essential_signer::Key;
20use essential_signer::PublicKey;
21use essential_types::contract::Contract;
22use essential_types::{Hash, Word};
23use rand::SeedableRng;
24use serde::Serialize;
25
26pub use essential_signer::ed25519_dalek;
27pub use essential_signer::secp256k1;
28pub use essential_signer::Padding;
29pub use essential_signer::Signature;
30
31mod store;
32
33const NAME: &str = "essential-wallet";
34
35#[derive(ValueEnum, Clone, Copy, Debug)]
36/// Which signature scheme to use.
37pub enum Scheme {
38    /// The secp256k1 signature scheme.
39    Secp256k1,
40    /// The ed25519 signature scheme.
41    Ed25519,
42}
43
44/// Essential Wallet
45/// **USE AT YOUR OWN RISK!**
46/// Stores secret keys in sqlcipher database.
47pub struct Wallet {
48    store: store::Store,
49    #[cfg(feature = "test-utils")]
50    dir: Option<tempfile::TempDir>,
51}
52
53impl Wallet {
54    /// Create a new wallet with a password and directory.
55    pub fn new(password: &str, path: PathBuf) -> anyhow::Result<Self> {
56        let mut path = db_dir(Some(path.clone()))?;
57        path.push("accounts.sqlite3");
58        let store = store::Store::new(password, &path)?;
59
60        #[cfg(not(feature = "test-utils"))]
61        let r = Ok(Self { store });
62        #[cfg(feature = "test-utils")]
63        let r = Ok(Self { store, dir: None });
64        r
65    }
66
67    /// Create a new wallet with a password.
68    ///
69    /// The wallet will find a suitable directory to store the database.
70    pub fn with_default_path(password: &str) -> anyhow::Result<Self> {
71        let path = db_dir(None)?;
72        Self::new(password, path)
73    }
74
75    #[cfg(feature = "test-utils")]
76    /// Create a wallet for testing that has an empty password and a temporary directory.
77    pub fn temp() -> anyhow::Result<Self> {
78        let dir = tempfile::tempdir()?;
79        let path = db_dir(Some(dir.path().to_path_buf()))?;
80        let mut s = Self::new("password", path)?;
81        s.dir = Some(dir);
82        Ok(s)
83    }
84
85    #[cfg(feature = "test-utils")]
86    /// Insert an existing key into the wallet.
87    /// Warning this is for testing only.
88    pub fn insert_key(&mut self, name: &str, key: Key) -> anyhow::Result<()> {
89        match key {
90            Key::Secp256k1(private_key) => {
91                self.store
92                    .set_secret(name, Scheme::Secp256k1, private_key.as_ref().as_slice())
93            }
94            Key::Ed25519(_) => todo!("Not supported yet"),
95        }
96    }
97
98    #[cfg(feature = "test-utils")]
99    /// Generate a private key.
100    /// Warning this is for testing only.
101    pub fn generate_private_key(&mut self, scheme: Scheme) -> anyhow::Result<Key> {
102        match scheme {
103            Scheme::Secp256k1 => {
104                let mut rng = rand::rngs::StdRng::from_entropy();
105                let (private_key, _) = secp256k1::generate_keypair(&mut rng);
106                Ok(Key::Secp256k1(private_key))
107            }
108            Scheme::Ed25519 => todo!("Not supported yet"),
109        }
110    }
111
112    /// Create a new key pair.
113    /// The key pair will be stored in the OS self.store.
114    /// The key will be stored at the name provided.
115    /// The scheme determines which signature scheme to use.
116    pub fn new_key_pair(&mut self, name: &str, scheme: Scheme) -> anyhow::Result<()> {
117        match scheme {
118            Scheme::Secp256k1 => {
119                let mut rng = rand::rngs::StdRng::from_entropy();
120                let (private_key, _) = secp256k1::generate_keypair(&mut rng);
121                self.store
122                    .set_secret(name, scheme, private_key.as_ref().as_slice())
123            }
124            Scheme::Ed25519 => todo!("Not supported yet"),
125        }
126    }
127
128    /// Delete a key pair at this name.
129    pub fn delete_key_pair(&mut self, name: &str) -> anyhow::Result<()> {
130        self.store.delete_secret(name)
131    }
132
133    /// List all names for key pairs stored in the OS self.store for this service.
134    pub fn list_names(&mut self) -> anyhow::Result<Vec<String>> {
135        Ok(self.list()?.into_iter().map(|(n, _)| n).collect())
136    }
137
138    /// Get the public key for this key pair.
139    pub fn get_public_key(&mut self, name: &str) -> anyhow::Result<PublicKey> {
140        let key = self.name_to_key(name)?;
141        Ok(essential_signer::public_key(&key))
142    }
143
144    /// Get the private key for this key pair.
145    pub fn get_private_key(&mut self, name: &str) -> anyhow::Result<Key> {
146        self.name_to_key(name)
147    }
148
149    /// Sign an contract.
150    ///
151    /// Requires the keypair be a secp256k1 key or this will return an error.
152    /// No padding is applied to the data before signing.
153    /// This is designed to be used for deploying contracts to the api.
154    pub fn sign_contract(
155        &mut self,
156        data: Contract,
157        name: &str,
158    ) -> anyhow::Result<essential_types::contract::SignedContract> {
159        let key = self.name_to_key(name)?;
160        match key {
161            Key::Secp256k1(key) => Ok(essential_sign::contract::sign(data, &key)),
162            Key::Ed25519(_) => Err(anyhow::anyhow!(
163                "Ed25519 not supported for signing contracts. Please use a Secp256k1 key"
164            ))?,
165        }
166    }
167
168    /// Create a signature using the key pair stored at this name.
169    ///
170    /// The data will be serialized as postcard, then hashed and the hash signed.
171    /// No padding is applied to the data before signing.
172    pub fn sign_postcard<T: Serialize>(
173        &mut self,
174        data: &T,
175        name: &str,
176    ) -> anyhow::Result<Signature> {
177        let key = self.name_to_key(name)?;
178        essential_signer::sign_postcard(data, &key)
179    }
180
181    /// Create a signature using the key pair stored at this name.
182    ///
183    /// The data will be serialized as postcard, then padded to be word aligned,
184    /// then hashed and the hash signed.
185    pub fn sign_postcard_with_padding<T: Serialize>(
186        &mut self,
187        data: &T,
188        padding: Padding,
189        name: &str,
190    ) -> anyhow::Result<Signature> {
191        let key = self.name_to_key(name)?;
192        essential_signer::sign_postcard_with_padding(data, padding, &key)
193    }
194
195    /// Create a signature using the key pair stored at this name.
196    pub fn sign_hash(&mut self, data: Hash, name: &str) -> anyhow::Result<Signature> {
197        let key = self.name_to_key(name)?;
198        essential_signer::sign_hash(data, &key)
199    }
200
201    /// Create a signature using the key pair stored at this name.
202    pub fn sign_words(&mut self, data: &[Word], name: &str) -> anyhow::Result<Signature> {
203        let key = self.name_to_key(name)?;
204        essential_signer::sign_words(data, &key)
205    }
206
207    /// Create a signature using the key pair stored at this name.
208    ///
209    /// The data will be padded to be word aligned, then hashed and the hash signed.
210    pub fn sign_bytes_with_padding(
211        &mut self,
212        data: Vec<u8>,
213        padding: Padding,
214        name: &str,
215    ) -> anyhow::Result<Signature> {
216        let key = self.name_to_key(name)?;
217        essential_signer::sign_bytes_with_padding(data, padding, &key)
218    }
219
220    /// Create a signature using the key pair stored at this name.
221    ///
222    /// The data will be hashed and the hash signed.
223    /// This will return an error if the data is not word aligned.
224    pub fn sign_aligned_bytes(&mut self, data: &[u8], name: &str) -> anyhow::Result<Signature> {
225        let key = self.name_to_key(name)?;
226        essential_signer::sign_aligned_bytes(data, &key)
227    }
228
229    /// Create a signature using the key pair stored at this name.
230    ///
231    /// The data will be hashed and the hash signed.
232    /// Word alignment is not checked.
233    pub fn sign_bytes_unchecked(&mut self, data: &[u8], name: &str) -> anyhow::Result<Signature> {
234        let key = self.name_to_key(name)?;
235        essential_signer::sign_bytes_unchecked(data, &key)
236    }
237
238    fn name_to_key(&mut self, name: &str) -> anyhow::Result<Key> {
239        let (private_key, scheme) = self.store.get_secret(name)?;
240        match scheme {
241            Scheme::Secp256k1 => {
242                let private_key = secp256k1::SecretKey::from_slice(private_key.as_slice())?;
243                Ok(Key::Secp256k1(private_key))
244            }
245            Scheme::Ed25519 => todo!("Not supported yet"),
246        }
247    }
248
249    /// List all accounts under this service.
250    fn list(&mut self) -> anyhow::Result<Vec<(String, Scheme)>> {
251        self.store.list()
252    }
253}
254
255impl Display for Scheme {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        match self {
258            Scheme::Secp256k1 => write!(f, "secp256k1"),
259            Scheme::Ed25519 => write!(f, "ed25519"),
260        }
261    }
262}
263
264impl FromStr for Scheme {
265    type Err = anyhow::Error;
266
267    fn from_str(s: &str) -> Result<Self, Self::Err> {
268        match s {
269            "secp256k1" => Ok(Scheme::Secp256k1),
270            "ed25519" => Ok(Scheme::Ed25519),
271            _ => Err(anyhow::anyhow!("Unknown scheme: {}", s)),
272        }
273    }
274}
275
276fn db_dir(in_path: Option<PathBuf>) -> anyhow::Result<PathBuf> {
277    let path = match in_path {
278        None => {
279            let mut path = dirs::home_dir().unwrap_or_else(|| {
280                dirs::document_dir().unwrap_or_else(|| {
281                    dirs::data_local_dir()
282                        .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR").to_string()))
283                })
284            });
285            path.push(format!(".{}", NAME));
286            path
287        }
288        Some(path) => path,
289    };
290
291    if !path.is_dir() {
292        std::fs::create_dir_all(&path)?;
293    }
294    Ok(path)
295}