diff --git a/Cargo.lock b/Cargo.lock index fddf31b..296cd78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,9 +83,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -320,6 +320,30 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "getrandom" version = "0.4.2" @@ -374,9 +398,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "ctutils", "typenum", @@ -427,10 +451,12 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -464,9 +490,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "log" @@ -482,9 +508,9 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "ml-kem" -version = "0.3.0-rc.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04437cb1a66c0b78740927b76cc61f218344b9f6ef3dd430e283274a718ef0e9" +checksum = "68c77d5ff6d755d09a0ef4d4d28c2b7e83658fe83e8c736d55e93d43e380d1cd" dependencies = [ "hybrid-array", "kem", @@ -496,9 +522,9 @@ dependencies = [ [[package]] name = "module-lattice" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa" +checksum = "dc7c90d33a0dac244570c26461d761ffaeadb3bfc2b17cc625ae2185cafdffae" dependencies = [ "ctutils", "hybrid-array", @@ -537,6 +563,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "plotters" version = "0.3.7" @@ -753,6 +785,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "syn" version = "2.0.117" @@ -776,9 +814,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" @@ -832,9 +870,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -845,9 +883,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -855,9 +893,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -868,9 +906,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -911,9 +949,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index d20ee3e..79b134c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ wharrgarbl-utils = { path = "./wharrgarbl-utils", version = "0.1" } wharrgarbl-neko = { path = "./wharrgarbl-neko", version = "0.1" } ctutils.workspace = true zeroize.workspace = true -ml-kem = { version = "0.3.0-rc.2", features = ["zeroize"] } +ml-kem = { version = "0.3.0", features = ["zeroize"] } hybrid-array = { workspace = true, features = ["alloc"] } rand_core.workspace = true aead.workspace = true diff --git a/src/handshake.rs b/src/handshake.rs index 6b56519..de24da8 100644 --- a/src/handshake.rs +++ b/src/handshake.rs @@ -22,8 +22,10 @@ pub struct ClientHandshake { decap: Option>, } -impl + ParameterSet> ClientHandshake +impl ClientHandshake where + S: NekoSec, + K: Kem + ParameterSet, K::DecapsulationKey: Decapsulate, { pub fn new(psk: Option<&[u8; 32]>) -> Self { @@ -104,7 +106,11 @@ pub struct ServerHandshake { neko: NekoState, } -impl + ParameterSet> ServerHandshake { +impl ServerHandshake +where + S: NekoSec, + K: Kem + ParameterSet, +{ pub fn new(psk: Option<&[u8; 32]>) -> Self { let mut neko = NekoState::::new(WHARRGHARBL_PROTO.as_bytes()); diff --git a/src/lib.rs b/src/lib.rs index 0422d53..1cd2e2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ pub mod transport; extern crate alloc; /// Version of WHARRGARBL that this crate implements. -pub static WHARRGHARBL_PROTO: &str = "WGBLv0.1"; +pub static WHARRGHARBL_PROTO: &str = "WGBLv0.2"; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] diff --git a/src/transport.rs b/src/transport.rs index 29eded0..9571fdf 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -1,9 +1,9 @@ use core::marker::PhantomData; -use aead::{AeadInOut, Buffer, Key, KeySizeUser, TagPosition}; +use aead::{AeadInOut, Buffer, Key, KeySizeUser, TagPosition, common::IvSizeUser}; use ctutils::{CtEq, CtSelect}; use hybrid_array::AssocArraySize; -use wharrgarbl_neko::{NekoNonce, NekoSec, NekoState, NekoTag}; +use wharrgarbl_neko::{NekoSec, NekoState, NekoTag}; use crate::{Role, WHARRGHARBL_PROTO}; @@ -16,8 +16,12 @@ impl aead::KeySizeUser for AeadNeko { type KeySize = as KeySizeUser>::KeySize; } +impl IvSizeUser for AeadNeko { + type IvSize = as IvSizeUser>::IvSize; +} + impl aead::AeadCore for AeadNeko { - type NonceSize = ::Size; + type NonceSize = as IvSizeUser>::IvSize; type TagSize = ::Size; const TAG_POSITION: TagPosition = TagPosition::Postfix; } diff --git a/wharrgarbl-neko/Cargo.toml b/wharrgarbl-neko/Cargo.toml index a2db576..aa9d8e1 100644 --- a/wharrgarbl-neko/Cargo.toml +++ b/wharrgarbl-neko/Cargo.toml @@ -14,6 +14,3 @@ ctutils.workspace = true keccak = { version = "0.2" } hybrid-array.workspace = true zerocopy = "0.8.48" - -# [dev-dependencies] -# zerocopy = "0.8.48" diff --git a/wharrgarbl-neko/README.md b/wharrgarbl-neko/README.md index e6a08df..c963974 100644 --- a/wharrgarbl-neko/README.md +++ b/wharrgarbl-neko/README.md @@ -2,9 +2,11 @@ A whimsical encryption crate that is inspired by the STROBE spec, though it takes diverging choices with regards to its internals and API surface for better performance while retaining security guarantees. -## SPEC +Built on top of the Keccak sponge function, it is capable of doing not just hashing, but also symmetric encryption and message authentication through the duplex construction. In the WHARRGARBL protocol, it is used to validate and authenticate the key exchange process as well. -WIP +## Specification + +Read about the NEKO encryption [specification here](./SPEC.md). ☢️ **WARNING: DO NOT USE IN PRODUCTION. THIS CRATE IS NOT AUDITED AND IS WIP, MAYBE EVEN NOT AS SECURE AS THOUGHT. AAAAAAAAAA** ☢️ diff --git a/wharrgarbl-neko/SPEC.md b/wharrgarbl-neko/SPEC.md new file mode 100644 index 0000000..c4bf526 --- /dev/null +++ b/wharrgarbl-neko/SPEC.md @@ -0,0 +1,116 @@ +# NEKO Specification + +NEKO is inspired by STROBE in that it uses Keccak in a duplex construction to perform encryption and message authentication. It has stripped down the amount of operation flags it uses and simplifies the internals, ridding the need to have "streaming" by instead opting for having different operating modes. + +In lieu of streaming, NEKO uses op-stacking to reduce the amount of permutations needed. There are two kinds of operations, NON-PERMUTING and PERMUTING. A permutation is when the internals invoke the Keccak permutation function on the NEKO state, resulting in a new, pseudorandomised state. A permutation resets the buffer, and thus allows more data to be ingested into the cipher. + +The pattern is a max of four chained ops, with the first being a permuting one or an INIT/CONT op. Any sequence of more than 3 non-permuting ops that don't incur a permutation during data ingestion, and NEKO will throw a panic. The available operations are as follows: + +#### Non-Permuting + +- Key (256-bit value) +- Nonce (128-bit value) +- Ad (Associated Data) +- Cleartext + +#### Permuting + +- Encrypt +- Decrypt +- Create MAC +- Verify MAC +- Ratchet +- Prf (Pseudo-Random Function) + +Non-permuting operations do not perform a permutation UNLESS more data is ingested than can be fitted into the internal state buffer. Whereas Permuting operations ALWAYS permute before data is ingested, permuting further if need to be ingest all the input data. + +A valid chain of operations can be \[`INIT`, `KEY`, `NONCE`, `AD`\]+\[`ENCRYPT`\]+\[`MAC`\], with the first three after `INIT` being non-permuting, and then followed by permuting operations that "reset" the chain. This example chain will permute AT LEAST two times in order to perform its operations. The operations performed are mixed into the internal state, as well as the state of the buffer when the permutation is performed. It encodes the bitflags of each of the operations, the final count of operations performed, the final position in the state, with some constants. + +## Construction + +NEKO internally uses the Keccakf1600 state buffer directly, with its layout of `[u64; 25]`. It operates on blocks of `u64`, and mixes `[u8]` input into these blocks. If a block isn't "filled" completely, the remainder of that block is left as padding, with then the position incremented to point to the next block. It tracks a block counter and an op counter. + +NEKO has two security levels, 128-bit and 256-bit security. Each level determines the *rate*, which is how many blocks are available for input, so 128 gives 21 blocks for input with 4 blocks for entropy/*capacity*, while 256 gives 17 blocks and 8 blocks for entropy/*capacity*. The input blocks are separated for data input with the last block reserved for encoding ops. 256 mode will require permuting more often than 128 mode, as it won't have as much input capacity before its input buffer is exhausted. + +Ops are tracked with a stack, with a max of 4 ops being "stacked" before a permutation must occur. When a permutation starts, the ops encoding block is selected (position block + 1) and then it is XOR'd with the list of op bits on the \[0..4\] part of the block, going from first op to last, with the \[4..8\] part of the block XOR'd with the ops count, the block position on the first bytes, with then `0x80` & `0` on the last bytes. This creates the start of the padding structure. The padding terminator is then XOR'd on the last byte of the reserved block with a `0x80` value that is rotated by the position counter. If the input fills the buffer completely, the position+1 and reserved block are the same, so the combined XOR'd blocks will form a single block construction. The permutation is then executed with the f1600 function, after which the block & ops counters are reset along with the ops stack. + +After the permutation concludes, the op which triggered the permutation is then encoded into the stack. There are two ways a permutation is invoked: via a user OP, or from a *continuation* for ingesting more data. In the case of a user op, the counter is set to zero, and the stack fully zeroed so the op can encode its flags onto the first slot when it begins. In the case of a *continuation*, the counter is set to `1`, and the first slot is encoded with the `CONT` flag bits with the rest of the slots zeroed. This ensures that in **every** case, there will always be 3 remaining free slots to form a 4 op chain. + +As such, how an operation works is as follows: User invokes op -> (Permute if required) -> Encode Op to free slot on stack -> Ingest Data -> (Permute if continuation) -> Finish. + +Internally, each operation *operates* on the data with one of 7 different actions: + +- **ABSORB**: This XOR's the input directly into the state: `State ^ Input`. This does not mutate the input. +- **ABSORB & SET**: This XOR's the input into the state, then sets the input as the resulting state: `State ^ Input, Input = State`. This mutates the input. +- **COPY STATE**: This copies out the state directly into the input: `Input = State`. This mutates the input. +- **EXCHANGE**: This XOR's the input to the state, and then XOR's the resulting state to the input: `State ^ Input, Input ^ State`. This mutates the input. +- **OVERWRITE**: This sets the state with the input bytes directly: `State = Input`. This does not mutate the input. +- **SQUEEZE**: This sets the input with the state, then zeroes out the state. `Input = State, State = 0`. This mutates the input. +- **ZERO STATE**: This op does not take input, but instead zeroes out a specific amount of state blocks according to security level: 128 bit zeros 2 blocks, 256 zeros 4 blocks. `State = 0`. + +## Flag Definitions + +NEKO's operation flags are defined with a 4 bit bitflag in a `u8`, with the remaining high bits reserved. The flags are defined as such: + +| FLAG | Bits | Value | +| --------- | ------ | ----- | +| TRANSPORT | 0b0001 | 1 | +| APP | 0b0010 | 2 | +| CIPHER | 0b0100 | 4 | +| META | 0b1000 | 8 | + +There's a special case where a flag with all bits zero represents a `RESET` op. This is an internal op, with the only other internal op being `CONT` which is just `TRANSPORT` flag toggled on its own. These form the two ops that are not meant to be made available to users: + +| OP | FLAGS | VALUE | +| ----- | --------- | ------------- | +| RESET | EMPTY | 0b0000, 0 | +| CONT | TRANSPORT | 0b0001, 1 | + +The internal flags are used to define when permutations occur either from a new OP or from a continuation of an OP to ingest more data. + +The rest of the valid ops are available to be invoked by user operations. They are defined as follows: + +| OP | FLAGS | VALUE | +| ------- | --------------------------- | ---------- | +| CLR | APP \| TRANSPORT | 0b0011, 3 | +| MAC | CIPHER \| TRANSPORT | 0b0101, 5 | +| ENC | CIPHER \| APP \| TRANSPORT | 0b0111, 7 | +| NONCE | META \| TRANSPORT | 0b1001, 9 | +| INIT | META \| APP | 0b1010, 10 | +| AD | META \| APP \| TRANSPORT | 0b1011, 11 | +| KEY | META \| CIPHER | 0b1100, 12 | +| RATCHET | META \| CIPHER \| TRANSPORT | 0b1101, 13 | +| PRF | META \| CIPHER \| APP | 0b1110, 14 | + +ALL valid user ops are defined with a combination of two or three flags. No user operation is defined with a single op flag or all four toggled. + +## Initialisation + +Creating a new Neko State instance is qualified as an INIT operation. This means that it must be encoded as an operation on the op stack. As such the initial op stack is [INIT, 0, 0, 0] and the op counter must be set to one. The buffer position counter is then set to 0 and the Keccak F1600 state buffer is initialised and zeroed: `[0u64; 25]`; + +Then, we must encode the preamble & NEKO version onto the state buffer. The preamble is defined as `[0x01, RATE, 0x07, 0x60]`. The `RATE` is calculated as `(200 - SecLevel / 4) / 8 - 1` and encoded as a `u8` value. So for `Neko128`, the `RATE` should resolve to `20`, and `Neko256` should resolve to `16`. These represent the max buffer position that input data can be absorbed into before needing to permute. + +The NEKO version string is then concatenated to the preamble. For version v0.2 of this specification, the string is `NEKOv0.2.0`, and it should be concatenated as a byte string. Then the combined preamble+version bytes should be written to the buffer in an `OVERWRITE` action. + +Additionally, a protocol byte string can be written to the state, following after the preamble+version with its own `OVERWRITE` action. + +Once done, the state is finalised and ready to be used. + +## User operations + +With an initialised Neko state, the following operations are available to the user (with the internal action & OP flag the state takes being described): + +- `key`: Applies `KEY` opflag. Takes a 32 byte key reference `&Array`, and `OVERWRITE`s it to the state. This is a non-permuting operation (it does not modify the input). +- `nonce`: Applies `NONCE` opflag. Takes a 16 byte reference `&Array`, and `ABSORB`s it to the state. This is a non-permuting operation (it does not modify the input). +- `ad`: Applies `AD` opflag. Takes a byte slice `&[u8]` and `ABSORB`s it to the state. This is a non-permuting operation (it does not modify the input). +- `ratchet`: Applies `RATCHET` opflag. Permutes the internal state and then zeroes out a set amount of bytes in the buffer. Either 16 bytes for `Neko128` or 32 bytes for `Neko256`. This is a permuting operation, but it doesn't mutate any input because it doesn't take any input. +- `prf`: Applies `PRF` opflag. Takes a mutable byte slice `&mut [u8]` and then `SQUEEZE`s the internal state to the slice. Used for expanding keys/nonces/etc. This is a permuting operation, as it modifies the input slice. +- `cleartext`: Applies `CLR` opflag. Takes a byte slice reference of cleartext `&[u8]`, and `ABSORB`s it to the state. While similar to `AD`, it is encoded as a separate operation for semantics, as `AD` is for data that doesn't get transmitted over-the-wire, but `CLR` is for data that is not encrypted but still transmitted. This is a non-permuting operation (it does not modify the input). +- `encrypt`: Applies `ENC` opflag. Takes a mutable byte slice of cleartext `&mut [u8]`, and `ABSORB & SET`s it with the internal state to encrypt the text in place to form the ciphertext. This is a permuting operation, as it modifies the input slice. +- `decrypt`: Applies `ENC` opflag. Takes a mutable byte slice of ciphertext `&mut [u8]`, and `EXCHANGE`s it with the internal state to decrypt the text in place to form the cleartext. This is a permuting operation, as it modifies the input slice. +- `create_mac`: Applies `MAC` opflag. This doesn't take any input, but creates a tag/MAC via `COPY STATE` action, creating a `Array` that represents a MAC of the processed state. This is a permuting operation, as it requires to process all previous operations into the internal state. +- `verify_mac`: Applies `MAC` opflag. This takes a tag reference `&Array` and verifies it by `EXCHANGE` with the internal state. If the tag/MAC resolves to all zeroes, then it is a valid tag, and the actions returns a success result. If not, it returns a failure result. This is a permuting operation, as it requires to process all previous operations into the internal state. + +All user operations follow a chain of 4 ops. Non-permuting ops can *stack*, so they add to the stack if their inputs don't cause the state to permute. Permuting ops always *reset* the stack, as they permute the state. When the stack resets due to an op, the stack is cleared and the op that triggered the reset is encoded into the first slot. `INIT` is always the very first op, so `KEY` + `NONCE` + `AD` can be added as ops without triggering a permutation. If another non-permuting op were to be chained at this point (like `CLR`), this would cause a panic. To resolve this, call a permuting op like `ENC` or `RATCHET`, and the stack is reset to be just the permuting op on the stack, with three more free slots. + +A panic is a must, because any occasion that we are going over 4 chained ops is a misuse of the protocol and thus MUST fail quickly. Under normal usage, no sequence of ops should cause a chain of more than 4 to occur, and with large enough payloads, the state would be getting permuted enough to ensure this is not required. diff --git a/wharrgarbl-neko/src/kats.rs b/wharrgarbl-neko/src/kats.rs index b9d1fa3..1de0c21 100644 --- a/wharrgarbl-neko/src/kats.rs +++ b/wharrgarbl-neko/src/kats.rs @@ -13,9 +13,9 @@ fn neko_128_init_state() { let second = neko.state[1].to_le_bytes(); let third = neko.state[2].to_le_bytes(); - assert_eq!(&first, b"\x01\x16\x07\x60NEKO"); + assert_eq!(&first, b"\x01\x14\x07\x60NEKO"); // Values that don't fill the block entirely leave padding - assert_eq!(&second, b"v0.1.0\0\0"); + assert_eq!(&second, b"v0.2.0\0\0"); assert_eq!(&third[..4], b"test"); // The rest of the state is zeroed assert_eq!(&neko.state[3..], &[0; 22]); @@ -29,9 +29,9 @@ fn neko_256_init_state() { let second = neko.state[1].to_le_bytes(); let third = neko.state[2].to_le_bytes(); - assert_eq!(&first, b"\x01\x14\x07\x60NEKO"); + assert_eq!(&first, b"\x01\x10\x07\x60NEKO"); // Values that don't fill the block entirely leave padding - assert_eq!(&second, b"v0.1.0\0\0"); + assert_eq!(&second, b"v0.2.0\0\0"); assert_eq!(&third[..4], b"test"); // The rest of the state is zeroed assert_eq!(&neko.state[3..], &[0; 22]); @@ -88,43 +88,78 @@ fn non_cipher_flag_ops_dont_permute_by_default() { neko.nonce(&nonce); neko.ad(ad); - let op = neko.state[3].as_bytes(); - let state = neko.state[4..8].as_bytes(); + let state = neko.state[3..7].as_bytes(); - // OPs are XOR'd onto the state, but when the state is zeroed, it - // is the same as overwriting, so the ops are visible. - assert_eq!(op, b"\x00\x08nyaan~"); assert_eq!(state, key.as_slice()); - let op = neko.state[8].as_bytes(); - let state = neko.state[9..11].as_bytes(); + let state = neko.state[7..9].as_bytes(); - assert_eq!(op, b"\x04\x09nyaan~"); assert_eq!(state, nonce.as_slice()); - let op = neko.state[11].as_bytes(); - let state = neko.state[12].as_bytes(); + let state = neko.state[9].as_bytes(); - assert_eq!(op, b"\x09\x0Anyaan~"); assert_eq!(state, b"addz\0\0\0\0"); - assert_eq!(&neko.state[13..], &[0; 12]); + let ops = &neko.ops_stack; + // OPs are XOR'd onto the state, but when the state is zeroed, it + // is the same as overwriting, so the ops are visible. + assert_eq!(ops, &[0x0A, 0x0C, 0x09, 0x0B]); + // The rest of the state is zeroed + assert_eq!(&neko.state[13..22], &[0; 9]); + assert_eq!(&neko.state[23..], &[0; 2]); + + // We must permute at this point + neko.ratchet(); + + let expected_state = [ + 0x0000000000000000, + 0x0000000000000000, + 0x14fd15236a301dbc, + 0x3d7a0f031c2332c7, + 0x13d95db32a39a74c, + 0xbfce1f9678690375, + 0xc444bd0f9bb70133, + 0x59600201db93b1de, + 0x6bc376b646e898ea, + 0x2e8a6c345fd3dca3, + 0x9df94788a5fc9f4d, + 0x2541272cca7a631c, + 0xabc8b248a4e0eee3, + 0x2a6befaf570b0120, + 0x5e296ccc9b587798, + 0x9b9d5caef6fc7d3c, + 0x371099e20d7965db, + 0x52b7fecf8d06aed7, + 0xae285d1c6cada2c7, + 0x12d17a37884449ea, + 0x85846b16d640a55b, + 0x0e7d16c0c2bf5a3e, + 0xce32132c0b110014, + 0x6620fda4f7642f84, + 0xd1ea97aadb49a663, + ]; - // Cleartext will permute the state + assert_eq!(&neko.state, &expected_state); + + let orig_state = neko.state[2]; + + // Cleartext will not permute the state unless the text is larger + // than the available permuted buffer. neko.cleartext(cleartext); - for block in neko.state[0..22].iter().map(IntoBytes::as_bytes) { + assert_eq!(&neko.ops_stack, &[0x0D, 0x03, 0, 0]); + + for block in neko.state[2..20].iter().map(IntoBytes::as_bytes) { // The permuted state means the message is mixed, so it can no longer be "read" // directly. - assert_ne!(block, b"\x0c\x03nyaan~"); assert_ne!(block, cleartext); } let orig_text = u64::from_le_bytes(*cleartext); - let orig_state = neko.state[0] ^ orig_text; - let recreated = neko.state[0] ^ orig_state; + // RECREATE + let recreated = neko.state[2] ^ orig_state; - // After XOR'ing from the state, we can reconstruct the cleartext, so we can confirm - // it is there. + // After XOR'ing the original state from the modified state, we can reconstruct the cleartext + // so we can confirm it is there. assert_eq!(orig_text, recreated); } @@ -152,31 +187,31 @@ fn cipher_ops_permute_state_by_default() { assert_eq!(neko.state[0..2].as_bytes(), &message); let expected_state = [ - 0x14bf7a55ecbfef9f, - 0xbf881650acfa3419, - 0x19e0cf77f80c9b9c, - 0x926cb071dcc542bf, - 0xb0c2ff19b2a1088a, - 0x28e1786b57d258b5, - 0xa10f75b4a49c5830, - 0xe0d2a4c88992ddab, - 0xf26af8755e5d7ce8, - 0xa130fa26d8697da0, - 0x2d6266942d30bb73, - 0x0c47cacae106048e, - 0x1f78293be88890d7, - 0xcf24691274e96761, - 0x0501d35fa5a7db34, - 0xd242e3a40bf885f8, - 0x717c3e620ea21bdf, - 0x12af362391a15895, - 0x8df18b22be842ea5, - 0xa32fd58b7771026c, - 0x4a61af391e658ec5, - 0x189f225280d6d986, - 0x2718bc59d75e8d60, - 0x0be0f806c2086f5a, - 0xd5f2731cc9c0fed0, + 0x33e5964098e69bb2, + 0xe8aae76360864f1d, + 0x6c10b3c273ae582c, + 0xd9584d46c8025d46, + 0x8eeace52fffacd4c, + 0x2346ce9726155884, + 0x0f3427af0a3c77f1, + 0xe2706ecbbd9596b4, + 0x840b7500b73e537c, + 0x0015960758c2e30e, + 0xf2cd5efad521e8e2, + 0xca7199cf34822634, + 0xe21f1c3744135b1a, + 0xe91599f57a74f2c9, + 0x1395bb13bd8eec8d, + 0x8417dd11dfee0671, + 0x95d9c20086520a10, + 0x90cfb46fc5a4963d, + 0x2aaf5cd4d234a06d, + 0x5e4372caf96bd84a, + 0x6a858536f819bb62, + 0xf7f33ca323c59700, + 0x38b5d9a41b4d08e1, + 0x39c33af857ae9a82, + 0x39ed20d798ecd321, ]; assert_eq!(&neko.state, &expected_state); @@ -188,29 +223,29 @@ fn cipher_ops_permute_state_by_default() { let ratched_expected_state = [ 0x0000000000000000, 0x0000000000000000, - 0xaeaae19feedba131, - 0x142c658c8c5d4d41, - 0x5b15a77c99a73daa, - 0xe8060d814eb54fe2, - 0xaf03a57bc4107f06, - 0x20a98fb94a745e23, - 0x311dd2be1529baad, - 0x4d50f23f34057523, - 0xea89ea72449476d5, - 0x82457373a57d4062, - 0x0aad58af56986ea2, - 0x8548e7cb9b7907c3, - 0xca95c689bed5fbbb, - 0x426b6a66b930e5ad, - 0x514c8865b88f89ea, - 0x361e3d8a73cd12f1, - 0xbcd3e3c6949cec98, - 0x6b210034af33aeac, - 0x2d06ca5415ba4459, - 0x57dfabd826619f1e, - 0x69f4dd80c2791cc4, - 0xd9086f9fde008f52, - 0xd71233e8001d2c80, + 0xe72010d5d3b254c0, + 0x34007830a1c7585d, + 0xbcd95dec900847a5, + 0xfe1be2676e130078, + 0xe6c29c4c48f292e6, + 0x3d711aed9763e259, + 0xa9b1329692f19ebd, + 0xf1378893d98ad184, + 0xe6f31bb95f5c361f, + 0x549decbceeac0f78, + 0x81b85f3f3d3a687d, + 0x7dac55db9b73bf34, + 0x6f07454e89ec5950, + 0x9abd19c6e33eec92, + 0x7358043c28de6955, + 0x625421243d6b4bd5, + 0xc986349494886128, + 0xc00e8e52d7734dff, + 0xbafa4f2b57d93144, + 0x022f1aa724503cd5, + 0x4b3633798cc9ae5e, + 0x532b723068ab8c72, + 0xa48327750108017c, ]; assert_eq!(&neko.state, &ratched_expected_state); diff --git a/wharrgarbl-neko/src/lib.rs b/wharrgarbl-neko/src/lib.rs index 85cefce..77a9f37 100644 --- a/wharrgarbl-neko/src/lib.rs +++ b/wharrgarbl-neko/src/lib.rs @@ -12,32 +12,41 @@ use core::marker::PhantomData; use aead::{ KeySizeUser, + common::IvSizeUser, consts::{U4, U10, U16, U25, U32, U128, U256}, }; use ctutils::CtEq; use hybrid_array::Array; use wharrgarbl_utils::OpFlags; +use zerocopy::IntoBytes; use crate::operators::{NekoOperate, NekoOperateMut}; pub use crate::traits::NekoSec; pub type Neko128 = U128; pub type Neko256 = U256; -pub type NekoNonce = Array; +pub type NekoNonce = Array as IvSizeUser>::IvSize>; pub type NekoKey = Array as KeySizeUser>::KeySize>; pub type NekoTag = Array; -pub static NEKO_VERSION: &str = "NEKOv0.1.0"; +pub static NEKO_VERSION: &str = "NEKOv0.2.0"; +const U64_CHUNK: usize = core::mem::size_of::(); +const MAX_OPS: usize = core::mem::size_of::(); impl KeySizeUser for NekoState { type KeySize = U32; } +impl IvSizeUser for NekoState { + type IvSize = U16; +} + #[derive(Clone)] pub struct NekoState { pub(crate) state: Array, position: usize, - start: usize, + ops_count: usize, + pub(crate) ops_stack: Array, sec: PhantomData, } @@ -45,7 +54,8 @@ impl zeroize::Zeroize for NekoState { fn zeroize(&mut self) { self.state.zeroize(); self.position.zeroize(); - self.position.zeroize(); + self.ops_count.zeroize(); + self.ops_stack.zeroize(); } } @@ -58,156 +68,160 @@ impl Drop for NekoState { } impl NekoState { + /// Create a new [`NekoState`] instance. It takes a protocol bytestring and encodes that + /// into the state, returning an initialised instance. + /// + /// ``` + /// use wharrgarbl_neko::{NekoState, Neko128}; + /// + /// let neko = NekoState::::new(b"whimsical"); + /// + /// assert_eq!(format!("{neko}"), "NEKOv0.2.0/1600-128"); + /// ``` pub fn new(protocol: &[u8]) -> Self { - let mut strobe = Self { + // OPS stack MUST be initialised with the INIT flag as the first op. + let ops_stack = [ops::INIT.bits(), 0, 0, 0]; + + let mut neko = Self { + // The buffer state is the zeroed buffer layout for KeccakF1600: [u64; 25] state: Array([0u64; keccak::PLEN]), + // No bytes have been loaded into the buffer yet, therefore set to zero position: 0, - start: 0, + // INIT op has been loaded into the stack, therefore op count must be 1. + ops_count: 1, + ops_stack: Array(ops_stack), sec: PhantomData, }; + // Preamble is defined with 0x01 as the first byte, the RATE as the second byte + // and then 0x07 & 0x60 as the third & fourth byte. let preamble: Array = Array::from([0x01, Sec::rate() as u8, 0x07, 0x60]); - // This is safe because the static string is always 10 bytes long. + // This is safe because the specification version string is always 10 bytes long. let version: Array = Array::try_from(NEKO_VERSION.as_bytes()).unwrap(); + // The NEKO specification version is concatenated to the end of the preamble let combined = preamble.concat(version); - strobe.overwrite(&combined); - strobe.overwrite(protocol); + // These two operations below DO NOT increment the ops counter, because it is defined + // as the INIT operation. + // Write the combined preamble+version to the zeroed buffer + NekoOperate::overwrite(&mut neko, &combined); + // Write the protocol byte string to the zeroed buffer + NekoOperate::overwrite(&mut neko, protocol); - strobe + // INIT operation has concluded, return the finalised NEKO state + neko } #[inline] + #[track_caller] fn begin_op(&mut self, red_flags: OpFlags) { - let old_start = self.start; - self.start = self.position + 1; - - self.absorb(&[ - old_start as u8, - red_flags.bits(), - b'n', - b'y', - b'a', - b'a', - b'n', - b'~', - ]); - } - - fn permutation_f(&mut self) { - let permuter = [ - self.start as u8, - self.position as u8, - 0x04, - 0x80, - b'M', - b'E', - b'O', - b'W', - ]; - - // XOR the permuter into first entropy block - self.state[Sec::rate()] ^= u64::from_le_bytes(permuter); - - keccak::Keccak::new().with_f1600(|permute| permute(&mut self.state.0)); - - self.position = 0; - self.start = 0; + // We can chain upto 4 OPs before needing to permute. + // So we can stack KEY+NONCE+AD ops before the next one + // MUST be a permuting op, like ENC/MAC/RATCHET/PRF. + // If we go over this, we've hit a failure state and MUST + // terminate. + assert!(self.ops_count < MAX_OPS); + + // Post-increment the ops counter. + let op_index = self.ops_count; + self.ops_count += 1; + + // Encode the opflags to the available stack slot. + self.ops_stack[op_index] = red_flags.bits(); } - #[inline] - fn absorb(&mut self, data: &[u8]) { - NekoOperate::new(self, data).operate(|(state, byte)| *state ^= *byte); - } - - #[inline] - fn absorb_and_set(&mut self, data: &mut [u8]) { - NekoOperateMut::new(self, data).operate_mut(|(state, byte)| { - *state ^= *byte; - *byte = *state; - }); - } + fn permutation_f(&mut self, continuation: OpFlags) { + // Last byte is zeroed in case the terminator overlaps + let permuter: Array = + Array([self.ops_count as u8, self.position as u8, 0x80u8.to_le(), 0]); - #[inline] - fn copy_state(&mut self, data: &mut [u8]) { - NekoOperateMut::new(self, data).operate_mut(|(state, byte)| *byte = *state); - } + let permuter_block = u64::from_ne_bytes(self.ops_stack.concat(permuter).0); - #[inline] - fn exchange(&mut self, data: &mut [u8]) { - NekoOperateMut::new(self, data).operate_mut(|(state, byte)| { - *byte ^= *state; - *state ^= *byte; - }); - } + // XOR the permuter block into the next block, spilling into the first entropy block + // if required + self.state[self.position.div_ceil(U64_CHUNK) + 1] ^= permuter_block; + // Flip a bit in the last byte of the first entropy block with a 1 to act as the padding terminator. + // The bit is selected via rotating right the value 0x80 (0b1000_0000) by the position counter. + self.state[Sec::rate()].as_mut_bytes()[7] ^= + 0x80u8.to_le().rotate_right(self.position as u32); - #[inline] - fn overwrite(&mut self, data: &[u8]) { - NekoOperate::new(self, data).operate(|(state, byte)| *state = *byte); - } + // The state has been fully prepared, and now can be permuted by the F1600 function. + keccak::Keccak::new().with_f1600(|permute| permute(&mut self.state.0)); - #[inline] - fn squeeze(&mut self, data: &mut [u8]) { - NekoOperateMut::new(self, data).operate_mut(|(state, byte)| { - *byte = *state; - *state = 0; - }); + // Reset the state, zeroing all counters/stack unless a CONTINUATION, in which case + // ops count is set to 1 and the first op slot is encoded with 0x01. + self.position = 0; + self.ops_count = continuation.bits() as usize; + self.ops_stack = Array([continuation.bits(), 0, 0, 0]); } #[inline] fn zero_state(&mut self) { + // Select the amount of bytes to zero, according to Security level + // 128 bits = 16 bytes to zero out to achieve forward secrecy + // 256 bits = 32 bytes to zero out to achieve forward secrecy let ratchet_bytes = Sec::ratchet_bytes(); self.state[0..ratchet_bytes].iter_mut().for_each(|block| { *block = 0; }); - self.position += ratchet_bytes; + self.position += ratchet_bytes * U64_CHUNK; } /// Sets a provided key into the cipher state. + /// + /// **This is a NON-PERMUTING operation**. pub fn key(&mut self, data: &NekoKey) { self.begin_op(ops::KEY); - self.overwrite(data); + NekoOperate::overwrite(self, data); } /// Absorbs a nonce value into the cipher state - pub fn nonce(&mut self, data: &NekoNonce) { + /// + /// **This is a NON-PERMUTING operation**. + pub fn nonce(&mut self, data: &NekoNonce) { self.begin_op(ops::NONCE); - self.absorb(data); + NekoOperate::absorb(self, data); } /// Absorb associated data into the cipher. + /// + /// **This is a NON-PERMUTING operation**. pub fn ad(&mut self, data: &[u8]) { self.begin_op(ops::AD); - self.absorb(data); + NekoOperate::absorb(self, data); } /// Pseudo-random Function. Used to generate new keys/nonces from the /// cipher state. + /// + /// **This is a PERMUTING operation** pub fn prf(&mut self, data: &mut [u8]) { - self.begin_op(ops::PRF); + self.permutation_f(ops::RST); - self.permutation_f(); + self.begin_op(ops::PRF); - self.squeeze(data); + NekoOperateMut::squeeze(self, data); } /// Create a message authentication code (MAC). This is used to validate the resulting /// state after ingesting/encrypting data. + /// + /// **This is a PERMUTING operation** pub fn create_mac(&mut self) -> NekoTag { - self.begin_op(ops::MAC); + self.permutation_f(ops::RST); - self.permutation_f(); + self.begin_op(ops::MAC); let mut tag: NekoTag = Default::default(); - self.copy_state(&mut tag); + NekoOperateMut::copy_state(self, &mut tag); tag } @@ -215,14 +229,19 @@ impl NekoState { /// Validates a provided MAC. After ingesting/decrypting data, the resulting state /// of the cipher should be the same as the sender's. The MAC validates that this /// is correct. Any differences in length/bits/etc, should result in an invalid MAC. + /// + /// **This is a PERMUTING operation** pub fn verify_mac(&mut self, data: &NekoTag) -> aead::Result<()> { - self.begin_op(ops::MAC); + self.permutation_f(ops::RST); - self.permutation_f(); + self.begin_op(ops::MAC); + // Copy the original MAC, because we are going to mutate it as part of validation. + // If the MAC becomes all zeros, then it is valid. Any 1 bits in the + // mutated copy indicates an invalid MAC. let mut mac_copy = *data; - self.exchange(&mut mac_copy); + NekoOperateMut::exchange(self, &mut mac_copy); let all_zero: NekoTag = Default::default(); @@ -234,40 +253,46 @@ impl NekoState { } /// Takes a cleartext buffer, and encrypts it in place. + /// + /// **This is a PERMUTING operation** pub fn encrypt(&mut self, data: &mut [u8]) { - self.begin_op(ops::ENC); + self.permutation_f(ops::RST); - self.permutation_f(); + self.begin_op(ops::ENC); - self.absorb_and_set(data); + NekoOperateMut::absorb_and_set(self, data); } /// Takes a ciphertext buffer, and decrypts it in place. + /// + /// **This is a PERMUTING operation** pub fn decrypt(&mut self, data: &mut [u8]) { - self.begin_op(ops::ENC); + self.permutation_f(ops::RST); - self.permutation_f(); + self.begin_op(ops::ENC); - self.exchange(data); + NekoOperateMut::exchange(self, data); } /// This is for mixing a cleartext message into the state. This is meant to be used /// both for when you send the cleartext message on one side, then when you receive the /// message on the other side. + /// + /// **This is a NON-PERMUTING operation**. pub fn cleartext(&mut self, data: &[u8]) { self.begin_op(ops::CLR); - self.permutation_f(); - - self.absorb(data); + NekoOperate::absorb(self, data); } /// Permutes and zeros a portion of the cipher state in order to provide forward secrecy. /// The amount of bits zeroed is dependent on the cipher strength (128/256). + /// + /// **This is a PERMUTING operation** pub fn ratchet(&mut self) { - self.begin_op(ops::RATCHET); + self.permutation_f(ops::RST); - self.permutation_f(); + self.begin_op(ops::RATCHET); self.zero_state(); } @@ -275,14 +300,13 @@ impl NekoState { impl core::fmt::Display for NekoState { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str(NEKO_VERSION)?; - write!(f, "/1600-{}", S::to_usize()) + write!(f, "{}/1600-{}", NEKO_VERSION, S::to_usize()) } } impl core::fmt::Debug for NekoState { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - // Do not reveal internal state of StrobeState, other than its security level + // Do not reveal internal state of NekoState, other than its security level f.debug_struct("NekoState") .field("sec", &S::to_usize()) .field("version", &NEKO_VERSION) @@ -303,10 +327,10 @@ mod tests { let display = std::format!("{s}"); let debug = std::format!("{s:?}"); - assert_eq!(&display, "NEKOv0.1.0/1600-128"); + assert_eq!(&display, "NEKOv0.2.0/1600-128"); assert_eq!( &debug, - "NekoState { sec: 128, version: \"NEKOv0.1.0\", .. }" + "NekoState { sec: 128, version: \"NEKOv0.2.0\", .. }" ); } } diff --git a/wharrgarbl-neko/src/operators.rs b/wharrgarbl-neko/src/operators.rs index 5c3439a..18e84df 100644 --- a/wharrgarbl-neko/src/operators.rs +++ b/wharrgarbl-neko/src/operators.rs @@ -1,8 +1,6 @@ use zerocopy::IntoBytes; -use crate::{NekoSec, NekoState}; - -const U64_CHUNK: usize = core::mem::size_of::(); +use crate::{NekoSec, NekoState, U64_CHUNK, ops}; pub(crate) struct NekoOperateMut<'s, S: NekoSec> { neko: &'s mut NekoState, @@ -10,86 +8,107 @@ pub(crate) struct NekoOperateMut<'s, S: NekoSec> { } impl<'s, S: NekoSec> NekoOperateMut<'s, S> { - #[inline] - pub(crate) const fn new(neko: &'s mut NekoState, data: &'s mut [u8]) -> Self { + #[inline(always)] + const fn new(neko: &'s mut NekoState, data: &'s mut [u8]) -> Self { Self { neko, data } } - #[inline] - fn next_block(&self) -> Option { - match self - .data - .len() - .min((S::rate() - self.neko.position) * U64_CHUNK) - { - 0 => None, - take => Some(take), - } - } - - #[inline] - pub(crate) fn operate_mut(&'s mut self, operation: fn((&mut u8, &mut u8))) { - while let Some(take) = self.next_block() { - let (block, rest) = self.data.split_at_mut(take); - - self.data = rest; - + #[inline(always)] + fn operate_mut(&'s mut self, operation: fn((&mut u8, &mut u8))) { + loop { // Trans the neko - let transed_bytes = self.neko.state[self.neko.position..].as_mut_bytes(); + let transed_bytes = + self.neko.state[self.neko.position.div_ceil(U64_CHUNK)..S::rate()].as_mut_bytes(); + + let take = transed_bytes + .iter_mut() + .zip(self.data.iter_mut()) + .map(operation) + .count(); - transed_bytes.iter_mut().zip(block).for_each(operation); + self.data = &mut self.data[take..]; if !self.data.is_empty() { - self.neko.permutation_f(); + self.neko.permutation_f(ops::CONT); } else { - self.neko.position += take.div_ceil(U64_CHUNK); + self.neko.position += take; break; } } } -} -pub(crate) struct NekoOperate<'s, S: NekoSec> { - neko: &'s mut NekoState, - data: &'s [u8], -} + #[inline] + pub(crate) fn absorb_and_set(neko: &'s mut NekoState, data: &'s mut [u8]) { + NekoOperateMut::new(neko, data).operate_mut(|(state, byte)| { + *state ^= *byte; + *byte = *state; + }); + } -impl<'s, S: NekoSec> NekoOperate<'s, S> { #[inline] - pub(crate) const fn new(neko: &'s mut NekoState, data: &'s [u8]) -> Self { - Self { neko, data } + pub(crate) fn copy_state(neko: &'s mut NekoState, data: &'s mut [u8]) { + NekoOperateMut::new(neko, data).operate_mut(|(state, byte)| *byte = *state); } #[inline] - fn next_block(&self) -> Option { - match self - .data - .len() - .min((S::rate() - self.neko.position) * U64_CHUNK) - { - 0 => None, - take => Some(take), - } + pub(crate) fn exchange(neko: &'s mut NekoState, data: &'s mut [u8]) { + NekoOperateMut::new(neko, data).operate_mut(|(state, byte)| { + *byte ^= *state; + *state ^= *byte; + }); } #[inline] - pub(crate) fn operate(&'s mut self, operation: fn((&mut u8, &u8))) { - while let Some(take) = self.next_block() { - let (block, rest) = self.data.split_at(take); + pub(crate) fn squeeze(neko: &'s mut NekoState, data: &'s mut [u8]) { + NekoOperateMut::new(neko, data).operate_mut(|(state, byte)| { + *byte = *state; + *state = 0; + }); + } +} - self.data = rest; +pub(crate) struct NekoOperate<'s, S: NekoSec> { + neko: &'s mut NekoState, + data: &'s [u8], +} + +impl<'s, S: NekoSec> NekoOperate<'s, S> { + #[inline(always)] + const fn new(neko: &'s mut NekoState, data: &'s [u8]) -> Self { + Self { neko, data } + } + #[inline(always)] + fn operate(&'s mut self, operation: fn((&mut u8, &u8))) { + loop { // Trans the neko - let transed_bytes = self.neko.state[self.neko.position..].as_mut_bytes(); + let transed_bytes = + self.neko.state[self.neko.position.div_ceil(U64_CHUNK)..S::rate()].as_mut_bytes(); + + let take = transed_bytes + .iter_mut() + .zip(self.data) + .map(operation) + .count(); - transed_bytes.iter_mut().zip(block).for_each(operation); + self.data = &self.data[take..]; if !self.data.is_empty() { - self.neko.permutation_f(); + self.neko.permutation_f(ops::CONT); } else { - self.neko.position += take.div_ceil(U64_CHUNK); + self.neko.position += take; break; } } } + + #[inline] + pub(crate) fn absorb(neko: &'s mut NekoState, data: &'s [u8]) { + NekoOperate::new(neko, data).operate(|(state, byte)| *state ^= *byte); + } + + #[inline] + pub(crate) fn overwrite(neko: &'s mut NekoState, data: &'s [u8]) { + NekoOperate::new(neko, data).operate(|(state, byte)| *state = *byte); + } } diff --git a/wharrgarbl-neko/src/ops.rs b/wharrgarbl-neko/src/ops.rs index faacd80..eecbd5b 100644 --- a/wharrgarbl-neko/src/ops.rs +++ b/wharrgarbl-neko/src/ops.rs @@ -2,12 +2,28 @@ use wharrgarbl_utils::OpFlags; use crate::flags::*; +// Internal State machine flags. These only get used during internal ops, and are +// not invoked directly via user ops. +pub const RST: OpFlags = OpFlags::EMPTY; +pub const CONT: OpFlags = OpFlags::new(TRANSPORT.bits()); + +// All User operations are defined below. All valid combinations must have either +// two flags or three flags. 0, 1 or 4 flags toggled are INVALID for User operations +// and are reserved for internal usage only. +// All higher bits on OpFlags are also reserved, but for v0.2 Spec, are ALWAYS invalid +// and should NEVER be used. If these are added, it will be for a new spec revision. + +// Message related ops, either for encrypting, MAC, or ingesting cleartext pub const CLR: OpFlags = OpFlags::new(APP.bits() | TRANSPORT.bits()); pub const MAC: OpFlags = OpFlags::new(CIPHER.bits() | TRANSPORT.bits()); pub const ENC: OpFlags = OpFlags::new(APP.bits() | CIPHER.bits() | TRANSPORT.bits()); -pub const KEY: OpFlags = OpFlags::new(META.bits()); +// Cipher related ops, either for init/loading keys/nonces/associated data, or for +// cryptographic operations like ratchet for forward secrecy, or prf for expanding +// new keys/nonces/other randomised data. +pub const INIT: OpFlags = OpFlags::new(META.bits() | APP.bits()); pub const NONCE: OpFlags = OpFlags::new(META.bits() | TRANSPORT.bits()); -pub const AD: OpFlags = OpFlags::new(META.bits() | APP.bits()); -pub const RATCHET: OpFlags = OpFlags::new(META.bits() | CIPHER.bits()); +pub const AD: OpFlags = OpFlags::new(META.bits() | APP.bits() | TRANSPORT.bits()); +pub const KEY: OpFlags = OpFlags::new(META.bits() | CIPHER.bits()); +pub const RATCHET: OpFlags = OpFlags::new(META.bits() | TRANSPORT.bits() | CIPHER.bits()); pub const PRF: OpFlags = OpFlags::new(META.bits() | APP.bits() | CIPHER.bits()); diff --git a/wharrgarbl-neko/src/traits.rs b/wharrgarbl-neko/src/traits.rs index b396ff4..6c089c5 100644 --- a/wharrgarbl-neko/src/traits.rs +++ b/wharrgarbl-neko/src/traits.rs @@ -1,17 +1,17 @@ -use aead::consts::{U128, U256}; +use aead::consts::{U128, U200, U256}; use hybrid_array::typenum::Unsigned; pub trait NekoSec: Unsigned { fn to_bytes() -> [u8; 2] { - Self::to_u16().to_le_bytes() + Self::U16.to_le_bytes() } fn ratchet_bytes() -> usize { - Self::to_usize().wrapping_shr(6) + Self::USIZE.wrapping_shr(6) } fn rate() -> usize { - keccak::PLEN - Self::ratchet_bytes() - 1 + (U200::USIZE - Self::USIZE / 4) / 8 - 1 } } diff --git a/wharrgarbl-utils/src/opflags.rs b/wharrgarbl-utils/src/opflags.rs index 1caf7bc..9a9cf51 100644 --- a/wharrgarbl-utils/src/opflags.rs +++ b/wharrgarbl-utils/src/opflags.rs @@ -9,6 +9,7 @@ pub struct OpFlags(u8); impl OpFlags { pub const EMPTY: OpFlags = OpFlags(0); + #[inline(always)] pub const fn new(val: u8) -> Self { Self(val) }