signer: implement mnemonics storage, and initialization from storage
This commit is contained in:
parent
d341b6dea9
commit
59e55ae9f2
155
src/signer.rs
155
src/signer.rs
@ -5,10 +5,16 @@
|
||||
|
||||
use crate::random;
|
||||
|
||||
use std::{convert::TryInto, error, fmt, str::FromStr};
|
||||
use std::{
|
||||
convert::TryInto,
|
||||
error, fmt, fs,
|
||||
io::{self, Write},
|
||||
path,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use miniscript::bitcoin::{
|
||||
self,
|
||||
self, secp256k1,
|
||||
util::bip32::{self, Error as Bip32Error},
|
||||
};
|
||||
|
||||
@ -18,6 +24,7 @@ pub enum SignerError {
|
||||
Randomness(random::RandomnessError),
|
||||
Mnemonic(bip39::Error),
|
||||
Bip32(Bip32Error),
|
||||
MnemonicStorage(io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for SignerError {
|
||||
@ -26,12 +33,15 @@ impl fmt::Display for SignerError {
|
||||
Self::Randomness(s) => write!(f, "Error related to getting randomness: {}", s),
|
||||
Self::Mnemonic(s) => write!(f, "Error when working with mnemonics: {}", s),
|
||||
Self::Bip32(e) => write!(f, "BIP32 error: {}", e),
|
||||
Self::MnemonicStorage(e) => write!(f, "BIP39 mnemonic storage error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for SignerError {}
|
||||
|
||||
pub const MNEMONICS_FOLDER_NAME: &str = "mnemonics";
|
||||
|
||||
// TODO: zeroize, mlock, etc.. For now we don't even encrypt the seed on disk so that'd be
|
||||
// overkill.
|
||||
/// A signer that keeps the key on the laptop. Based on BIP39.
|
||||
@ -40,6 +50,43 @@ pub struct HotSigner {
|
||||
master_xpriv: bip32::ExtendedPrivKey,
|
||||
}
|
||||
|
||||
// TODO: instead of copying them here we could have a util module with those helpers.
|
||||
// Create a directory with no permission for group and other users.
|
||||
fn create_dir(path: &path::Path) -> io::Result<()> {
|
||||
#[cfg(unix)]
|
||||
return {
|
||||
use fs::DirBuilder;
|
||||
use std::os::unix::fs::DirBuilderExt;
|
||||
|
||||
let mut builder = DirBuilder::new();
|
||||
builder.mode(0o700).recursive(true).create(path)
|
||||
};
|
||||
|
||||
// TODO: permissions on Windows..
|
||||
#[cfg(not(unix))]
|
||||
return { fs::create_dir_all(path) };
|
||||
}
|
||||
|
||||
// Create a file with no permission for the group and other users, and only read permissions for
|
||||
// the current user.
|
||||
fn create_file(path: &path::Path) -> Result<fs::File, std::io::Error> {
|
||||
let mut options = fs::OpenOptions::new();
|
||||
let options = options.read(true).write(true).create_new(true);
|
||||
|
||||
#[cfg(unix)]
|
||||
return {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
options.mode(0o400).open(path)
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
return {
|
||||
// TODO: permissions for Windows...
|
||||
options.open(path)
|
||||
};
|
||||
}
|
||||
|
||||
impl HotSigner {
|
||||
fn from_mnemonic(
|
||||
network: bitcoin::Network,
|
||||
@ -67,16 +114,90 @@ impl HotSigner {
|
||||
Self::from_mnemonic(network, mnemonic)
|
||||
}
|
||||
|
||||
fn mnemonics_folder(datadir_root: &path::Path, network: bitcoin::Network) -> path::PathBuf {
|
||||
[
|
||||
datadir_root,
|
||||
path::Path::new(&network.to_string()),
|
||||
path::Path::new(MNEMONICS_FOLDER_NAME),
|
||||
]
|
||||
.iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Read all the mnemonics from the datadir for the given network.
|
||||
pub fn from_datadir(
|
||||
datadir_root: &path::Path,
|
||||
network: bitcoin::Network,
|
||||
) -> Result<Vec<Self>, SignerError> {
|
||||
let mut signers = Vec::new();
|
||||
|
||||
let mnemonic_paths = fs::read_dir(Self::mnemonics_folder(datadir_root, network))
|
||||
.map_err(SignerError::MnemonicStorage)?;
|
||||
for entry in mnemonic_paths {
|
||||
let mnemonic = fs::read_to_string(entry.map_err(SignerError::MnemonicStorage)?.path())
|
||||
.map_err(SignerError::MnemonicStorage)?;
|
||||
signers.push(Self::from_str(network, &mnemonic)?);
|
||||
}
|
||||
|
||||
Ok(signers)
|
||||
}
|
||||
|
||||
/// The BIP39 mnemonics from which the master key of this signer is derived.
|
||||
pub fn words(&self) -> [&'static str; 12] {
|
||||
let words: Vec<&'static str> = self.mnemonic.word_iter().collect();
|
||||
words.try_into().expect("Always 12 words")
|
||||
}
|
||||
|
||||
/// The BIP39 mnemonic words as a string.
|
||||
pub fn mnemonic_str(&self) -> String {
|
||||
let mut mnemonic_str = String::with_capacity(12 * 7);
|
||||
let words = self.words();
|
||||
|
||||
for (i, word) in words.iter().enumerate() {
|
||||
mnemonic_str += word;
|
||||
if i < words.len() - 1 {
|
||||
mnemonic_str += " ";
|
||||
}
|
||||
}
|
||||
|
||||
mnemonic_str
|
||||
}
|
||||
|
||||
/// Store the mnemonic in a file within the given "data directory".
|
||||
/// The file is stored within a "mnemonics" folder, with the filename set to the fingerprint of
|
||||
/// the master xpub corresponding to this mnemonic.
|
||||
pub fn store(
|
||||
&self,
|
||||
datadir_root: &path::Path,
|
||||
network: bitcoin::Network,
|
||||
secp: &secp256k1::Secp256k1<impl secp256k1::Signing>,
|
||||
) -> Result<(), SignerError> {
|
||||
let mut mnemonics_folder = Self::mnemonics_folder(datadir_root, network);
|
||||
if !mnemonics_folder.exists() {
|
||||
create_dir(&mnemonics_folder).map_err(SignerError::MnemonicStorage)?;
|
||||
}
|
||||
|
||||
// This will fail if a file with this fingerprint exists already.
|
||||
mnemonics_folder.push(format!(
|
||||
"mnemonic-{:x}.txt",
|
||||
self.master_xpriv.fingerprint(secp)
|
||||
));
|
||||
let mnemonic_path = mnemonics_folder;
|
||||
let mut mnemonic_file =
|
||||
create_file(&mnemonic_path).map_err(SignerError::MnemonicStorage)?;
|
||||
mnemonic_file
|
||||
.write_all(self.mnemonic_str().as_bytes())
|
||||
.map_err(SignerError::MnemonicStorage)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::testutils::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn hot_signer_gen() {
|
||||
@ -92,11 +213,7 @@ mod tests {
|
||||
|
||||
// Roundtrips.
|
||||
let signer = HotSigner::generate(bitcoin::Network::Bitcoin).unwrap();
|
||||
let mnemonics_str = signer.words().iter().fold(String::new(), |mut s, w| {
|
||||
s += w;
|
||||
s += " ";
|
||||
s
|
||||
});
|
||||
let mnemonics_str = signer.mnemonic_str();
|
||||
assert_eq!(
|
||||
HotSigner::from_str(bitcoin::Network::Bitcoin, &mnemonics_str)
|
||||
.unwrap()
|
||||
@ -104,4 +221,28 @@ mod tests {
|
||||
signer.words()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hot_signer_storage() {
|
||||
let secp = secp256k1::Secp256k1::signing_only();
|
||||
let tmp_dir = tmp_dir();
|
||||
fs::create_dir_all(&tmp_dir).unwrap();
|
||||
let network = bitcoin::Network::Bitcoin;
|
||||
|
||||
let words_set: HashSet<_> = (0..10)
|
||||
.map(|_| {
|
||||
let signer = HotSigner::generate(network).unwrap();
|
||||
signer.store(&tmp_dir, network, &secp).unwrap();
|
||||
signer.words()
|
||||
})
|
||||
.collect();
|
||||
let words_read: HashSet<_> = HotSigner::from_datadir(&tmp_dir, network)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|signer| signer.words())
|
||||
.collect();
|
||||
assert_eq!(words_set, words_read);
|
||||
|
||||
fs::remove_dir_all(tmp_dir).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user