diff --git a/Cargo.toml b/Cargo.toml index 73fc410..0e78b25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,11 @@ docopt = "0.6" rustc-serialize = "0.3" log = "0.3" epoll = "0.2" +sodiumoxide = {version = "0.0.9", optional = true} [build-dependencies] gcc = "0.3" + +[features] +default = [] +crypto = ["sodiumoxide"] diff --git a/src/cloud.rs b/src/cloud.rs index 01b0dd4..0a7a624 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -14,6 +14,7 @@ use super::types::{Table, Protocol, VirtualInterface, Range, Error, NetworkId}; use super::device::Device; use super::udpmessage::{encode, decode, Options, Message}; use super::{ethernet, ip}; +use super::Crypto; struct PeerList { timeout: Duration, @@ -92,7 +93,8 @@ pub struct GenericCloud { table: Box, socket: UdpSocket, device: Device, - network_id: Option, + options: Options, + crypto: Crypto, next_peerlist: SteadyTime, update_freq: Duration, buffer_out: [u8; 64*1024], @@ -102,11 +104,14 @@ pub struct GenericCloud { impl GenericCloud

{ pub fn new(device: Device, listen: String, network_id: Option, table: Box

, - peer_timeout: Duration, learning: bool, broadcast: bool, addresses: Vec) -> Self { + peer_timeout: Duration, learning: bool, broadcast: bool, addresses: Vec, + crypto: Crypto) -> Self { let socket = match UdpSocket::bind(&listen as &str) { Ok(socket) => socket, _ => panic!("Failed to open socket") }; + let mut options = Options::default(); + options.network_id = network_id; GenericCloud{ peers: PeerList::new(peer_timeout), addresses: addresses, @@ -116,7 +121,8 @@ impl GenericCloud

{ table: table, socket: socket, device: device, - network_id: network_id, + options: options, + crypto: crypto, next_peerlist: SteadyTime::now(), update_freq: peer_timeout/2, buffer_out: [0; 64*1024], @@ -127,9 +133,7 @@ impl GenericCloud

{ fn send_msg(&mut self, addr: Addr, msg: &Message) -> Result<(), Error> { debug!("Sending {:?} to {}", msg, addr); - let mut options = Options::default(); - options.network_id = self.network_id; - let size = encode(&options, msg, &mut self.buffer_out); + let size = encode(&mut self.options, msg, &mut self.buffer_out, &mut self.crypto); match self.socket.send_to(&self.buffer_out[..size], addr) { Ok(written) if written == size => Ok(()), Ok(_) => Err(Error::SocketError("Sent out truncated packet")), @@ -210,7 +214,7 @@ impl GenericCloud

{ } fn handle_net_message(&mut self, peer: SocketAddr, options: Options, msg: Message) -> Result<(), Error> { - if let Some(id) = self.network_id { + if let Some(id) = self.options.network_id { if options.network_id != Some(id) { info!("Ignoring message from {} with wrong token {:?}", peer, options.network_id); return Err(Error::WrongNetwork(options.network_id)); @@ -279,7 +283,7 @@ impl GenericCloud

{ match &events[i as usize].data { &0 => match self.socket.recv_from(&mut buffer) { Ok((size, src)) => { - match decode(&buffer[..size]).and_then(|(options, msg)| self.handle_net_message(src, options, msg)) { + match decode(&mut buffer[..size], &mut self.crypto).and_then(|(options, msg)| self.handle_net_message(src, options, msg)) { Ok(_) => (), Err(e) => error!("Error: {:?}", e) } diff --git a/src/crypto.rs b/src/crypto.rs new file mode 100644 index 0000000..42fae38 --- /dev/null +++ b/src/crypto.rs @@ -0,0 +1,73 @@ +use sodiumoxide::crypto::stream::chacha20::{Key as CryptoKey, Nonce, stream_xor_inplace, gen_nonce, + NONCEBYTES, KEYBYTES}; +use sodiumoxide::crypto::auth::hmacsha512256::{Key as AuthKey, Tag, authenticate, verify}; +use sodiumoxide::crypto::pwhash::{derive_key, SALTBYTES, Salt, HASHEDPASSWORDBYTES, + OPSLIMIT_INTERACTIVE, MEMLIMIT_INTERACTIVE}; + +use super::types::Error; + +pub enum Crypto { + None, + ChaCha20HmacSha512256{key: Vec, nonce: Nonce} +} + +fn inc_nonce(nonce: &mut Nonce) { + for i in 1..NONCEBYTES+1 { + let mut val = nonce.0[NONCEBYTES-i]; + val = val.wrapping_add(1); + nonce.0[NONCEBYTES-i] = val; + if val != 0 { + break; + } + } +} + +impl Crypto { + pub fn is_secure(&self) -> bool { + match self { + &Crypto::None => false, + _ => true + } + } + + pub fn from_shared_key(password: &str) -> Self { + let salt = "vpn cloud----vpn cloud----vpn cloud"; + 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 key = key[..KEYBYTES].iter().map(|b| *b).collect(); + Crypto::ChaCha20HmacSha512256{key: key, nonce: gen_nonce()} + } + + pub fn decrypt(&self, mut buf: &mut [u8], nonce: &[u8], hash: &[u8]) -> Result<(), Error> { + match self { + &Crypto::None => Ok(()), + &Crypto::ChaCha20HmacSha512256{ref key, nonce: _} => { + let crypto_key = CryptoKey::from_slice(key).unwrap(); + let nonce = Nonce::from_slice(nonce).unwrap(); + let auth_key = AuthKey::from_slice(key).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")) + } + } + } + } + + pub fn encrypt(&mut self, mut buf: &mut [u8]) -> (Vec, Vec) { + match self { + &mut Crypto::None => (Vec::new(), Vec::new()), + &mut Crypto::ChaCha20HmacSha512256{ref key, mut nonce} => { + let crypto_key = CryptoKey::from_slice(key).unwrap(); + let auth_key = AuthKey::from_slice(key).unwrap(); + inc_nonce(&mut nonce); + stream_xor_inplace(&mut buf, &nonce, &crypto_key); + let hash = authenticate(&buf, &auth_key); + (nonce.0.iter().map(|v| *v).collect(), hash.0.iter().map(|v| *v).collect()) + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 4b420e1..2327853 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,12 @@ extern crate time; extern crate docopt; extern crate rustc_serialize; extern crate epoll; +#[cfg(feature = "crypto")] extern crate sodiumoxide; mod util; mod types; +#[cfg(feature = "crypto")] mod crypto; +#[cfg(not(feature = "crypto"))] mod no_crypto; mod udpmessage; mod ethernet; mod ip; @@ -23,10 +26,10 @@ use ethernet::SwitchTable; use ip::RoutingTable; use types::{Error, Mode, Type, Range, Table}; use cloud::{TapCloud, TunCloud}; - +#[cfg(feature = "crypto")] pub use crypto::Crypto; +#[cfg(not(feature = "crypto"))] pub use no_crypto::Crypto; //TODO: Implement IPv6 -//TODO: Encryption //TODO: Call close @@ -50,6 +53,7 @@ static USAGE: &'static str = include_str!("usage.txt"); struct Args { flag_type: Type, flag_mode: Mode, + flag_shared_key: Option, flag_subnet: Vec, flag_device: String, flag_listen: String, @@ -97,16 +101,20 @@ fn main() { name.hash(&mut s); s.finish() }); + let crypto = match args.flag_shared_key { + Some(key) => Crypto::from_shared_key(&key), + None => Crypto::None + }; match args.flag_type { Type::Tap => { - let mut cloud = TapCloud::new(device, args.flag_listen, network_id, table, peer_timeout, learning, broadcasting, ranges); + let mut cloud = TapCloud::new(device, args.flag_listen, network_id, table, peer_timeout, learning, broadcasting, ranges, crypto); for addr in args.flag_addr { cloud.connect(&addr as &str, true).expect("Failed to send"); } cloud.run() }, Type::Tun => { - let mut cloud = TunCloud::new(device, args.flag_listen, network_id, table, peer_timeout, learning, broadcasting, ranges); + let mut cloud = TunCloud::new(device, args.flag_listen, network_id, table, peer_timeout, learning, broadcasting, ranges, crypto); for addr in args.flag_addr { cloud.connect(&addr as &str, true).expect("Failed to send"); } diff --git a/src/no_crypto.rs b/src/no_crypto.rs new file mode 100644 index 0000000..fc1123f --- /dev/null +++ b/src/no_crypto.rs @@ -0,0 +1,23 @@ +use super::types::Error; + +pub enum Crypto { + None +} + +impl Crypto { + pub fn is_secure(&self) -> bool { + false + } + + pub fn from_shared_key(_password: &str) -> Self { + panic!("This binary has no crypto support"); + } + + pub fn decrypt(&self, mut _buf: &mut [u8], _nonce: &[u8], _hash: &[u8]) -> Result<(), Error> { + Ok(()) + } + + pub fn encrypt(&mut self, mut _buf: &mut [u8]) -> (Vec, Vec) { + (Vec::new(), Vec::new()) + } +} diff --git a/src/types.rs b/src/types.rs index 4e24b9e..944f02a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -125,4 +125,5 @@ pub enum Error { WrongNetwork(Option), SocketError(&'static str), TunTapDevError(&'static str), + CryptoError(&'static str) } diff --git a/src/udpmessage.rs b/src/udpmessage.rs index 20f1818..72d8d47 100644 --- a/src/udpmessage.rs +++ b/src/udpmessage.rs @@ -1,12 +1,13 @@ -use std::{mem, ptr, fmt}; +use std::{mem, ptr, fmt, slice}; use std::net::{SocketAddr, SocketAddrV4, Ipv4Addr}; use std::u16; use super::types::{Error, NetworkId, Range, Address}; use super::util::{as_obj, as_bytes, to_vec}; +use super::Crypto; const MAGIC: [u8; 3] = [0x76, 0x70, 0x6e]; -const VERSION: u8 = 0; +const VERSION: u8 = 1; #[repr(packed)] struct TopHeader { @@ -25,7 +26,7 @@ impl Default for TopHeader { #[derive(Default, Debug, PartialEq, Eq)] pub struct Options { - pub network_id: Option + pub network_id: Option, } @@ -59,7 +60,7 @@ impl<'a> fmt::Debug for Message<'a> { } } -pub fn decode(data: &[u8]) -> Result<(Options, Message), Error> { +pub fn decode<'a>(data: &'a mut [u8], crypto: &mut Crypto) -> Result<(Options, Message<'a>), Error> { if data.len() < mem::size_of::() { return Err(Error::ParseError("Empty message")); } @@ -81,6 +82,21 @@ pub fn decode(data: &[u8]) -> Result<(Options, Message), Error> { options.network_id = Some(id); pos += 8; } + if header.flags & 0x02 > 0 { + if data.len() < pos + 40 { + return Err(Error::ParseError("Truncated options")); + } + if !crypto.is_secure() { + return Err(Error::CryptoError("Unexpected encrypted data")); + } + let nonce = &data[pos..pos+8]; + pos += 8; + let hash = &data[pos..pos+32]; + pos += 32; + // Cheat data mutable to make the borrow checker happy + let data = unsafe { slice::from_raw_parts_mut(mem::transmute(data[pos..].as_ptr()), data.len()-pos) }; + try!(crypto.decrypt(data, nonce, hash)); + } let msg = match header.msgtype { 0 => Message::Data(&data[pos..]), 1 => { @@ -141,7 +157,7 @@ pub fn decode(data: &[u8]) -> Result<(Options, Message), Error> { Ok((options, msg)) } -pub fn encode(options: &Options, msg: &Message, buf: &mut [u8]) -> usize { +pub fn encode(options: &Options, msg: &Message, buf: &mut [u8], crypto: &mut Crypto) -> usize { assert!(buf.len() >= mem::size_of::()); let mut pos = 0; let mut header = TopHeader::default(); @@ -154,6 +170,9 @@ pub fn encode(options: &Options, msg: &Message, buf: &mut [u8]) -> usize { if options.network_id.is_some() { header.flags |= 0x01; } + if crypto.is_secure() { + header.flags |= 0x02 + } 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(); @@ -165,6 +184,16 @@ pub fn encode(options: &Options, msg: &Message, buf: &mut [u8]) -> usize { } pos += 8; } + let (nonce_pos, hash_pos) = if crypto.is_secure() { + let nonce_pos = pos; + pos += 8; + let hash_pos = pos; + pos += 32; + (nonce_pos, hash_pos) + } else { + (0, 0) + }; + let crypto_pos = pos; match msg { &Message::Data(ref data) => { assert!(buf.len() >= pos + data.len()); @@ -218,20 +247,30 @@ pub fn encode(options: &Options, msg: &Message, buf: &mut [u8]) -> usize { &Message::Close => { } } + if crypto.is_secure() { + let (nonce, hash) = crypto.encrypt(&mut buf[crypto_pos..pos]); + assert_eq!(nonce.len(), 8); + assert_eq!(hash.len(), 32); + unsafe { + ptr::copy_nonoverlapping(nonce.as_ptr(), buf[nonce_pos..].as_mut_ptr(), 8); + ptr::copy_nonoverlapping(hash.as_ptr(), buf[hash_pos..].as_mut_ptr(), 32); + } + } pos } #[test] fn encode_message_packet() { - let options = Options::default(); + let mut options = Options::default(); + let mut crypto = Crypto::None; let payload = [1,2,3,4,5]; let msg = Message::Data(&payload); let mut buf = [0; 1024]; - let size = encode(&options, &msg, &mut buf[..]); + let size = encode(&mut options, &msg, &mut buf[..], &mut crypto); assert_eq!(size, 13); assert_eq!(&buf[..8], &[118,112,110,0,0,0,0,0]); - let (options2, msg2) = decode(&buf[..size]).unwrap(); + let (options2, msg2) = decode(&mut buf[..size], &mut crypto).unwrap(); assert_eq!(options, options2); assert_eq!(msg, msg2); } @@ -239,13 +278,14 @@ fn encode_message_packet() { #[test] fn encode_message_peers() { use std::str::FromStr; - let options = Options::default(); + let mut options = Options::default(); + let mut crypto = Crypto::None; let msg = Message::Peers(vec![SocketAddr::from_str("1.2.3.4:123").unwrap(), SocketAddr::from_str("5.6.7.8:12345").unwrap()]); let mut buf = [0; 1024]; - let size = encode(&options, &msg, &mut buf[..]); + let size = encode(&mut options, &msg, &mut buf[..], &mut crypto); assert_eq!(size, 22); assert_eq!(&buf[..size], &[118,112,110,0,0,0,0,1,2,1,2,3,4,0,123,5,6,7,8,48,57,0]); - let (options2, msg2) = decode(&buf[..size]).unwrap(); + let (options2, msg2) = decode(&mut buf[..size], &mut crypto).unwrap(); assert_eq!(options, options2); assert_eq!(msg, msg2); } @@ -254,39 +294,42 @@ fn encode_message_peers() { fn encode_option_network_id() { let mut options = Options::default(); options.network_id = Some(134); + let mut crypto = Crypto::None; let msg = Message::Close; let mut buf = [0; 1024]; - let size = encode(&options, &msg, &mut buf[..]); + let size = encode(&mut options, &msg, &mut buf[..], &mut crypto); assert_eq!(size, 16); assert_eq!(&buf[..size], &[118,112,110,0,0,0,1,3,0,0,0,0,0,0,0,134]); - let (options2, msg2) = decode(&buf[..size]).unwrap(); + let (options2, msg2) = decode(&mut buf[..size], &mut crypto).unwrap(); assert_eq!(options, options2); assert_eq!(msg, msg2); } #[test] fn encode_message_init() { - let options = Options::default(); + let mut options = Options::default(); + let mut crypto = Crypto::None; let addrs = vec![]; let msg = Message::Init(addrs); let mut buf = [0; 1024]; - let size = encode(&options, &msg, &mut buf[..]); + let size = encode(&mut options, &msg, &mut buf[..], &mut crypto); assert_eq!(size, 9); assert_eq!(&buf[..size], &[118,112,110,0,0,0,0,2,0]); - let (options2, msg2) = decode(&buf[..size]).unwrap(); + let (options2, msg2) = decode(&mut buf[..size], &mut crypto).unwrap(); assert_eq!(options, options2); assert_eq!(msg, msg2); } #[test] fn encode_message_close() { - let options = Options::default(); + let mut options = Options::default(); + let mut crypto = Crypto::None; let msg = Message::Close; let mut buf = [0; 1024]; - let size = encode(&options, &msg, &mut buf[..]); + let size = encode(&mut options, &msg, &mut buf[..], &mut crypto); assert_eq!(size, 8); assert_eq!(&buf[..size], &[118,112,110,0,0,0,0,3]); - let (options2, msg2) = decode(&buf[..size]).unwrap(); + let (options2, msg2) = decode(&mut buf[..size], &mut crypto).unwrap(); assert_eq!(options, options2); assert_eq!(msg, msg2); } diff --git a/src/usage.txt b/src/usage.txt index 53aa63a..f621ee3 100644 --- a/src/usage.txt +++ b/src/usage.txt @@ -13,6 +13,7 @@ Options: -c , --connect Address of a peer to connect to. --subnet The local subnets to use. --network-id Optional token that identifies the network. + --shared-key The shared key to encrypt all traffic. --peer-timeout Peer timeout in seconds. [default: 1800] --dst-timeout Switch table entry timeout in seconds. [default: 300] diff --git a/vpncloud.md b/vpncloud.md index ca2bde5..98cab77 100644 --- a/vpncloud.md +++ b/vpncloud.md @@ -44,6 +44,11 @@ vpncloud(1) -- Peer-to-peer VPN MAC address. The prefix length is the number of significant front bits that distinguish the subnet from other subnets. Example: `10.1.1.0/24`. + * `--shared-key `: + + An optional shared key to encrypt the VPN data. If this option is not set, + the traffic will be sent unencrypted. + * `--network-id `: An optional token that identifies the network and helps to distinguish it @@ -177,11 +182,17 @@ example. it can conflict with DHCP servers of the local network and can have severe side effects. -- VpnCloud is not designed to be secure. It encapsulates the network data but - it (currently) does not encrypt and authenticate it. Attackers with read - access to the UDP stream can read the whole traffic including any unencrypted - passwords in the payload. Attackers with write access to the UDP stream can - manipulate or suppress the whole traffic and even send data on their own. +- VpnCloud is not designed for high security use cases. Although the used crypto + 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 + on attacks that manipulate the message series itself (i.e. suppress messages, + reorder them, and duplicate them). ## NETWORK PROTOCOL