Crypto feature

pull/9/head
Dennis Schwerdel 2015-11-23 15:40:04 +01:00
parent 83574b9b47
commit 46728ce362
9 changed files with 205 additions and 36 deletions

View File

@ -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"]

View File

@ -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<P: Protocol> {
table: Box<Table>,
socket: UdpSocket,
device: Device,
network_id: Option<NetworkId>,
options: Options,
crypto: Crypto,
next_peerlist: SteadyTime,
update_freq: Duration,
buffer_out: [u8; 64*1024],
@ -102,11 +104,14 @@ pub struct GenericCloud<P: Protocol> {
impl<P: Protocol> GenericCloud<P> {
pub fn new(device: Device, listen: String, network_id: Option<NetworkId>, table: Box<Table>,
peer_timeout: Duration, learning: bool, broadcast: bool, addresses: Vec<Range>) -> Self {
peer_timeout: Duration, learning: bool, broadcast: bool, addresses: Vec<Range>,
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<P: Protocol> GenericCloud<P> {
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<P: Protocol> GenericCloud<P> {
fn send_msg<Addr: ToSocketAddrs+fmt::Display>(&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<P: Protocol> GenericCloud<P> {
}
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<P: Protocol> GenericCloud<P> {
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)
}

73
src/crypto.rs Normal file
View File

@ -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<u8>, 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<u8>, Vec<u8>) {
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())
}
}
}
}

View File

@ -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<String>,
flag_subnet: Vec<String>,
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");
}

23
src/no_crypto.rs Normal file
View File

@ -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<u8>, Vec<u8>) {
(Vec::new(), Vec::new())
}
}

View File

@ -125,4 +125,5 @@ pub enum Error {
WrongNetwork(Option<NetworkId>),
SocketError(&'static str),
TunTapDevError(&'static str),
CryptoError(&'static str)
}

View File

@ -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<NetworkId>
pub network_id: Option<NetworkId>,
}
@ -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::<TopHeader>() {
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::<TopHeader>());
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);
}

View File

@ -13,6 +13,7 @@ Options:
-c <addr>, --connect <addr> Address of a peer to connect to.
--subnet <subnet> The local subnets to use.
--network-id <network_id> Optional token that identifies the network.
--shared-key <shared_key> The shared key to encrypt all traffic.
--peer-timeout <peer_timeout> Peer timeout in seconds. [default: 1800]
--dst-timeout <dst_timeout> Switch table entry timeout in seconds.
[default: 300]

View File

@ -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 <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 <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