diff --git a/src/config.rs b/src/config.rs index 23cc1cf6d..2f26005ab 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use libp2p::{Multiaddr, PeerId}; use libp2p::multiaddr::Protocol; -use libp2p::identity::Keypair; +use libp2p::identity::{Keypair, PublicKey}; use rand::{Rng, rngs::EntropyRng}; use serde_derive::{Serialize, Deserialize}; use std::fs; @@ -18,39 +18,139 @@ const BOOTSTRAP_NODES: &[&'static str] = &[ "/ip6/2a03:b0c0:0:1010::23:1001/tcp/4001/p2p/QmSoLer265NRgSp2LA3dPaeykiS1J6DifTC88f5uVQKNAd", ]; +/// See test cases for examples how to write such file. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConfigFile { - private_key: [u8; 32], + #[serde(flatten)] + key: KeyMaterial, bootstrap: Vec, } +/// KeyMaterial is an additional abstraction as the identity::Keypair does not support Clone or +/// Debug nor is there a clearcut way to serialize and deserialize such yet at least. +#[derive(Clone, Serialize, Deserialize)] +#[serde(untagged)] +enum KeyMaterial { + Ed25519 { + private_key: [u8; 32], + #[serde(skip)] + keypair: Option, + }, + RsaPkcs8File { + #[serde(rename = "rsa_pkcs8_filename")] + filename: String, + #[serde(skip)] + keypair: Option, + }, +} + +use std::fmt; + +impl fmt::Debug for KeyMaterial { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match *self { + KeyMaterial::Ed25519 { ref keypair, .. } => + if let Some(kp) = keypair.as_ref() { + write!(fmt, "{:?}", kp) + } else { + write!(fmt, "Ed25519(not loaded)") + }, + KeyMaterial::RsaPkcs8File { ref keypair, ref filename } => + if let Some(kp) = keypair.as_ref() { + write!(fmt, "{:?}", kp.public()) + } else { + write!(fmt, "Rsa(not loaded: {:?})", filename) + }, + } + } +} + +#[derive(Debug)] +enum KeyMaterialLoadingFailure { + Io(std::io::Error), + RsaDecoding(libp2p::identity::error::DecodingError), +} + +impl KeyMaterial { + + fn clone_keypair(&self) -> Keypair { + match *self { + KeyMaterial::Ed25519 { ref keypair, .. } => keypair.clone().map(Keypair::Ed25519), + KeyMaterial::RsaPkcs8File { ref keypair, .. } => keypair.clone().map(Keypair::Rsa), + }.expect("KeyMaterial needs to be loaded before accessing the keypair") + } + + fn public(&self) -> PublicKey { + self.clone_keypair().public() + } + + fn load(&mut self) -> Result<(), KeyMaterialLoadingFailure> { + match self { + &mut KeyMaterial::Ed25519 { ref private_key, ref mut keypair } if keypair.is_none() => { + let mut cloned = private_key.clone(); + let sk = libp2p::identity::ed25519::SecretKey::from_bytes(&mut cloned) + .expect("Failed to extract ed25519::SecretKey"); + + let kp = libp2p::identity::ed25519::Keypair::from(sk); + + *keypair = Some(kp); + }, + &mut KeyMaterial::RsaPkcs8File { ref filename, ref mut keypair } if keypair.is_none() => { + let mut bytes = std::fs::read(filename) + .map_err(|e| KeyMaterialLoadingFailure::Io(e))?; + let kp = libp2p::identity::rsa::Keypair::from_pkcs8(&mut bytes) + .map_err(|e| KeyMaterialLoadingFailure::RsaDecoding(e))?; + *keypair = Some(kp); + } + _ => { /* all set */ } + } + + Ok(()) + } + + fn into_loaded(mut self) -> Result { + self.load()?; + Ok(self) + } +} + impl ConfigFile { pub fn new>(path: P) -> Self { - fs::read_to_string(&path).map(|content| { - serde_json::from_str(&content).unwrap() - }).unwrap_or_else(|_| { - let config = ConfigFile::default(); - let string = serde_json::to_string_pretty(&config).unwrap(); - fs::write(path, string).unwrap(); - config - }) + fs::read_to_string(&path) + .map(|content| Self::parse(&content)) + .map(|parsed| parsed.into_loaded().unwrap()) + .unwrap_or_else(|_| { + let config = ConfigFile::default(); + config.store_at(path).unwrap(); + + config.into_loaded().unwrap() + }) } - pub fn secio_key_pair(&self) -> Keypair { - // FIXME: the keypair should be extract at load time and only cloned here - // there could also some (de)serialization support in libp2p like there is for rsa and - // secp256k1? - let mut cloned = self.private_key.clone(); - let sk = libp2p::identity::ed25519::SecretKey::from_bytes(&mut cloned) - .expect("Failed to extract ed25519::SecretKey"); + fn load(&mut self) -> Result<(), KeyMaterialLoadingFailure> { + self.key.load() + } + + fn into_loaded(mut self) -> Result { + self.load()?; + Ok(self) + } + + fn parse(s: &str) -> Self { + serde_json::from_str(s).unwrap() + } - let kp = libp2p::identity::ed25519::Keypair::from(sk); + pub fn store_at>(&self, path: P) -> std::io::Result<()> { + let string = serde_json::to_string_pretty(self).unwrap(); + fs::write(path, string) + } - Keypair::Ed25519(kp) + pub fn secio_key_pair(&self) -> Keypair { + self.key.clone_keypair() } pub fn peer_id(&self) -> PeerId { - self.secio_key_pair().public().into_peer_id() + self.key.public().into_peer_id() } pub fn bootstrap(&self) -> Vec<(Multiaddr, PeerId)> { @@ -69,13 +169,48 @@ impl ConfigFile { impl Default for ConfigFile { fn default() -> Self { + // the ed25519 has no chance of working with go-ipfs as of now because of + // https://github.com/libp2p/specs/issues/138 and + // https://github.com/libp2p/go-libp2p-core/blob/dc718fa4dab1866476fd9f379718fdd619455a4f/peer/peer.go#L23-L34 + // + // and on the other hand: + // https://github.com/libp2p/rust-libp2p/blob/eb7b7bd919b93e6acf00847c19d1a76c09016120/core/src/peer_id.rs#L62-L74 let private_key: [u8; 32] = EntropyRng::new().gen(); + let bootstrap = BOOTSTRAP_NODES.iter().map(|node| { node.parse().unwrap() }).collect(); ConfigFile { - private_key, + key: KeyMaterial::Ed25519 { private_key, keypair: None }.into_loaded().unwrap(), bootstrap, } } } + +#[cfg(test)] +mod tests { + + use super::ConfigFile; + + #[test] + fn supports_older_v1_and_ed25519_v2() { + let input = r#"{"private_key":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"bootstrap":[]}"#; + + let actual = ConfigFile::parse(input); + + let roundtrip = serde_json::to_string(&actual).unwrap(); + + assert_eq!(input, roundtrip); + } + + #[test] + fn supports_v2() { + let input = r#"{"rsa_pkcs8_filename":"foobar.pk8","bootstrap":[]}"#; + + let actual = ConfigFile::parse(input); + + let roundtrip = serde_json::to_string(&actual).unwrap(); + + assert_eq!(input, roundtrip); + } +}