#1 prototype self-delimiting crypto-based message format

Merge realizado
fnordomat mesclou 1 commits de fnordomat/delimited-crypto-message-packets em fnordomat/master 3 anos atrás
4 arquivos alterados com 407 adições e 30 exclusões
  1. 107 18
      Cargo.lock
  2. 3 2
      Cargo.toml
  3. 295 7
      src/lib.rs
  4. 2 3
      src/main.rs

+ 107 - 18
Cargo.lock

@@ -1,6 +1,38 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
 [[package]]
+name = "aes"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd2bc6d3f370b5666245ff421e231cba4353df936e26986d2918e61a8fd6aef6"
+dependencies = [
+ "aes-soft",
+ "aesni",
+ "block-cipher",
+]
+
+[[package]]
+name = "aes-soft"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63dd91889c49327ad7ef3b500fd1109dbd3c509a03db0d4a9ce413b79f575cb6"
+dependencies = [
+ "block-cipher",
+ "byteorder",
+ "opaque-debug",
+]
+
+[[package]]
+name = "aesni"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6fe808308bb07d393e2ea47780043ec47683fcf19cf5efc8ca51c50cc8c68a"
+dependencies = [
+ "block-cipher",
+ "opaque-debug",
+]
+
+[[package]]
 name = "ansi_term"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -39,6 +71,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
 [[package]]
+name = "block-buffer"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
+dependencies = [
+ "block-padding",
+ "generic-array",
+]
+
+[[package]]
+name = "block-cipher"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f337a3e6da609650eb74e02bc9fac7b735049f7623ab12f2e4c719316fcc7e80"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-modes"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9b14fd8a4739e6548d4b6018696cf991dcf8c6effd9ef9eb33b29b8a650972"
+dependencies = [
+ "block-cipher",
+ "block-padding",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
+
+[[package]]
 name = "byteorder"
 version = "1.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -51,17 +118,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
 
 [[package]]
-name = "c2-chacha"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "217192c943108d8b13bac38a1d51df9ce8a407a3f5a71ab633980665e68fbd9a"
-dependencies = [
- "byteorder",
- "ppv-lite86",
- "stream-cipher",
-]
-
-[[package]]
 name = "cfg-if"
 version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -71,9 +127,10 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
 name = "chatbox"
 version = "0.0.0-pre-alpha"
 dependencies = [
+ "aes",
  "anyhow",
+ "block-modes",
  "bytes",
- "c2-chacha",
  "clap",
  "lazy_static",
  "matches",
@@ -85,6 +142,7 @@ dependencies = [
  "rand_chacha",
  "regex",
  "serde_yaml",
+ "sha3",
 ]
 
 [[package]]
@@ -103,6 +161,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "digest"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
 name = "dtoa"
 version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -110,11 +177,12 @@ checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b"
 
 [[package]]
 name = "generic-array"
-version = "0.12.3"
+version = "0.14.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
+checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
 dependencies = [
  "typenum",
+ "version_check",
 ]
 
 [[package]]
@@ -138,6 +206,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "keccak"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
+
+[[package]]
 name = "lazy_static"
 version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -204,6 +278,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "opaque-debug"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+
+[[package]]
 name = "ppv-lite86"
 version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -284,12 +364,15 @@ dependencies = [
 ]
 
 [[package]]
-name = "stream-cipher"
-version = "0.3.2"
+name = "sha3"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c"
+checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809"
 dependencies = [
- "generic-array",
+ "block-buffer",
+ "digest",
+ "keccak",
+ "opaque-debug",
 ]
 
 [[package]]
@@ -326,6 +409,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 
 [[package]]
+name = "version_check"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
+
+[[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"

+ 3 - 2
Cargo.toml

@@ -7,11 +7,11 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+aes          = "0.5.0"
 anyhow       = "1.0.32"
+block-modes  = "0.6.1"
 bytes        = "0.5.6"
-c2-chacha    = "0.2.4"
 clap         = "2.33.2"
-# itertools    = "0.9.0"
 lazy_static  = "1.4.0"
 num-bigint   = "0.3"
 num-rational = "0.3"
@@ -22,6 +22,7 @@ rand         = "0.7.3"
 rand_chacha  = "0.2.2"
 regex        = { version= "1.3.9", default_features= false, features=["std"] }
 serde_yaml   = "0.8.13"
+sha3         = "0.9.1"
 
 [profile.bench]
 debug = true

+ 295 - 7
src/lib.rs

@@ -22,8 +22,8 @@ extern crate test;
 // Note: the method described in the original article (using Huffman trees and thus only probabilities that are negative powers of 2) would eliminate the need for inefficient multiplications of arbitrary bignums, which turned out (after developing this prototype) to be too inefficient. This is probably the biggest single thing / tradeoff we can do to attain acceptable performance. Any other ideas?
 
 // Next steps / ideas:
-// - Optimize until it actually becomes usable (see above)
-// - Develop a message / packet format (encrypted, authenticated, numbered, delimited).
+// - Optimize until it actually becomes usable (see above - specialize everything to powers of 2, then start profiling in earnest)
+// - Obtain independent review of the crypto, the message format and everything else
 // - Mark some nonterminals as non-coding to make messages somewhat robust against tampering?
 
 #[macro_use]
@@ -63,6 +63,14 @@ use std::io::BufReader;
 // use c2_chacha::stream_cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek};
 // use c2_chacha::{ChaCha12, ChaCha20};
 
+use aes::block_cipher::generic_array::GenericArray;
+use aes::block_cipher::{BlockCipher, NewBlockCipher};
+use aes::Aes128;
+use block_modes::{BlockMode, Cbc};
+use block_modes::block_padding::Pkcs7;
+type Aes128Cbc = Cbc<Aes128, Pkcs7>;
+use sha3::{Digest, Sha3_256};
+
 use std::cmp::min;
 
 // Not regex::bytes::Regex: used for reading YAML which is always unicode
@@ -814,7 +822,8 @@ impl<'a> PrefixfreeDecoder<'a> {
     }
 }
 
-
+/// The plain decoder, without the requirement for the grammar to be prefix-free.
+/// Instead, it relies on the sentences being delimited externally.
 impl<'a> Decoder<'a> {
     pub fn new(scfg: &'a SCFG, inputs: &'a mut dyn Iterator<Item = Vec<u8>>) -> Decoder<'a> {
 
@@ -828,6 +837,13 @@ impl<'a> Decoder<'a> {
         }
     }
 
+    /// @precondition: use only when remaining_input is empty
+    pub fn refill(self: &'a mut Self, additional_inputs: &'a mut dyn Iterator<Item = Vec<u8>>) {
+        let head = additional_inputs.next().unwrap();
+        self.input = head;
+        self.remaining_inputs = Box::new(additional_inputs);
+    }
+
     pub fn decode(self: &mut Self) -> Result<BigRational, anyhow::Error> {
         loop {
             let k = step_decode(&self.grammar, &mut self.state, &self.input);
@@ -839,6 +855,9 @@ impl<'a> Decoder<'a> {
                 Ok(DecodeStepResult::Finished {
                     choices
                 }) => {
+                    // in case we want to refill with additional inputs
+                    // TODO: test whether this works as intended; apply
+                    self.state = vec![DecoderStateEntry::from_previous_choices(choices.clone())];
                     match self.remaining_inputs.next() {
                         None => {
                             let l = compute_bounds(&self.grammar, &choices).l;
@@ -847,14 +866,14 @@ impl<'a> Decoder<'a> {
                         Some(input) => {
                             let next = input;
                             self.input = next;
-                            self.state = vec![DecoderStateEntry::from_previous_choices(choices)];
                         }
                     }
                 }
 
-                Ok(DecodeStepResult::PrefixParsed {
-                    ..
-                }) => {
+                // Since the grammar is unambiguous, and this is the parser that must
+                // parse the entire input (language not assumed to be prefix-free),
+                // it must be fatal.
+                Ok(DecodeStepResult::PrefixParsed { .. }) => {
                     return Err(anyhow::Error::msg("Only a prefix was parsed"));
                 }
 
@@ -985,6 +1004,244 @@ impl Iterator for DeterministicPseudoRandoms {
     }
 }
 
+
+
+/***********************
+ * MESSAGE <-> PACKETS *
+ ***********************/
+
+pub struct Packetizer<'a> {
+    rng: rand_chacha::ChaCha8Rng,
+    aes_key: GenericArray<u8, <aes::Aes128 as aes::NewBlockCipher>::KeySize>,
+    messages_iter: Box<&'a mut dyn Iterator<Item = Vec<u8>>>,
+    conversation_id: &'a [u8; 8],
+    /// only 6 bytes are used
+    sequence_no: usize,
+}
+
+fn xor_blocks(block1: &[u8], block2: &[u8]) -> Vec<u8>
+{
+    let mut out : Vec<u8> = Vec::with_capacity(block1.len());
+    block1.iter().zip(block2.iter()).for_each(
+        |(x1, x2)|
+        {
+            out.push(x1.clone() ^ x2.clone());
+        }
+    );
+    return out
+}
+
+/// HMAC, general definition: https://tools.ietf.org/html/rfc2104
+/// "To compute HMAC over the data `text' we perform
+///  H(K XOR opad, H(K XOR ipad, text))"
+/// Truncating HMACs is permissible.
+fn hmac_sha3(key: &[u8], msg: &[u8]) -> Vec<u8> {
+    let ipad = vec![0x36; 16];
+    let opad = vec![0x5C; 16];
+    let mut hash_inner = Sha3_256::new();
+    hash_inner.update(xor_blocks(key, &ipad[..]));
+    hash_inner.update(msg.clone());
+    let result_inner = hash_inner.finalize();
+    let mut hash_outer = Sha3_256::new();
+    hash_outer.update(xor_blocks(key, &opad[..]));
+    hash_outer.update(result_inner);
+    let result_outer : Vec<u8> = hash_outer.finalize().to_vec();
+    result_outer
+}
+
+#[derive(Clone, Debug)]
+struct Header {
+    msg_size: u16,
+    conversation_id: Vec<u8>,
+    sequence_no: usize,
+    enc_hdr_as_vec: Vec<u8>,
+}
+
+#[derive(Clone, Debug)]
+struct Packet {
+    header: Header,
+    payload: Vec<u8>,
+}
+
+#[derive(Clone, Debug)]
+enum DepacketizeError {
+    HeaderHmacMismatch,
+    HeaderAndMessageHmacMismatch,
+    TooShortForHeader,
+    TooShortForHeaderAndMessageHmac,
+    TooShortForMessage,
+}
+
+/// See if it is already possible to glean a packet header from the given input,
+/// which may just be a prefix of the full input.
+/// Intended to be used to detect the start of a packet, to "synchronize" decoder
+/// with message stream.
+fn try_read_packet_header(key: &[u8], input: &[u8]) -> Result<Header, DepacketizeError> {
+
+    if input.len() < 40 {
+        return Err(DepacketizeError::TooShortForHeader);
+    }
+
+    let (encrypted_nonce, input) = input.split_at(16);
+    let (xor_hdr, input) = input.split_at(16);
+    let (hmac_hdr, _) = input.split_at(8);
+
+    let my_hmac_hdr : Vec<u8> = hmac_sha3(&key[..], &xor_hdr[..]);
+    if hmac_hdr != &my_hmac_hdr[..8] {
+        return Err(DepacketizeError::HeaderHmacMismatch);
+    }
+
+    let cipher = Aes128::new(GenericArray::from_slice(key));
+    let mut unencrypted_nonce = GenericArray::clone_from_slice(&encrypted_nonce);
+    cipher.decrypt_block(&mut unencrypted_nonce);
+    let hdr = xor_blocks(&unencrypted_nonce, xor_hdr);
+
+    let hdrx = &hdr[..];
+    let (conv_id, hdrx) = hdrx.split_at(8);
+    let (size, seq_no) = hdrx.split_at(2);
+
+    let mut seq_no8 : [u8; 8] = Default::default();
+    seq_no8[2..].copy_from_slice(&seq_no);
+    let seq_no = usize::from_be_bytes(seq_no8);
+
+    let mut size2: [u8; 2] = Default::default();
+    size2.copy_from_slice(&size[..]);
+    let size = u16::from_be_bytes(size2);
+
+    let mut enc_hdr_as_vec = vec![];
+    enc_hdr_as_vec.extend_from_slice(&encrypted_nonce[..]);
+    enc_hdr_as_vec.extend_from_slice(&xor_hdr[..]);
+
+    Ok(Header{
+        msg_size: size,
+        conversation_id: conv_id.to_vec(),
+        sequence_no: seq_no,
+        enc_hdr_as_vec: enc_hdr_as_vec.to_vec(),
+    })
+}
+
+fn try_depacketize(key: &[u8], input: &[u8]) -> Result<Packet, DepacketizeError> {
+
+    match try_read_packet_header(key, input) {
+        Ok(header) => {
+            let size = header.msg_size;
+
+            if input.len() < 48 {
+                return Err(DepacketizeError::TooShortForHeaderAndMessageHmac);
+            }
+
+            let (_, input) = input.split_at(40);
+            let (hmac_hdr_msg, input) = input.split_at(8);
+
+            if input.len() < size.into() {
+                return Err(DepacketizeError::TooShortForMessage);
+            }
+
+            let (iv, ciphertext) = input.split_at(16);
+
+            let mut enc_hdr_msg = vec![];
+            enc_hdr_msg.extend_from_slice(&header.enc_hdr_as_vec[..]);
+            enc_hdr_msg.extend_from_slice(&iv[..]);
+            enc_hdr_msg.extend_from_slice(&ciphertext[..size as usize - iv.len()]);
+            let my_hmac_hdr_msg : Vec<u8> = hmac_sha3(&key[..], &enc_hdr_msg[..]);
+
+            if hmac_hdr_msg != &my_hmac_hdr_msg[..8] {
+                return Err(DepacketizeError::HeaderAndMessageHmacMismatch);
+            }
+
+            let cipher = Aes128Cbc::new_var(&key, &iv).unwrap();
+            let decrypted_ciphertext = cipher.decrypt_vec(&ciphertext).unwrap();
+            Ok(Packet{ payload: decrypted_ciphertext, header })
+        }
+        Err(e) => {
+            Err(e)
+        }
+    }
+}
+
+/// Message format:
+/// - header:
+///   - AES128block(key, nonce) (16 bytes)
+///   - header, encrypted by xor'ing with nonce
+///     - conversation id (8 bytes)
+///     - ciphertext size
+///   - HMAC (encrypted header) truncated to 8 bytes
+///   - HMAC (encrypted nonce || encrypted header || iv || ciphertext), truncated to 8 bytes
+/// - payload:
+///   - random iv (16 bytes)
+///   - ciphertext, consisting of message encrypted as
+///     AES128Cbc(key, iv=encrypted nonce, message)
+impl<'a> Iterator for Packetizer<'a> {
+    type Item = Vec<u8>;
+
+    fn next(&mut self) -> Option<Vec<u8>> {
+
+        match self.messages_iter.next() {
+            Some(message) => {
+                let key = self.aes_key.clone();
+
+                let mut nonce_vec = vec![0u8; 16];
+                self.rng.fill(&mut nonce_vec[..]);
+                let mut nonce_array = GenericArray::clone_from_slice(&nonce_vec[..]);
+
+                let cipher_n = Aes128::new(&key);
+                let unencrypted_nonce = nonce_array.clone();
+                cipher_n.encrypt_block(&mut nonce_array);
+                let mut encrypted_nonce = nonce_array.to_vec();
+
+                let mut iv = vec![0u8; 16];
+                self.rng.fill(&mut iv[..]);
+                let cipher = Aes128Cbc::new_var(&key, &iv).unwrap();
+                let mut ciphertext = cipher.encrypt_vec(&message[..]);
+
+                let mut hdr = vec![];
+                // 2 bytes are plenty, considering the inefficiency of the encoding.
+                let size : u16 = ciphertext.len() as u16 + iv.len() as u16;
+                let mut size_be = u16::to_be_bytes(size).to_vec();
+
+                // 8 bytes for conversation id
+                hdr.extend_from_slice(self.conversation_id);
+                // 2 bytes for size of message
+                hdr.append(&mut size_be);
+                // 6 bytes for sequence number
+                hdr.extend_from_slice(&usize::to_be_bytes(self.sequence_no)[2..8]);
+
+                self.sequence_no += 1;
+
+                let mut xor_hdr = vec![];
+                for i in 0 .. hdr.len() {
+                    xor_hdr.push(hdr[i] ^ unencrypted_nonce[i]);
+                }
+
+                let mut hmac_hdr : Vec<u8> = hmac_sha3(&key[..], &xor_hdr[..]);
+                let mut enc_hdr_msg = vec![];
+                enc_hdr_msg.extend_from_slice(&encrypted_nonce[..]);
+                enc_hdr_msg.extend_from_slice(&xor_hdr[..]);
+                enc_hdr_msg.extend_from_slice(&iv[..]);
+                enc_hdr_msg.extend_from_slice(&ciphertext[..]);
+                let mut hmac_hdr_msg : Vec<u8> = hmac_sha3(&key[..], &enc_hdr_msg[..]);
+
+                hmac_hdr.truncate(8);
+                hmac_hdr_msg.truncate(8);
+
+                let mut enc_msg = vec![];
+                enc_msg.append(&mut encrypted_nonce);
+                enc_msg.append(&mut xor_hdr.clone());
+                enc_msg.append(&mut hmac_hdr.clone());
+                enc_msg.append(&mut hmac_hdr_msg.clone());
+                enc_msg.append(&mut iv);
+                enc_msg.append(&mut ciphertext);
+
+                Some(enc_msg)
+            }
+            None => {
+                None
+            }
+        }
+    }
+}
+
+
 trait GetOk<'a, I: std::fmt::Display, R> {
     fn get_ok(&self, index: I) -> std::result::Result<&R, anyhow::Error>;
     fn has(&self, index: I) -> bool;
@@ -1547,6 +1804,37 @@ mod tests {
     }
 
     #[test]
+    fn packetizer() {
+        let miniseed = 45;
+        let conv_id = b"XYZZYXYZ";
+
+        let mut inputs_iter = vec![b"\x00\xff%!)(*(@)_@(#)*_@!)%*(".to_vec(),
+                                   b"barfoo".to_vec()].into_iter();
+        let key = GenericArray::from_slice(&[0u8; 16]);
+
+        let mut p = Packetizer{
+            rng: rand_chacha::ChaCha8Rng::seed_from_u64(miniseed),
+            aes_key: *key,
+            conversation_id: conv_id,
+            messages_iter: Box::new(&mut inputs_iter),
+            sequence_no: 0
+        };
+
+        let nx = p.next();
+        assert_matches!(nx, Some(..));
+
+        if let Some(stuff) = nx {
+            println!("{:?}", &stuff[..]);
+
+            let mx = try_read_packet_header(key, &stuff[..]);
+            assert_matches!(mx, Ok(..));
+
+            let mx = try_depacketize(key, &stuff[..]);
+            assert_matches!(mx, Ok(..));
+        }
+    }
+
+    #[test]
     fn bigrationals_and_bytevecs() {
         for input in vec![
             vec![0x00, 0x00],

+ 2 - 3
src/main.rs

@@ -35,13 +35,12 @@ use chatbox::*;
 
 struct LengthDelimitedInputIterator<'a> {
     iter: Box<&'a mut dyn Iterator<Item = u8>>,
-    mem: Vec<Vec<u8>>,
     err: Option<anyhow::Error>,
 }
 
 impl<'a> LengthDelimitedInputIterator<'a> {
     fn new(iter: Box<&'a mut dyn Iterator<Item = u8>>) -> Self {
-        LengthDelimitedInputIterator{ iter: iter, mem: vec![], err: None }
+        LengthDelimitedInputIterator{ iter: iter, err: None }
     }
 }
 
@@ -162,7 +161,7 @@ fn main() -> Result<(), anyhow::Error> {
         // form, using a shared secret and indistinguishable from random to an observer not in \{Alice, Bob\}.
         // It should be quickly recognizable by the legitimate decoder in possession of the shared secret.
         //
-        // Let's work out the details and draft an RFC ...
+        // Let's work out the details and draft an RFC ... see Packetizer code for a prototype
 
     } else if matches.occurrences_of("decode") > 0 {