From 83fb9415999ae0945463f6ef07d541aa96cab6fa Mon Sep 17 00:00:00 2001 From: Dennis Schwerdel Date: Tue, 24 Nov 2015 23:47:38 +0100 Subject: [PATCH] Complete rewrite of crypto part --- Cargo.toml | 4 +- src/crypto/dummy.rs | 6 +- src/crypto/sodium.rs | 136 ++++++++++++++++++++++++++++--------------- src/main.rs | 5 +- src/udpmessage.rs | 58 +++++++++--------- vpncloud.md | 49 ++++++++-------- 6 files changed, 146 insertions(+), 112 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 116a52d..cf3761b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,11 @@ docopt = "0.6" rustc-serialize = "0.3" log = "0.3" epoll = "0.2" -sodiumoxide = {version = "0.0.9", optional = true} +libsodium-sys = {version = "0.0.9", optional = true} [build-dependencies] gcc = "0.3" [features] default = [] -crypto = ["sodiumoxide"] #not default as it requires external library +crypto = ["libsodium-sys"] #not default as it requires external library diff --git a/src/crypto/dummy.rs b/src/crypto/dummy.rs index 8fbf5b2..7022b49 100644 --- a/src/crypto/dummy.rs +++ b/src/crypto/dummy.rs @@ -13,7 +13,7 @@ impl Crypto { 0 } - pub fn auth_bytes(&self) -> usize { + pub fn additional_bytes(&self) -> usize { 0 } @@ -21,11 +21,11 @@ impl Crypto { panic!("This binary has no crypto support"); } - pub fn decrypt(&self, mut _buf: &mut [u8], _nonce: &[u8], _hash: &[u8]) -> Result<(), Error> { + pub fn decrypt(&self, mut _buf: &mut [u8], _nonce: &[u8], _hash: &[u8]) -> Result { unreachable!("This should never be called") } - pub fn encrypt(&mut self, mut _buf: &mut [u8]) -> (Vec, Vec) { + pub fn encrypt(&mut self, mut _buf: &mut [u8], _mlen: usize, _nonce_bytes: &mut [u8], _header: &[u8]) -> usize { unreachable!("This should never be called") } } diff --git a/src/crypto/sodium.rs b/src/crypto/sodium.rs index 9af3dd5..da24c4d 100644 --- a/src/crypto/sodium.rs +++ b/src/crypto/sodium.rs @@ -1,19 +1,15 @@ -use std::mem; +use std::{mem, ptr}; -use sodiumoxide::crypto::stream::chacha20::{Key as CryptoKey, Nonce, stream_xor_inplace, gen_nonce, - KEYBYTES, NONCEBYTES}; -use sodiumoxide::crypto::auth::hmacsha512256::{Key as AuthKey, Tag, authenticate, verify, TAGBYTES}; -use sodiumoxide::crypto::pwhash::{derive_key, SALTBYTES, Salt, HASHEDPASSWORDBYTES, - OPSLIMIT_INTERACTIVE, MEMLIMIT_INTERACTIVE}; +use libsodium_sys::*; use super::super::types::Error; pub enum Crypto { None, - ChaCha20HmacSha512256{crypto_key: CryptoKey, auth_key: AuthKey, nonce: Nonce} + ChaCha20Poly1305{key: [u8; 32], nonce: [u8; 8]} } -fn inc_nonce(nonce: Nonce) -> Nonce { +fn inc_nonce(nonce: [u8; 8]) -> [u8; 8] { unsafe { let mut num: u64 = mem::transmute(nonce); num = num.wrapping_add(1); @@ -25,64 +21,101 @@ impl Crypto { pub fn method(&self) -> u8 { match self { &Crypto::None => 0, - &Crypto::ChaCha20HmacSha512256{crypto_key: _, auth_key: _, nonce: _} => 1 + &Crypto::ChaCha20Poly1305{key: _, nonce: _} => 1 } } pub fn nonce_bytes(&self) -> usize { match self { &Crypto::None => 0, - &Crypto::ChaCha20HmacSha512256{crypto_key: _, auth_key: _, nonce: _} => NONCEBYTES + &Crypto::ChaCha20Poly1305{key: _, ref nonce} => nonce.len() } } - pub fn auth_bytes(&self) -> usize { + pub fn additional_bytes(&self) -> usize { match self { &Crypto::None => 0, - &Crypto::ChaCha20HmacSha512256{crypto_key: _, auth_key: _, nonce: _} => TAGBYTES + &Crypto::ChaCha20Poly1305{key: _, nonce: _} => crypto_aead_chacha20poly1305_ABYTES } } pub fn from_shared_key(password: &str) -> Self { - let salt = "vpncloudVPNCLOUDvpncl0udVpnCloud"; - assert_eq!(salt.len(), SALTBYTES); - let mut key = [0; HASHEDPASSWORDBYTES]; - derive_key(&mut key, password.as_bytes(), &Salt::from_slice(salt.as_bytes()).unwrap(), - OPSLIMIT_INTERACTIVE, MEMLIMIT_INTERACTIVE).unwrap(); - let mut crypto_key = CryptoKey([0; KEYBYTES]); - let mut auth_key = AuthKey([0; KEYBYTES]); - for i in 0..KEYBYTES { - crypto_key.0[i] = key[i]; + let salt = "vpncloudVPNCLOUDvpncl0udVpnCloud".as_bytes(); + assert_eq!(salt.len(), crypto_pwhash_scryptsalsa208sha256_SALTBYTES); + let mut key = [0; crypto_pwhash_scryptsalsa208sha256_STRBYTES]; + let res = unsafe { crypto_pwhash_scryptsalsa208sha256( + key.as_mut_ptr(), + key.len() as u64, + password.as_bytes().as_ptr(), + password.as_bytes().len() as u64, + salt.as_ptr() as *const [u8; crypto_pwhash_scryptsalsa208sha256_SALTBYTES], + crypto_pwhash_scryptsalsa208sha256_OPSLIMIT_INTERACTIVE as u64, + crypto_pwhash_scryptsalsa208sha256_MEMLIMIT_INTERACTIVE + ) }; + if res != 0 { + panic!("Key derivation failed"); } - for i in 0..KEYBYTES { - auth_key.0[i] = key[KEYBYTES+i]; + let mut crypto_key = [0; 32]; + for i in 0..crypto_key.len() { + crypto_key[i] = key[i]; } - Crypto::ChaCha20HmacSha512256{crypto_key: crypto_key, auth_key: auth_key, nonce: gen_nonce()} + let mut nonce = [0u8; 8]; + unsafe { randombytes_buf(nonce.as_mut_ptr(), nonce.len()) }; + Crypto::ChaCha20Poly1305{key: crypto_key, nonce: nonce} } - pub fn decrypt(&self, mut buf: &mut [u8], nonce: &[u8], hash: &[u8]) -> Result<(), Error> { + pub fn decrypt(&self, mut buf: &mut [u8], nonce: &[u8], header: &[u8]) -> Result { match self { - &Crypto::None => unreachable!("This should never be called"), - &Crypto::ChaCha20HmacSha512256{ref crypto_key, ref auth_key, nonce: _} => { - let nonce = Nonce::from_slice(nonce).unwrap(); - let hash = Tag::from_slice(hash).unwrap(); - stream_xor_inplace(&mut buf, &nonce, crypto_key); - match verify(&hash, &buf, auth_key) { - true => Ok(()), - false => Err(Error::CryptoError("Decryption failed")) + &Crypto::None => Ok(buf.len()), + &Crypto::ChaCha20Poly1305{ref key, nonce: _} => { + let mut mlen: u64 = buf.len() as u64; + let res = unsafe { crypto_aead_chacha20poly1305_decrypt( + buf.as_mut_ptr(), // Base pointer to buffer + &mut mlen, // Mutable size of buffer (will be set to used size) + ptr::null_mut::<[u8; 0]>(), // Mutable base pointer to secret nonce (always NULL) + buf.as_ptr(), // Base pointer to message + buf.len() as u64, // Size of message + header.as_ptr(), // Base pointer to additional data + header.len() as u64, // Size of additional data + nonce.as_ptr() as *const [u8; 8], // Base pointer to public nonce + key.as_ptr() as *const [u8; 32] // Base pointer to key + ) }; + match res { + 0 => Ok(mlen as usize), + _ => Err(Error::CryptoError("Failed to decrypt")) } } } } - pub fn encrypt(&mut self, mut buf: &mut [u8]) -> (Vec, Vec) { + pub fn encrypt(&mut self, mut buf: &mut [u8], mlen: usize, nonce_bytes: &mut [u8], header: &[u8]) -> usize { match self { - &mut Crypto::None => unreachable!("This should never be called"), - &mut Crypto::ChaCha20HmacSha512256{ref crypto_key, ref auth_key, ref mut nonce} => { + &mut Crypto::None => mlen, + &mut Crypto::ChaCha20Poly1305{ref key, ref mut nonce} => { *nonce = inc_nonce(*nonce); - let hash = authenticate(&buf, auth_key); - stream_xor_inplace(&mut buf, nonce, crypto_key); - (nonce.0.iter().map(|v| *v).collect(), hash.0.iter().map(|v| *v).collect()) + let mut clen: u64 = buf.len() as u64; + assert_eq!(nonce_bytes.len(), nonce.len()); + assert_eq!(nonce.len(), crypto_aead_chacha20poly1305_NPUBBYTES); + assert_eq!(key.len(), crypto_aead_chacha20poly1305_KEYBYTES); + assert_eq!(0, crypto_aead_chacha20poly1305_NSECBYTES); + assert!(clen as usize >= mlen + crypto_aead_chacha20poly1305_ABYTES); + let res = unsafe { crypto_aead_chacha20poly1305_encrypt( + buf.as_mut_ptr(), // Base pointer to buffer + &mut clen, // Mutable size of buffer (will be set to used size) + buf.as_ptr(), // Base pointer to message + mlen as u64, // Size of message + header.as_ptr(), // Base pointer to additional data + header.len() as u64, // Size of additional data + ptr::null::<[u8; 0]>(), // Base pointer to secret nonce (always NULL) + nonce.as_ptr() as *const [u8; 8], // Base pointer to public nonce + key.as_ptr() as *const [u8; 32] // Base pointer to key + ) }; + assert_eq!(res, 0); + assert_eq!(clen as usize, mlen + crypto_aead_chacha20poly1305_ABYTES); + unsafe { + ptr::copy_nonoverlapping(nonce.as_ptr(), nonce_bytes.as_mut_ptr(), nonce.len()); + } + clen as usize } } } @@ -93,14 +126,21 @@ fn encrypt_decrypt() { let mut sender = Crypto::from_shared_key("test"); let receiver = Crypto::from_shared_key("test"); let msg = "HelloWorld0123456789"; - let mut buffer: Vec = msg.bytes().collect(); - let (nonce1, hash1) = sender.encrypt(&mut buffer); - assert!(msg.as_bytes() != &buffer as &[u8]); - receiver.decrypt(&mut buffer, &nonce1, &hash1).unwrap(); - assert_eq!(msg.as_bytes(), &buffer as &[u8]); - let (nonce2, hash2) = sender.encrypt(&mut buffer); + let msg_bytes = msg.as_bytes(); + let mut buffer = [0u8; 1024]; + let header = [0u8; 8]; + for i in 0..msg_bytes.len() { + buffer[i] = msg_bytes[i]; + } + let mut nonce1 = [0u8; 8]; + let size = sender.encrypt(&mut buffer, msg_bytes.len(), &mut nonce1, &header); + assert_eq!(size, msg_bytes.len() + sender.additional_bytes()); + assert!(msg_bytes != &buffer[..msg_bytes.len()] as &[u8]); + receiver.decrypt(&mut buffer[..size], &nonce1, &header).unwrap(); + assert_eq!(msg_bytes, &buffer[..msg_bytes.len()] as &[u8]); + let mut nonce2 = [0u8; 8]; + let size = sender.encrypt(&mut buffer, msg_bytes.len(), &mut nonce2, &header); assert!(nonce1 != nonce2); - assert!(hash1 == hash2); - receiver.decrypt(&mut buffer, &nonce2, &hash2).unwrap(); - assert_eq!(msg.as_bytes(), &buffer as &[u8]); + receiver.decrypt(&mut buffer[..size], &nonce2, &header).unwrap(); + assert_eq!(msg_bytes, &buffer[..msg_bytes.len()] as &[u8]); } diff --git a/src/main.rs b/src/main.rs index dd252c4..06a027c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate time; extern crate docopt; extern crate rustc_serialize; extern crate epoll; -#[cfg(feature = "crypto")] extern crate sodiumoxide; +#[cfg(feature = "crypto")] extern crate libsodium_sys; mod util; mod types; @@ -31,9 +31,6 @@ use crypto::Crypto; //TODO: Implement IPv6 //TODO: Call close -//FIXME: The crypto method should be signaled as part of the protocol -//FIXME: The HMAC should also include the header -//FIXME: Encryption should also include all following additional headers struct SimpleLogger; diff --git a/src/udpmessage.rs b/src/udpmessage.rs index b3d686b..abb7a0b 100644 --- a/src/udpmessage.rs +++ b/src/udpmessage.rs @@ -65,7 +65,8 @@ impl<'a> fmt::Debug for Message<'a> { } pub fn decode<'a>(data: &'a mut [u8], crypto: &mut Crypto) -> Result<(Options, Message<'a>), Error> { - if data.len() < mem::size_of::() { + let mut end = data.len(); + if end < mem::size_of::() { return Err(Error::ParseError("Empty message")); } let mut pos = 0; @@ -81,18 +82,21 @@ pub fn decode<'a>(data: &'a mut [u8], crypto: &mut Crypto) -> Result<(Options, M return Err(Error::CryptoError("Wrong crypto method")); } if crypto.method() > 0 { - let len = crypto.nonce_bytes() + crypto.auth_bytes(); - if data.len() < pos + len { + let len = crypto.nonce_bytes(); + if end < pos + len { return Err(Error::ParseError("Truncated crypto header")); } - let (crypto_header, crypto_data) = data[pos..].split_at_mut(len); - pos += len; - let (nonce, hash) = crypto_header.split_at(crypto.nonce_bytes()); - try!(crypto.decrypt(crypto_data, nonce, hash)); + { + let (before, after) = data.split_at_mut(pos); + let (nonce, crypto_data) = after.split_at_mut(len); + pos += len; + end = try!(crypto.decrypt(crypto_data, nonce, &before[..mem::size_of::()])) + pos; + } + assert_eq!(end, data.len()-crypto.additional_bytes()); } let mut options = Options::default(); if header.flags & 0x01 > 0 { - if data.len() < pos + NETWORK_ID_BYTES { + if end < pos + NETWORK_ID_BYTES { return Err(Error::ParseError("Truncated options")); } let id = u64::from_be(*unsafe { as_obj::(&data[pos..pos+NETWORK_ID_BYTES]) }); @@ -100,15 +104,15 @@ pub fn decode<'a>(data: &'a mut [u8], crypto: &mut Crypto) -> Result<(Options, M pos += NETWORK_ID_BYTES; } let msg = match header.msgtype { - 0 => Message::Data(&data[pos..]), + 0 => Message::Data(&data[pos..end]), 1 => { - if data.len() < pos + 1 { + if end < pos + 1 { return Err(Error::ParseError("Empty peers")); } let count = data[pos]; pos += 1; let len = count as usize * 6; - if data.len() < pos + len { + if end < pos + len { return Err(Error::ParseError("Peer data too short")); } let mut peers = Vec::with_capacity(count as usize); @@ -127,28 +131,24 @@ pub fn decode<'a>(data: &'a mut [u8], crypto: &mut Crypto) -> Result<(Options, M Message::Peers(peers) }, 2 => { - if data.len() < pos + 1 { + if end < pos + 1 { return Err(Error::ParseError("Init data too short")); } let count = data[pos] as usize; pos += 1; let mut addrs = Vec::with_capacity(count); for _ in 0..count { - if data.len() < pos + 1 { + if end < pos + 1 { return Err(Error::ParseError("Init data too short")); } let len = data[pos] as usize; pos += 1; - if data.len() < pos + len { + if end < pos + len + 1 { return Err(Error::ParseError("Init data too short")); } let base = Address(to_vec(&data[pos..pos+len])); - pos += len; - if data.len() < pos + 1 { - return Err(Error::ParseError("Init data too short")); - } - let prefix_len = data[pos]; - pos += 1; + let prefix_len = data[pos+len]; + pos += len + 1; addrs.push(Range{base: base, prefix_len: prefix_len}); } Message::Init(addrs) @@ -176,11 +176,7 @@ pub fn encode(options: &Options, msg: &Message, buf: &mut [u8], crypto: &mut Cry let header_dat = unsafe { as_bytes(&header) }; unsafe { ptr::copy_nonoverlapping(header_dat.as_ptr(), buf[pos..].as_mut_ptr(), header_dat.len()) }; pos += header_dat.len(); - let nonce_pos = pos; pos += crypto.nonce_bytes(); - let hash_pos = pos; - pos += crypto.auth_bytes(); - let crypto_pos = pos; if let Some(id) = options.network_id { assert!(buf.len() >= pos + NETWORK_ID_BYTES); unsafe { @@ -243,13 +239,11 @@ pub fn encode(options: &Options, msg: &Message, buf: &mut [u8], crypto: &mut Cry } } if crypto.method() > 0 { - let (nonce, hash) = crypto.encrypt(&mut buf[crypto_pos..pos]); - assert_eq!(nonce.len(), crypto.nonce_bytes()); - assert_eq!(hash.len(), crypto.auth_bytes()); - unsafe { - ptr::copy_nonoverlapping(nonce.as_ptr(), buf[nonce_pos..].as_mut_ptr(), crypto.nonce_bytes()); - ptr::copy_nonoverlapping(hash.as_ptr(), buf[hash_pos..].as_mut_ptr(), crypto.auth_bytes()); - } + let (header, rest) = buf.split_at_mut(mem::size_of::()); + let (nonce, rest) = rest.split_at_mut(crypto.nonce_bytes()); + let crypto_start = header.len() + nonce.len(); + assert!(rest.len() >= pos - crypto_start + crypto.additional_bytes()); + pos = crypto.encrypt(rest, pos-crypto_start, nonce, header) + crypto_start; } pos } @@ -279,7 +273,7 @@ fn encode_message_encrypted() { let msg = Message::Data(&payload); let mut buf = [0; 1024]; let size = encode(&mut options, &msg, &mut buf[..], &mut crypto); - assert_eq!(size, 53); + assert_eq!(size, 37); assert_eq!(&buf[..8], &[118,112,110,1,1,0,0,0]); let (options2, msg2) = decode(&mut buf[..size], &mut crypto).unwrap(); assert_eq!(options, options2); diff --git a/vpncloud.md b/vpncloud.md index 3051f82..dfad84b 100644 --- a/vpncloud.md +++ b/vpncloud.md @@ -92,10 +92,6 @@ vpncloud(1) -- Peer-to-peer VPN Display the help. - * `-V`, `--version`: - - Print the version and exit. - ## DESCRIPTION @@ -198,11 +194,10 @@ vpncloud -t tun -c REMOTE_HOST:PORT --subnet 10.0.0.X/32 --ifup 'ifconfig $IFNAM primitives are expected to be very secure, their application has not been reviewed. The shared key is hashed using *ScryptSalsa208Sha256* to derive a key, - which is used to encrypt the payload of messages using *ChaCha20*. The - authenticity of messages is verified using *HmacSha512256* hashes. - This method only protects the contents of the message (payload, peer list, - etc.) but not the header of each message. - Also, this method does only protect against attacks on single messages but not + which is used to encrypt the payload of messages using *ChaCha20Poly1305*. + The encryption includes an authentication that also protects the header and + all additional headers. + This method does only protect against attacks on single messages but not on attacks that manipulate the message series itself (i.e. suppress messages, reorder them, and duplicate them). @@ -224,7 +219,21 @@ Every packet sent over UDP contains the following header (in order): This field specifies the version and helps nodes to parse the rest of the header and the packet. - * 2 `reserved bytes` that are currently unused + * 1 byte `crypto method` + + This field specifies the method that must be used to decrypt the rest of the + data (additional headers and message contents). The currently supported + methods are: + + - Method `0`, **No encryption**: Rest of the data can be read without + decrypting it. + + - Method `1`, **ChaCha20Poly1305**: The header is followed by a 8 byte + *nonce*. The rest of the data is encrypted with the + `libsodium::crypto_aead_chacha20poly1305` method, using the 8 byte header + as additional data. + + * 1 `reserved byte` that is currently unused * 1 byte for `flags` @@ -234,7 +243,6 @@ Every packet sent over UDP contains the following header (in order): order. Currently the following additional headers are supported: - Bit 1: Network ID - - Bit 2: Crypto information * 1 byte for the `message type` @@ -246,23 +254,18 @@ Every packet sent over UDP contains the following header (in order): - Type 2: Initial message - Type 3: Closing message -After this 8 byte header, the additional headers as specified in the `flags` -field will follow in the order of their respective flag bits. +After this 8 byte header, the rest of the message follows. It is encrypted using +the method specified in the header. + +In the decrypted data, the additional headers as specified in the `flags` field +will follow in the order of their respective flag bits. * **Network ID**: The network id is encoded as 8 bytes. - * **Crypto information**: - - If this header is present, the contents of the message are encrypted and - must have to decrypted before decoding. - This option contains 40 bytes. The first 8 bytes are the **nonce** for this - message and the later 32 bytes are the **authentication hash** of the - message. - -After the additional headers, message as specified in the `message type` field -will follow: +After the additional headers, the message as specified in the `message type` +field will follow: * **Data packet** (message type 0): This packet contains payload. The format of the data depends on the device