signer: implement mnemonics storage, and initialization from storage

This commit is contained in:
Antoine Poinsot 2023-01-19 17:29:31 +01:00
parent d341b6dea9
commit 59e55ae9f2
No known key found for this signature in database
GPG Key ID: E13FC145CD3F4304

View File

@ -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();
}
}