// Copyright 2020 Parity Technologies (UK) Ltd. // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. //! The `pnet` protocol implements *Pre-shared Key Based Private Networks in libp2p*, //! as specified in [the spec](https://github.com/libp2p/specs/blob/master/pnet/Private-Networks-PSK-V1.md) //! //! Libp2p nodes configured with a pre-shared key can only communicate with other nodes with //! the same key. mod crypt_writer; use crypt_writer::CryptWriter; use futures::prelude::*; use log::trace; use pin_project::pin_project; use rand::RngCore; use salsa20::{ stream_cipher::{NewStreamCipher, SyncStreamCipher}, Salsa20, XSalsa20, }; use sha3::{digest::ExtendableOutput, Shake128}; use std::{ error, fmt::{self, Write}, io, io::Error as IoError, num::ParseIntError, pin::Pin, str::FromStr, task::{Context, Poll}, }; const KEY_SIZE: usize = 32; const NONCE_SIZE: usize = 24; const WRITE_BUFFER_SIZE: usize = 1024; const FINGERPRINT_SIZE: usize = 16; /// A pre-shared key, consisting of 32 bytes of random data. #[derive(Copy, Clone, PartialEq, Eq)] pub struct PreSharedKey([u8; KEY_SIZE]); impl PreSharedKey { /// Compute PreSharedKey fingerprint identical to the go-libp2p fingerprint. /// The computation of the fingerprint is not specified in the spec. /// /// This provides a way to check that private keys are properly configured /// without dumping the key itself to the console. pub fn fingerprint(&self) -> Fingerprint { use std::io::{Read, Write}; let mut enc = [0u8; 64]; let nonce: [u8; 8] = *b"finprint"; let mut out = [0u8; 16]; let mut cipher = Salsa20::new(&self.0.into(), &nonce.into()); cipher.apply_keystream(&mut enc); let mut hasher = Shake128::default(); hasher.write_all(&enc).expect("shake128 failed"); hasher.xof_result().read(&mut out).expect("shake128 failed"); Fingerprint(out) } } fn parse_hex_key(s: &str) -> Result<[u8; KEY_SIZE], KeyParseError> { if s.len() == KEY_SIZE * 2 { let mut r = [0u8; KEY_SIZE]; for i in 0..KEY_SIZE { r[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16) .map_err(KeyParseError::InvalidKeyChar)?; } Ok(r) } else { Err(KeyParseError::InvalidKeyLength) } } fn to_hex(bytes: &[u8]) -> String { let mut hex = String::with_capacity(bytes.len() * 2); for byte in bytes { write!(hex, "{:02x}", byte).expect("Can't fail on writing to string"); } hex } /// Parses a PreSharedKey from a key file /// /// currently supports only base16 encoding. impl FromStr for PreSharedKey { type Err = KeyParseError; fn from_str(s: &str) -> Result { if let &[keytype, encoding, key] = s.lines().take(3).collect::>().as_slice() { if keytype != "/key/swarm/psk/1.0.0/" { return Err(KeyParseError::InvalidKeyType); } if encoding != "/base16/" { return Err(KeyParseError::InvalidKeyEncoding); } parse_hex_key(key.trim_end()).map(PreSharedKey) } else { Err(KeyParseError::InvalidKeyFile) } } } impl fmt::Debug for PreSharedKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("PreSharedKey") .field(&to_hex(&self.0)) .finish() } } /// Dumps a PreSharedKey in key file format compatible with go-libp2p impl fmt::Display for PreSharedKey { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "/key/swarm/psk/1.0.0/")?; writeln!(f, "/base16/")?; writeln!(f, "{}", to_hex(&self.0)) } } /// A PreSharedKey fingerprint computed from a PreSharedKey #[derive(Copy, Clone, PartialEq, Eq)] pub struct Fingerprint([u8; FINGERPRINT_SIZE]); /// Dumps the fingerprint as hex impl fmt::Display for Fingerprint { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", to_hex(&self.0)) } } /// Error when parsing a PreSharedKey #[derive(Clone, Debug, PartialEq, Eq)] pub enum KeyParseError { /// file does not have the expected structure InvalidKeyFile, /// unsupported key type InvalidKeyType, /// unsupported key encoding. Currently only base16 is supported InvalidKeyEncoding, /// Key is of the wrong length InvalidKeyLength, /// key string contains a char that is not consistent with the specified encoding InvalidKeyChar(ParseIntError), } /// Private network configuration #[derive(Debug, Copy, Clone)] pub struct PnetConfig { /// the PreSharedKey to use for encryption key: PreSharedKey, } impl PnetConfig { pub fn new(key: PreSharedKey) -> Self { Self { key } } /// upgrade a connection to use pre shared key encryption. /// /// the upgrade works by both sides exchanging 24 byte nonces and then encrypting /// subsequent traffic with XSalsa20 pub async fn handshake( self, mut socket: TSocket, ) -> Result, PnetError> where TSocket: AsyncRead + AsyncWrite + Send + Unpin + 'static, { trace!("exchanging nonces"); let mut local_nonce = [0u8; NONCE_SIZE]; let mut remote_nonce = [0u8; NONCE_SIZE]; rand::thread_rng().fill_bytes(&mut local_nonce); socket .write_all(&local_nonce) .await .map_err(PnetError::HandshakeError)?; socket .read_exact(&mut remote_nonce) .await .map_err(PnetError::HandshakeError)?; trace!("setting up ciphers"); let write_cipher = XSalsa20::new(&self.key.0.into(), &local_nonce.into()); let read_cipher = XSalsa20::new(&self.key.0.into(), &remote_nonce.into()); Ok(PnetOutput::new(socket, write_cipher, read_cipher)) } } /// The result of a handshake. This implements AsyncRead and AsyncWrite and can therefore /// be used as base for additional upgrades. #[pin_project] pub struct PnetOutput { #[pin] inner: CryptWriter, read_cipher: XSalsa20, } impl PnetOutput { fn new(inner: S, write_cipher: XSalsa20, read_cipher: XSalsa20) -> Self { Self { inner: CryptWriter::with_capacity(WRITE_BUFFER_SIZE, inner, write_cipher), read_cipher, } } } impl AsyncRead for PnetOutput { fn poll_read( self: Pin<&mut Self>, cx: &mut Context, buf: &mut [u8], ) -> Poll> { let this = self.project(); let result = this.inner.get_pin_mut().poll_read(cx, buf); if let Poll::Ready(Ok(size)) = &result { trace!("read {} bytes", size); this.read_cipher.apply_keystream(&mut buf[..*size]); trace!("decrypted {} bytes", size); } result } } impl AsyncWrite for PnetOutput { fn poll_write( self: Pin<&mut Self>, cx: &mut Context, buf: &[u8], ) -> Poll> { self.project().inner.poll_write(cx, buf) } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { self.project().inner.poll_flush(cx) } fn poll_close(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { self.project().inner.poll_close(cx) } } /// Error when writing or reading private swarms #[derive(Debug)] pub enum PnetError { /// Error during handshake. HandshakeError(IoError), /// I/O error. IoError(IoError), } impl From for PnetError { #[inline] fn from(err: IoError) -> PnetError { PnetError::IoError(err) } } impl error::Error for PnetError { fn cause(&self) -> Option<&dyn error::Error> { match *self { PnetError::HandshakeError(ref err) => Some(err), PnetError::IoError(ref err) => Some(err), } } } impl fmt::Display for PnetError { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { match self { PnetError::HandshakeError(e) => write!(f, "Handshake error: {}", e), PnetError::IoError(e) => write!(f, "I/O error: {}", e), } } } #[cfg(test)] mod tests { use super::*; use quickcheck::*; impl Arbitrary for PreSharedKey { fn arbitrary(g: &mut G) -> PreSharedKey { let mut key = [0; KEY_SIZE]; g.fill_bytes(&mut key); PreSharedKey(key) } } #[test] fn psk_tostring_parse() { fn prop(key: PreSharedKey) -> bool { let text = key.to_string(); text.parse::() .map(|res| res == key) .unwrap_or(false) } QuickCheck::new() .tests(10) .quickcheck(prop as fn(PreSharedKey) -> _); } #[test] fn psk_parse_failure() { use KeyParseError::*; assert_eq!("".parse::().unwrap_err(), InvalidKeyFile); assert_eq!( "a\nb\nc".parse::().unwrap_err(), InvalidKeyType ); assert_eq!( "/key/swarm/psk/1.0.0/\nx\ny" .parse::() .unwrap_err(), InvalidKeyEncoding ); assert_eq!( "/key/swarm/psk/1.0.0/\n/base16/\ny" .parse::() .unwrap_err(), InvalidKeyLength ); } #[test] fn fingerprint() { // checked against go-ipfs output let key = "/key/swarm/psk/1.0.0/\n/base16/\n6189c5cf0b87fb800c1a9feeda73c6ab5e998db48fb9e6a978575c770ceef683".parse::().unwrap(); let expected = "45fc986bbc9388a11d939df26f730f0c"; let actual = key.fingerprint().to_string(); assert_eq!(expected, actual); } }