From 0a21040e08bb75a90181e33214c901eae9b0d54f Mon Sep 17 00:00:00 2001 From: Daniel Karzel Date: Fri, 8 Jan 2021 12:04:48 +1100 Subject: [PATCH] Deterministic peer id from seed for alice This includes the introduction of the --data-dir parameter instead of the --database. Both the seed file and the database are stored in the data-dir, the database in sub-folder `database`. --- Cargo.lock | 12 ++ swap/Cargo.toml | 1 + swap/src/cli.rs | 4 +- swap/src/config.rs | 2 + swap/src/config/seed.rs | 196 ++++++++++++++++++ swap/src/fs.rs | 14 ++ swap/src/lib.rs | 2 + swap/src/main.rs | 27 ++- swap/src/network.rs | 36 +++- swap/src/protocol/alice.rs | 32 ++- swap/src/seed.rs | 58 ++++++ swap/tests/happy_path.rs | 2 + swap/tests/happy_path_restart_alice.rs | 5 +- .../happy_path_restart_bob_after_comm.rs | 2 + .../happy_path_restart_bob_before_comm.rs | 2 + swap/tests/punish.rs | 2 + swap/tests/refund_restart_alice.rs | 5 +- swap/tests/testutils/mod.rs | 10 +- 18 files changed, 377 insertions(+), 35 deletions(-) create mode 100644 swap/src/config/seed.rs create mode 100644 swap/src/fs.rs create mode 100644 swap/src/seed.rs diff --git a/Cargo.lock b/Cargo.lock index fcff9e5a..53c8cd69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2212,6 +2212,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "pem" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c220d01f863d13d96ca82359d1e81e64a7c6bf0637bcde7b2349630addf0c6" +dependencies = [ + "base64 0.13.0", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3293,6 +3304,7 @@ dependencies = [ "miniscript", "monero", "monero-harness", + "pem", "port_check", "prettytable-rs", "rand 0.7.3", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index 28e4bf27..028c5796 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -27,6 +27,7 @@ log = { version = "0.4", features = ["serde"] } miniscript = { version = "4", features = ["serde"] } monero = { version = "0.9", features = ["serde_support"] } monero-harness = { path = "../monero-harness" } +pem = "0.8" prettytable-rs = "0.8" rand = "0.7" reqwest = { version = "0.10", default-features = false, features = ["socks"] } diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 4d7f7798..66047dc7 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -7,8 +7,8 @@ use crate::{bitcoin, monero}; #[derive(structopt::StructOpt, Debug)] pub struct Options { // TODO: Default value should points to proper configuration folder in home folder - #[structopt(long = "database", default_value = "./.swap-db/")] - pub db_path: String, + #[structopt(long = "data-dir", default_value = "./.swap-data/")] + pub data_dir: String, #[structopt(subcommand)] pub cmd: Command, diff --git a/swap/src/config.rs b/swap/src/config.rs index bd56630e..2161931b 100644 --- a/swap/src/config.rs +++ b/swap/src/config.rs @@ -1,3 +1,5 @@ +pub mod seed; + use crate::bitcoin::Timelock; use conquer_once::Lazy; use std::time::Duration; diff --git a/swap/src/config/seed.rs b/swap/src/config/seed.rs new file mode 100644 index 00000000..371225e3 --- /dev/null +++ b/swap/src/config/seed.rs @@ -0,0 +1,196 @@ +use crate::{fs::ensure_directory_exists, seed}; +use pem::{encode, Pem}; +use seed::SEED_LENGTH; +use std::{ + ffi::OsStr, + fmt, + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +#[derive(Clone, Copy, PartialEq)] +pub struct Seed(seed::Seed); + +impl Seed { + pub fn random() -> Result { + Ok(Seed(seed::Seed::random()?)) + } + + pub fn from_file_or_generate(data_dir: &PathBuf) -> Result { + let file_path_buf = data_dir.join("seed.pem"); + let file_path = Path::new(&file_path_buf); + + if file_path.exists() { + return Self::from_file(&file_path); + } + + tracing::info!("No seed file found, creating at: {}", file_path.display()); + + let random_seed = Seed::random()?; + random_seed.write_to(file_path.to_path_buf())?; + + Ok(random_seed) + } + + fn from_file(seed_file: D) -> Result + where + D: AsRef, + { + let file = Path::new(&seed_file); + let contents = fs::read_to_string(file)?; + let pem = pem::parse(contents)?; + + tracing::info!("Read in seed from file: {}", file.display()); + + Self::from_pem(pem) + } + + fn from_pem(pem: pem::Pem) -> Result { + if pem.contents.len() != SEED_LENGTH { + Err(Error::IncorrectLength(pem.contents.len())) + } else { + let mut array = [0; SEED_LENGTH]; + for (i, b) in pem.contents.iter().enumerate() { + array[i] = *b; + } + + Ok(Self::from(array)) + } + } + + fn write_to(&self, seed_file: PathBuf) -> Result<(), Error> { + ensure_directory_exists(&seed_file)?; + + let data = (self.0).bytes(); + let pem = Pem { + tag: String::from("SEED"), + contents: data.to_vec(), + }; + + let pem_string = encode(&pem); + + let mut file = File::create(seed_file)?; + file.write_all(pem_string.as_bytes())?; + + Ok(()) + } +} + +impl fmt::Debug for Seed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Seed([*****])") + } +} + +impl fmt::Display for Seed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<[u8; SEED_LENGTH]> for Seed { + fn from(bytes: [u8; 32]) -> Self { + Seed(seed::Seed::from(bytes)) + } +} + +impl From for seed::Seed { + fn from(seed: Seed) -> Self { + seed.0 + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Seed generation: ")] + SeedGeneration(#[from] crate::seed::Error), + #[error("io: ")] + Io(#[from] io::Error), + #[error("PEM parse: ")] + PemParse(#[from] pem::PemError), + #[error("expected 32 bytes of base64 encode, got {0} bytes")] + IncorrectLength(usize), + #[error("RNG: ")] + Rand(#[from] rand::Error), + #[error("no default path")] + NoDefaultPath, +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env::temp_dir; + + #[test] + fn seed_byte_string_must_be_32_bytes_long() { + let _seed = Seed::from(*b"this string is exactly 32 bytes!"); + } + + #[test] + fn seed_from_pem_works() { + let payload: &str = "syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM="; + + // 32 bytes base64 encoded. + let pem_string: &str = "-----BEGIN SEED----- +syl9wSYaruvgxg9P5Q1qkZaq5YkM6GvXkxe+VYrL/XM= +-----END SEED----- +"; + + let want = base64::decode(payload).unwrap(); + let pem = pem::parse(pem_string).unwrap(); + let got = Seed::from_pem(pem).unwrap(); + + assert_eq!((got.0).bytes(), *want); + } + + #[test] + fn seed_from_pem_fails_for_short_seed() { + let short = "-----BEGIN SEED----- +VnZUNFZ4dlY= +-----END SEED----- +"; + let pem = pem::parse(short).unwrap(); + match Seed::from_pem(pem) { + Ok(_) => panic!("should fail for short payload"), + Err(e) => { + match e { + Error::IncorrectLength(_) => {} // pass + _ => panic!("should fail with IncorrectLength error"), + } + } + } + } + + #[test] + #[should_panic] + fn seed_from_pem_fails_for_long_seed() { + let long = "-----BEGIN SEED----- +mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= +mbKANv2qKGmNVg1qtquj6Hx1pFPelpqOfE2JaJJAMEg1FlFhNRNlFlE= +-----END SEED----- +"; + let pem = pem::parse(long).unwrap(); + match Seed::from_pem(pem) { + Ok(_) => panic!("should fail for long payload"), + Err(e) => { + match e { + Error::IncorrectLength(_) => {} // pass + _ => panic!("should fail with IncorrectLength error"), + } + } + } + } + + #[test] + fn round_trip_through_file_write_read() { + let tmpfile = temp_dir().join("seed.pem"); + + let seed = Seed::random().unwrap(); + seed.write_to(tmpfile.clone()) + .expect("Write seed to temp file"); + + let rinsed = Seed::from_file(tmpfile).expect("Read from temp file"); + assert_eq!(seed.0, rinsed.0); + } +} diff --git a/swap/src/fs.rs b/swap/src/fs.rs new file mode 100644 index 00000000..f3dc889b --- /dev/null +++ b/swap/src/fs.rs @@ -0,0 +1,14 @@ +use std::path::Path; + +pub fn ensure_directory_exists(file: &Path) -> Result<(), std::io::Error> { + if let Some(path) = file.parent() { + if !path.exists() { + tracing::info!( + "Parent directory does not exist, creating recursively: {}", + file.display() + ); + return std::fs::create_dir_all(path); + } + } + Ok(()) +} diff --git a/swap/src/lib.rs b/swap/src/lib.rs index c190ab95..c33479a7 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -23,9 +23,11 @@ pub mod bitcoin; pub mod cli; pub mod config; pub mod database; +pub mod fs; pub mod monero; pub mod network; pub mod protocol; +pub mod seed; pub mod trace; pub type Never = std::convert::Infallible; diff --git a/swap/src/main.rs b/swap/src/main.rs index 74db62e3..7cf0a65c 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -24,9 +24,10 @@ use swap::{ cli::{Command, Options, Resume}, config::Config, database::{Database, Swap}, - monero, + monero, network, network::transport::build, protocol::{alice, alice::AliceState, bob, bob::BobState}, + seed::Seed, trace::init_tracing, SwapAmounts, }; @@ -41,12 +42,19 @@ async fn main() -> Result<()> { init_tracing(LevelFilter::Info).expect("initialize tracing"); let opt = Options::from_args(); - let config = Config::testnet(); - info!("Database: {}", opt.db_path); - let db = Database::open(std::path::Path::new(opt.db_path.as_str())) - .context("Could not open database")?; + info!( + "Database and Seed will be stored in directory: {}", + opt.data_dir + ); + let data_dir = std::path::Path::new(opt.data_dir.as_str()).to_path_buf(); + let db = + Database::open(data_dir.join("database").as_path()).context("Could not open database")?; + + let seed = swap::config::seed::Seed::from_file_or_generate(&data_dir) + .expect("Could not retrieve/initialize seed") + .into(); match opt.cmd { Command::SellXmr { @@ -106,6 +114,7 @@ async fn main() -> Result<()> { monero_wallet, config, db, + &seed, ) .await?; } @@ -201,6 +210,7 @@ async fn main() -> Result<()> { monero_wallet, config, db, + &seed, ) .await?; } @@ -267,7 +277,7 @@ async fn setup_wallets( Ok((bitcoin_wallet, monero_wallet)) } - +#[allow(clippy::too_many_arguments)] async fn alice_swap( swap_id: Uuid, state: AliceState, @@ -276,12 +286,11 @@ async fn alice_swap( monero_wallet: Arc, config: Config, db: Database, + seed: &Seed, ) -> Result { - let alice_behaviour = alice::Behaviour::default(); - + let alice_behaviour = alice::Behaviour::new(network::Seed::new(seed.bytes())); let alice_peer_id = alice_behaviour.peer_id(); info!("Own Peer-ID: {}", alice_peer_id); - let alice_transport = build(alice_behaviour.identity())?; let (mut event_loop, handle) = diff --git a/swap/src/network.rs b/swap/src/network.rs index 916ffa5a..0884bea4 100644 --- a/swap/src/network.rs +++ b/swap/src/network.rs @@ -1,5 +1,7 @@ +use crate::seed::SEED_LENGTH; +use bitcoin::hashes::{sha256, Hash, HashEngine}; use futures::prelude::*; -use libp2p::core::Executor; +use libp2p::{core::Executor, identity::ed25519}; use std::pin::Pin; use tokio::runtime::Handle; @@ -17,3 +19,35 @@ impl Executor for TokioExecutor { let _ = self.handle.spawn(future); } } + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct Seed([u8; SEED_LENGTH]); + +impl Seed { + /// prefix "NETWORK" to the provided seed and apply sha256 + pub fn new(seed: [u8; crate::seed::SEED_LENGTH]) -> Self { + let mut engine = sha256::HashEngine::default(); + + engine.input(&seed); + engine.input(b"NETWORK"); + + let hash = sha256::Hash::from_engine(engine); + Self(hash.into_inner()) + } + + pub fn bytes(&self) -> [u8; SEED_LENGTH] { + self.0 + } + + pub fn derive_libp2p_identity(&self) -> libp2p::identity::Keypair { + let mut engine = sha256::HashEngine::default(); + + engine.input(&self.bytes()); + engine.input(b"LIBP2P_IDENTITY"); + + let hash = sha256::Hash::from_engine(engine); + let key = + ed25519::SecretKey::from_bytes(hash.into_inner()).expect("we always pass 32 bytes"); + libp2p::identity::Keypair::Ed25519(key.into()) + } +} diff --git a/swap/src/protocol/alice.rs b/swap/src/protocol/alice.rs index 2ce70bd3..96571299 100644 --- a/swap/src/protocol/alice.rs +++ b/swap/src/protocol/alice.rs @@ -13,7 +13,7 @@ use crate::{ peer_tracker::{self, PeerTracker}, request_response::AliceToBob, transport::SwapTransport, - TokioExecutor, + Seed, TokioExecutor, }, protocol::bob, SwapAmounts, @@ -145,6 +145,20 @@ pub struct Behaviour { } impl Behaviour { + pub fn new(seed: Seed) -> Self { + let identity = seed.derive_libp2p_identity(); + + Self { + pt: PeerTracker::default(), + amounts: Amounts::default(), + message0: message0::Behaviour::default(), + message1: message1::Behaviour::default(), + message2: message2::Behaviour::default(), + message3: message3::Behaviour::default(), + identity, + } + } + pub fn identity(&self) -> Keypair { self.identity.clone() } @@ -178,19 +192,3 @@ impl Behaviour { debug!("Sent Message2"); } } - -impl Default for Behaviour { - fn default() -> Self { - let identity = Keypair::generate_ed25519(); - - Self { - pt: PeerTracker::default(), - amounts: Amounts::default(), - message0: message0::Behaviour::default(), - message1: message1::Behaviour::default(), - message2: message2::Behaviour::default(), - message3: message3::Behaviour::default(), - identity, - } - } -} diff --git a/swap/src/seed.rs b/swap/src/seed.rs new file mode 100644 index 00000000..382f64da --- /dev/null +++ b/swap/src/seed.rs @@ -0,0 +1,58 @@ +use ::bitcoin::secp256k1::{self, constants::SECRET_KEY_SIZE, SecretKey}; +use rand::prelude::*; +use std::fmt; + +pub const SEED_LENGTH: usize = 32; + +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct Seed([u8; SEED_LENGTH]); + +impl Seed { + pub fn random() -> Result { + let mut bytes = [0u8; SECRET_KEY_SIZE]; + rand::thread_rng().fill_bytes(&mut bytes); + + // If it succeeds once, it'll always succeed + let _ = SecretKey::from_slice(&bytes)?; + + Ok(Seed(bytes)) + } + + pub fn bytes(&self) -> [u8; SEED_LENGTH] { + self.0 + } +} + +impl fmt::Debug for Seed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Seed([*****])") + } +} + +impl fmt::Display for Seed { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl From<[u8; SEED_LENGTH]> for Seed { + fn from(bytes: [u8; SEED_LENGTH]) -> Self { + Seed(bytes) + } +} + +#[derive(Debug, Copy, Clone, thiserror::Error)] +pub enum Error { + #[error("Secp256k1: ")] + Secp256k1(#[from] secp256k1::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generate_random_seed() { + let _ = Seed::random().unwrap(); + } +} diff --git a/swap/tests/happy_path.rs b/swap/tests/happy_path.rs index 899e1e41..5eb25ef5 100644 --- a/swap/tests/happy_path.rs +++ b/swap/tests/happy_path.rs @@ -11,6 +11,7 @@ use swap::{ config::Config, monero, protocol::{alice, bob}, + seed::Seed, }; use testcontainers::clients::Cli; use testutils::init_tracing; @@ -65,6 +66,7 @@ async fn happy_path() { xmr_alice, alice_multiaddr.clone(), config, + &Seed::random().unwrap(), ) .await; diff --git a/swap/tests/happy_path_restart_alice.rs b/swap/tests/happy_path_restart_alice.rs index aaf6b4bf..84ef611a 100644 --- a/swap/tests/happy_path_restart_alice.rs +++ b/swap/tests/happy_path_restart_alice.rs @@ -8,6 +8,7 @@ use swap::{ database::Database, monero, protocol::{alice, alice::AliceState, bob}, + seed::Seed, }; use tempfile::tempdir; use testcontainers::clients::Cli; @@ -42,6 +43,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { let config = Config::regtest(); + let alice_seed = Seed::random().unwrap(); let ( start_state, mut alice_event_loop, @@ -57,6 +59,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { alice_xmr_starting_balance, alice_multiaddr.clone(), config, + &alice_seed, ) .await; @@ -125,7 +128,7 @@ async fn given_alice_restarts_after_encsig_is_learned_resume_swap() { }; let (mut event_loop_after_restart, event_loop_handle_after_restart) = - testutils::init_alice_event_loop(alice_multiaddr); + testutils::init_alice_event_loop(alice_multiaddr, &alice_seed); tokio::spawn(async move { event_loop_after_restart.run().await }); let alice_state = alice::swap::swap( diff --git a/swap/tests/happy_path_restart_bob_after_comm.rs b/swap/tests/happy_path_restart_bob_after_comm.rs index 94705cba..09042086 100644 --- a/swap/tests/happy_path_restart_bob_after_comm.rs +++ b/swap/tests/happy_path_restart_bob_after_comm.rs @@ -8,6 +8,7 @@ use swap::{ database::Database, monero, protocol::{alice, bob, bob::BobState}, + seed::Seed, }; use tempfile::tempdir; use testcontainers::clients::Cli; @@ -57,6 +58,7 @@ async fn given_bob_restarts_after_encsig_is_sent_resume_swap() { alice_xmr_starting_balance, alice_multiaddr.clone(), config, + &Seed::random().unwrap(), ) .await; diff --git a/swap/tests/happy_path_restart_bob_before_comm.rs b/swap/tests/happy_path_restart_bob_before_comm.rs index 8f8e2510..c8fb93d0 100644 --- a/swap/tests/happy_path_restart_bob_before_comm.rs +++ b/swap/tests/happy_path_restart_bob_before_comm.rs @@ -8,6 +8,7 @@ use swap::{ database::Database, monero, protocol::{alice, alice::AliceState, bob, bob::BobState}, + seed::Seed, }; use tempfile::tempdir; use testcontainers::clients::Cli; @@ -59,6 +60,7 @@ async fn given_bob_restarts_after_xmr_is_locked_resume_swap() { alice_xmr_starting_balance, alice_multiaddr.clone(), Config::regtest(), + &Seed::random().unwrap(), ) .await; diff --git a/swap/tests/punish.rs b/swap/tests/punish.rs index badd8bc4..276f9c63 100644 --- a/swap/tests/punish.rs +++ b/swap/tests/punish.rs @@ -11,6 +11,7 @@ use swap::{ config::Config, monero, protocol::{alice, alice::AliceState, bob, bob::BobState}, + seed::Seed, }; use testcontainers::clients::Cli; use testutils::init_tracing; @@ -63,6 +64,7 @@ async fn alice_punishes_if_bob_never_acts_after_fund() { alice_xmr_starting_balance, alice_multiaddr.clone(), config, + &Seed::random().unwrap(), ) .await; diff --git a/swap/tests/refund_restart_alice.rs b/swap/tests/refund_restart_alice.rs index dbc14862..4f2b3170 100644 --- a/swap/tests/refund_restart_alice.rs +++ b/swap/tests/refund_restart_alice.rs @@ -9,6 +9,7 @@ use swap::{ database::Database, monero, protocol::{alice, alice::AliceState, bob, bob::BobState}, + seed::Seed, }; use tempfile::tempdir; use testcontainers::clients::Cli; @@ -47,6 +48,7 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { .parse() .expect("failed to parse Alice's address"); + let alice_seed = Seed::random().unwrap(); let ( alice_state, mut alice_event_loop_1, @@ -62,6 +64,7 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { alice_xmr_starting_balance, alice_multiaddr.clone(), Config::regtest(), + &alice_seed, ) .await; @@ -121,7 +124,7 @@ async fn given_alice_restarts_after_xmr_is_locked_abort_swap() { }; let (mut alice_event_loop_2, alice_event_loop_handle_2) = - testutils::init_alice_event_loop(alice_multiaddr); + testutils::init_alice_event_loop(alice_multiaddr, &alice_seed); let alice_final_state = { let alice_db = Database::open(alice_db_datadir.path()).unwrap(); diff --git a/swap/tests/testutils/mod.rs b/swap/tests/testutils/mod.rs index 7eb0693d..8fff163d 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -7,9 +7,10 @@ use swap::{ bitcoin, config::Config, database::Database, - monero, + monero, network, network::transport::build, protocol::{alice, alice::AliceState, bob, bob::BobState}, + seed::Seed, SwapAmounts, }; use tempfile::tempdir; @@ -106,13 +107,13 @@ pub async fn init_alice_state( pub fn init_alice_event_loop( listen: Multiaddr, + seed: &Seed, ) -> ( alice::event_loop::EventLoop, alice::event_loop::EventLoopHandle, ) { - let alice_behaviour = alice::Behaviour::default(); + let alice_behaviour = alice::Behaviour::new(network::Seed::new(seed.bytes())); let alice_transport = build(alice_behaviour.identity()).unwrap(); - alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap() } @@ -125,6 +126,7 @@ pub async fn init_alice( xmr_starting_balance: monero::Amount, listen: Multiaddr, config: Config, + seed: &Seed, ) -> ( AliceState, alice::event_loop::EventLoop, @@ -146,7 +148,7 @@ pub async fn init_alice( let alice_start_state = init_alice_state(btc_to_swap, xmr_to_swap, alice_btc_wallet.clone(), config).await; - let (event_loop, event_loop_handle) = init_alice_event_loop(listen); + let (event_loop, event_loop_handle) = init_alice_event_loop(listen, seed); let alice_db_datadir = tempdir().unwrap(); let alice_db = Database::open(alice_db_datadir.path()).unwrap();