From 0e09c08af501c21d6342b188a17f0843bf66cb8c 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 | 13 +++ swap/Cargo.toml | 2 + swap/src/alice.rs | 6 +- swap/src/cli.rs | 4 +- swap/src/config.rs | 3 + swap/src/config/seed.rs | 196 ++++++++++++++++++++++++++++++++++++ swap/src/fs.rs | 14 +++ swap/src/lib.rs | 3 + swap/src/main.rs | 29 ++++-- swap/src/network.rs | 36 ++++++- swap/src/seed.rs | 58 +++++++++++ swap/tests/testutils/mod.rs | 6 +- 12 files changed, 353 insertions(+), 17 deletions(-) create mode 100644 swap/src/config.rs 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 620b74b2..b73b4d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2242,6 +2242,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" @@ -3346,6 +3357,7 @@ dependencies = [ "log", "monero", "monero-harness", + "pem", "port_check", "prettytable-rs", "rand 0.7.3", @@ -3361,6 +3373,7 @@ dependencies = [ "strum", "tempfile", "testcontainers", + "thiserror", "time", "tokio", "tracing", diff --git a/swap/Cargo.toml b/swap/Cargo.toml index c4119c9d..a2441aae 100644 --- a/swap/Cargo.toml +++ b/swap/Cargo.toml @@ -24,6 +24,7 @@ libp2p-tokio-socks5 = "0.4" log = { version = "0.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"] } @@ -36,6 +37,7 @@ sled = "0.34" structopt = "0.3" strum = { version = "0.20", features = ["derive"] } tempfile = "3" +thiserror = "1" time = "0.2" tokio = { version = "0.2", features = ["rt-threaded", "time", "macros", "sync"] } tracing = { version = "0.1", features = ["attributes"] } diff --git a/swap/src/alice.rs b/swap/src/alice.rs index 903d22da..3e409fed 100644 --- a/swap/src/alice.rs +++ b/swap/src/alice.rs @@ -6,7 +6,7 @@ use crate::{ peer_tracker::{self, PeerTracker}, request_response::AliceToBob, transport::SwapTransport, - TokioExecutor, + Seed, TokioExecutor, }, SwapAmounts, }; @@ -142,8 +142,8 @@ pub struct Behaviour { } impl Behaviour { - pub fn identity(&self) -> Keypair { - self.identity.clone() + pub fn identity(&self, seed: Seed) -> Keypair { + seed.derive_libp2p_identity() } pub fn peer_id(&self) -> PeerId { diff --git a/swap/src/cli.rs b/swap/src/cli.rs index 6e1412f4..e406ef8d 100644 --- a/swap/src/cli.rs +++ b/swap/src/cli.rs @@ -5,8 +5,8 @@ use uuid::Uuid; #[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 new file mode 100644 index 00000000..bb9401b9 --- /dev/null +++ b/swap/src/config.rs @@ -0,0 +1,3 @@ +mod seed; + +pub use self::seed::Seed; 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 04a66898..64e05d17 100644 --- a/swap/src/lib.rs +++ b/swap/src/lib.rs @@ -20,9 +20,12 @@ pub mod alice; pub mod bitcoin; pub mod bob; pub mod cli; +pub mod config; pub mod database; +pub mod fs; pub mod monero; pub mod network; +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 ff48cf08..c2edb55f 100644 --- a/swap/src/main.rs +++ b/swap/src/main.rs @@ -26,8 +26,9 @@ use swap::{ bob::swap::BobState, cli::{Command, Options, Resume}, database::{Database, Swap}, - monero, + monero, network, network::transport::build, + seed::Seed, trace::init_tracing, SwapAmounts, }; @@ -43,12 +44,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::from_file_or_generate(&data_dir) + .expect("Could not retrieve/initialize seed") + .into(); match opt.cmd { Command::SellXmr { @@ -108,6 +116,7 @@ async fn main() -> Result<()> { monero_wallet, config, db, + &seed, ) .await?; } @@ -204,6 +213,7 @@ async fn main() -> Result<()> { monero_wallet, config, db, + &seed, ) .await?; } @@ -270,7 +280,7 @@ async fn setup_wallets( Ok((bitcoin_wallet, monero_wallet)) } - +#[allow(clippy::too_many_arguments)] async fn alice_swap( swap_id: Uuid, state: AliceState, @@ -279,13 +289,14 @@ async fn alice_swap( monero_wallet: Arc, config: Config, db: Database, + seed: &Seed, ) -> Result { let alice_behaviour = alice::Behaviour::default(); - let alice_peer_id = alice_behaviour.peer_id(); + let identity = alice_behaviour.identity(network::Seed::new(seed.bytes())); + let alice_peer_id = PeerId::from(identity.public()); info!("Own Peer-ID: {}", alice_peer_id); - - let alice_transport = build(alice_behaviour.identity())?; + let alice_transport = build(identity)?; let (mut event_loop, handle) = alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen_addr)?; 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/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/testutils/mod.rs b/swap/tests/testutils/mod.rs index bff01109..562b21d7 100644 --- a/swap/tests/testutils/mod.rs +++ b/swap/tests/testutils/mod.rs @@ -5,7 +5,7 @@ use rand::rngs::OsRng; use std::sync::Arc; use swap::{ alice, alice::swap::AliceState, bitcoin, bob, bob::swap::BobState, database::Database, monero, - network::transport::build, SwapAmounts, + network, network::transport::build, seed::Seed, SwapAmounts, }; use tempfile::tempdir; use testcontainers::{clients::Cli, Container}; @@ -106,8 +106,10 @@ pub fn init_alice_event_loop( alice::event_loop::EventLoop, alice::event_loop::EventLoopHandle, ) { + let seed = Seed::random().unwrap(); let alice_behaviour = alice::Behaviour::default(); - let alice_transport = build(alice_behaviour.identity()).unwrap(); + let alice_transport = + build(alice_behaviour.identity(network::Seed::new(seed.bytes()))).unwrap(); alice::event_loop::EventLoop::new(alice_transport, alice_behaviour, listen).unwrap() }