diff --git a/.changes/add-aes-cbc.md b/.changes/add-aes-cbc.md new file mode 100644 index 00000000..b47b3883 --- /dev/null +++ b/.changes/add-aes-cbc.md @@ -0,0 +1,6 @@ +--- +"iota-crypto": minor +--- + +Add AES-CBC algorithms (`Aes128CbcHmac256`, `Aes192CbcHmac384`, `Aes256CbcHmac512`). + diff --git a/Cargo.toml b/Cargo.toml index 97c4d866..72c91323 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,12 +25,14 @@ name = "crypto" [features] default = [ ] std = [ ] +aes-cbc = [ "aes-crate", "block-modes", "cipher", "hmac", "sha", "subtle" ] aes-kw = [ "aes-crate" ] +aes-gcm = [ "aes-gcm-crate", "cipher"] chacha = [ "chacha20poly1305", "cipher" ] ed25519 = [ "ed25519-zebra" ] x25519 = [ "x25519-dalek", "curve25519-dalek" ] random = [ "getrandom" ] -aes = [ "aes-gcm", "cipher" ] +aes = [ "aes-cbc", "aes-gcm", "aes-kw" ] blake2b = [ "blake2", "digest" ] ternary_hashes = [ ] curl-p = [ "ternary_hashes", "bee-ternary", "lazy_static/spin_no_std" ] @@ -62,9 +64,10 @@ slip10 = [ "hmac", "sha", "ed25519", "random", "serde" ] cipher = [ "aead", "generic-array" ] [dependencies] +block-modes = { version = "0.8", optional = true, default-features = false } aead = { version = "0.4", optional = true, default-features = false } aes-crate = { version = "0.7", optional = true, default-features = false, package = "aes" } -aes-gcm = { version = "0.9", optional = true, default-features = false, features = [ "aes" ] } +aes-gcm-crate = { version = "0.9", optional = true, default-features = false, package = "aes-gcm", features = [ "aes" ] } bee-common-derive = { version = "0.1.1-alpha", optional = true, default-features = false } bee-ternary = { version = "0.6.0", optional = true, default-features = false } blake2 = { version = "0.9", optional = true, default-features = false } @@ -78,6 +81,7 @@ hmac_ = { version = "0.11", optional = true, default-features = false, package = lazy_static = { version = "1.4", optional = true, default-features = false } pbkdf2 = { version = "0.8", optional = true, default-features = false } rand = { version = "0.8", optional = true, default-features = false } +subtle = { version = "2.4", default-features = false, optional = true } sha2 = { version = "0.9", optional = true, default-features = false } serde = { version = "1.0", optional = true, features = [ "derive" ] } sha3 = { version = "0.9", optional = true, default-features = false } diff --git a/README.md b/README.md index c5e3335b..c61b3de7 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ To be included in this list an implementation must: | ciphers | AES-256-GCM | [`aes`](/src/ciphers/aes.rs) | [spec][AES-GCM-SPEC] | `aes-gcm` | [nist][AES-GCM-TEST] | ★★★☆☆ | | ciphers | AES-KW | [`aes-kw`](/src/ciphers/aes_kw.rs) | [spec][AES-GCM-SPEC] | `aes-crate` | [nist][AES-GCM-TEST] | ★★★☆☆ | | ciphers | XCHACHA20-POLY1305 | [`chacha`](/src/ciphers/chacha.rs) | [rfc][XCHACHA-RFC] | `chacha20poly1305` | [official][XCHACHA-TEST] | ★★★★★ | +| ciphers | AES-CBC | [`aes-cbc`](/src/aes_cbc.rs) | [rfc](https://tools.ietf.org/html/rfc7518) | `crypto.rs` | [official](https://tools.ietf.org/html/rfc7518#appendix-B) | ☆☆☆☆☆ | | hashes | BLAKE2b-160 | [`blake2b`](/src/hashes/blake2b.rs) | [rfc][BLAKE2B-RFC] | `blake2` | [official][BLAKE2B-TEST] | ★★★★☆ | | hashes | BLAKE2b-256 | [`blake2b`](/src/hashes/blake2b.rs) | [rfc][BLAKE2B-RFC] | `blake2` | [official][BLAKE2B-TEST] | ★★★★☆ | | hashes | CURL-P | [`curl-p`](/src/hashes/ternary/curl_p/mod.rs) | [rfc][CURL-RFC] | `bee-ternary` | official | ★★☆☆☆ | diff --git a/src/ciphers/aes_cbc.rs b/src/ciphers/aes_cbc.rs new file mode 100644 index 00000000..12739669 --- /dev/null +++ b/src/ciphers/aes_cbc.rs @@ -0,0 +1,192 @@ +// Copyright 2020 IOTA Stiftung +// SPDX-License-Identifier: Apache-2.0 + +use aes_crate::{Aes128, Aes192, Aes256, BlockCipher, NewBlockCipher}; +use block_modes::{block_padding::Pkcs7, BlockMode, Cbc}; + +use core::{ + marker::PhantomData, + num::NonZeroUsize, + ops::{Shl, Sub}, +}; +use generic_array::{ + sequence::Split, + typenum::{Double, Unsigned, B1, U16, U24, U32}, + ArrayLength, +}; +use hmac_::{Hmac, Mac, NewMac}; +use sha2::{ + digest::{BlockInput, FixedOutput, Reset, Update}, + Sha256, Sha384, Sha512, +}; +use subtle::ConstantTimeEq; + +use crate::ciphers::traits::{Aead, Key, Nonce, Tag}; + +/// AES-CBC using 128-bit key and HMAC SHA-256. +pub type Aes128CbcHmac256 = AesCbc; + +/// AES-CBC using 192-bit key and HMAC SHA-384. +pub type Aes192CbcHmac384 = AesCbc; + +/// AES-CBC using 256-bit key and HMAC SHA-512. +pub type Aes256CbcHmac512 = AesCbc; + +type AesCbcPkcs7 = Cbc; + +type NonceLength = U16; + +type DigestOutput = ::OutputSize; + +/// AES in Cipher Block Chaining mode with PKCS #7 padding and HMAC +/// +/// See [RFC7518#Section-5.2](https://tools.ietf.org/html/rfc7518#section-5.2) +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AesCbc { + cipher: PhantomData, + digest: PhantomData, + key_len: PhantomData, + tag_len: PhantomData, +} + +impl AesCbc +where + Cipher: BlockCipher + NewBlockCipher, +{ + const BLOCK_SIZE: usize = <::BlockSize as Unsigned>::USIZE; +} + +impl AesCbc +where + Cipher: BlockCipher + + NewBlockCipher + + block_modes::cipher::BlockCipher + + block_modes::cipher::NewBlockCipher + + aes_crate::BlockEncrypt + + aes_crate::BlockDecrypt, + Digest: Clone + Default + BlockInput + FixedOutput + Reset + Update, + KeyLen: ArrayLength + Shl, + TagLen: ArrayLength, + Double: ArrayLength, + DigestOutput: ArrayLength + Sub, +{ + fn compute_tag( + key: &Key, + nonce: &Nonce, + associated_data: &[u8], + ciphertext: &[u8], + ) -> crate::Result> { + // The octet string AL is equal to the number of bits in the Additional + // Authenticated Data A expressed as a 64-bit unsigned big-endian integer. + // + // A message Authentication Tag T is computed by applying HMAC to the + // following data, in order: + // + // the Additional Authenticated Data A, + // the Initialization Vector IV, + // the ciphertext E computed in the previous step, and + // the octet string AL defined above. + // + // The string MAC_KEY is used as the MAC key. We denote the output + // of the MAC computed in this step as M. The first T_LEN octets of + // M are used as T. + let mut hmac: Hmac = + Hmac::new_from_slice(&key[..KeyLen::USIZE]).map_err(|_| crate::Error::CipherError { alg: Self::NAME })?; + + hmac.update(associated_data); + hmac.update(nonce); + hmac.update(ciphertext); + hmac.update(&((associated_data.len() as u64) * 8).to_be_bytes()); + + Ok(Split::split(hmac.finalize().into_bytes()).0) + } + + fn cipher(key: &Key, nonce: &Nonce) -> crate::Result> { + AesCbcPkcs7::new_from_slices(&key[KeyLen::USIZE..], nonce) + .map_err(|_| crate::Error::CipherError { alg: Self::NAME }) + } +} + +impl Aead for AesCbc +where + Cipher: BlockCipher + NewBlockCipher + aes_crate::BlockEncrypt + aes_crate::BlockDecrypt, + Digest: Clone + Default + BlockInput + FixedOutput + Reset + Update, + KeyLen: ArrayLength + Shl, + TagLen: ArrayLength, + Double: ArrayLength, + DigestOutput: ArrayLength + Sub, +{ + type KeyLength = Double; + type NonceLength = NonceLength; + type TagLength = TagLen; + + const NAME: &'static str = "AES-CBC"; + + fn encrypt( + key: &Key, + nonce: &Nonce, + associated_data: &[u8], + plaintext: &[u8], + ciphertext: &mut [u8], + tag: &mut Tag, + ) -> crate::Result<()> { + let padding: usize = Self::padsize(plaintext).map(NonZeroUsize::get).unwrap_or_default(); + let expected: usize = plaintext.len() + padding; + + if expected > ciphertext.len() { + return Err(crate::Error::BufferSize { + name: "ciphertext", + needs: expected, + has: ciphertext.len(), + }); + } + + let cipher: AesCbcPkcs7 = Self::cipher(key, nonce)?; + let length: usize = plaintext.len(); + + ciphertext[..length].copy_from_slice(plaintext); + + cipher + .encrypt(ciphertext, length) + .map_err(|_| crate::Error::CipherError { alg: Self::NAME })?; + + tag.copy_from_slice(&Self::compute_tag(key, nonce, associated_data, ciphertext)?); + + Ok(()) + } + + fn decrypt( + key: &Key, + nonce: &Nonce, + associated_data: &[u8], + plaintext: &mut [u8], + ciphertext: &[u8], + tag: &Tag, + ) -> crate::Result { + if ciphertext.len() > plaintext.len() { + return Err(crate::Error::BufferSize { + name: "plaintext", + needs: ciphertext.len(), + has: plaintext.len(), + }); + } + + let cipher: AesCbcPkcs7 = Self::cipher(key, nonce)?; + let computed: Tag = Self::compute_tag(key, nonce, associated_data, ciphertext)?; + + if !bool::from(computed.ct_eq(tag)) { + return Err(crate::Error::CipherError { alg: Self::NAME }); + } + + plaintext[..ciphertext.len()].copy_from_slice(ciphertext); + + cipher + .decrypt(plaintext) + .map_err(|_| crate::Error::CipherError { alg: Self::NAME }) + .map(|output| output.len()) + } + + fn padsize(plaintext: &[u8]) -> Option { + NonZeroUsize::new(Self::BLOCK_SIZE - (plaintext.len() % Self::BLOCK_SIZE)) + } +} diff --git a/src/ciphers/aes.rs b/src/ciphers/aes_gcm.rs similarity index 79% rename from src/ciphers/aes.rs rename to src/ciphers/aes_gcm.rs index a1ac5d62..d9a5233c 100644 --- a/src/ciphers/aes.rs +++ b/src/ciphers/aes_gcm.rs @@ -3,5 +3,5 @@ use crate::ciphers::traits::consts::{U12, U16, U32}; -pub type Aes256Gcm = aes_gcm::Aes256Gcm; +pub type Aes256Gcm = aes_gcm_crate::Aes256Gcm; impl_aead!(Aes256Gcm, "AES-256-GCM", U32, U12, U16); diff --git a/src/ciphers/macros.rs b/src/ciphers/macros.rs index e9f83985..0241af7d 100644 --- a/src/ciphers/macros.rs +++ b/src/ciphers/macros.rs @@ -1,6 +1,7 @@ // Copyright 2020 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 +#[allow(unused_macros)] macro_rules! impl_aead { ($impl:ident, $name:expr, $key_len:ident, $nonce_len:ident, $tag_len:ident) => { impl $crate::ciphers::traits::Aead for $impl { diff --git a/src/ciphers/mod.rs b/src/ciphers/mod.rs index 754c9f5d..4090c1cc 100644 --- a/src/ciphers/mod.rs +++ b/src/ciphers/mod.rs @@ -9,9 +9,13 @@ mod macros; #[cfg_attr(docsrs, doc(cfg(feature = "chacha")))] pub mod chacha; -#[cfg(feature = "aes")] -#[cfg_attr(docsrs, doc(cfg(feature = "aes")))] -pub mod aes; +#[cfg(feature = "aes-gcm")] +#[cfg_attr(docsrs, doc(cfg(feature = "aes-gcm")))] +pub mod aes_gcm; + +#[cfg(feature = "aes-cbc")] +#[cfg_attr(docsrs, doc(cfg(feature = "aes-cbc")))] +pub mod aes_cbc; #[cfg(feature = "aes-kw")] #[cfg_attr(docsrs, doc(cfg(feature = "aes-kw")))] diff --git a/src/ciphers/traits.rs b/src/ciphers/traits.rs index e8dd74d2..6da9977f 100644 --- a/src/ciphers/traits.rs +++ b/src/ciphers/traits.rs @@ -22,11 +22,12 @@ pub type Tag = GenericArray::TagLength>; /// Example using [`Aes256Gcm`][`crate::ciphers::aes::Aes256Gcm`]: /// /// ```rust +/// # #[cfg(all(feature = "random", feature = "aes-gcm"))] +/// # { /// use crypto::ciphers::{ -/// aes::Aes256Gcm, +/// aes_gcm::Aes256Gcm, /// traits::{Aead, Key, Nonce, Tag}, /// }; -/// /// let plaintext: &[u8] = b"crypto.rs"; /// let associated_data: &[u8] = b"stronghodl"; /// let mut encrypted: Vec = vec![0; plaintext.len()]; @@ -43,6 +44,9 @@ pub type Tag = GenericArray::TagLength>; /// assert_eq!(decrypted, plaintext); /// /// # Ok::<(), crypto::Error>(()) +/// # } +/// # #[cfg(not(feature = "random"))] +/// # Ok::<(), crypto::Error>(()) /// ``` pub trait Aead { /// The size of the [`key`][`Key`] required by this algorithm. diff --git a/tests/aead.rs b/tests/aead.rs index 7f4118fc..a86e967e 100644 --- a/tests/aead.rs +++ b/tests/aead.rs @@ -1,7 +1,7 @@ // Copyright 2020 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#![cfg(any(feature = "aes", feature = "chacha"))] +#![cfg(any(feature = "aes-gcm", feature = "chacha", feature = "aes-cbc"))] mod utils; @@ -25,8 +25,9 @@ fn test_aead_one(tv: &TestVector) -> crypto::Result<()> { let expected_ctx = hex::decode(tv.ciphertext).unwrap(); let expected_tag = hex::decode(tv.tag).unwrap(); + let padding: usize = A::padsize(&ptx).map(|size| size.get()).unwrap_or_default(); - let mut ctx = vec![0; ptx.len()]; + let mut ctx = vec![0; ptx.len() + padding]; A::try_encrypt(&key, &nonce, &aad, &ptx, &mut ctx, &mut tag)?; assert_eq!(&ctx[..], &expected_ctx[..]); @@ -73,10 +74,10 @@ fn test_aead_all(tvs: &[TestVector]) -> crypto::Result<()> { Ok(()) } -#[cfg(feature = "aes")] +#[cfg(feature = "aes-gcm")] mod aes { use super::{test_aead_all, TestVector}; - use crypto::ciphers::aes::Aes256Gcm; + use crypto::ciphers::aes_gcm::Aes256Gcm; #[test] fn test_vectors_aes_256_gcm() { @@ -84,6 +85,27 @@ mod aes { } } +#[cfg(feature = "aes-cbc")] +mod aes_cbc { + use super::{test_aead_all, TestVector}; + use crypto::ciphers::aes_cbc::{Aes128CbcHmac256, Aes192CbcHmac384, Aes256CbcHmac512}; + + #[test] + fn test_vectors_aes_128_cbc_hmac_256() { + test_aead_all::(&include!("fixtures/aes_128_cbc_hmac_sha_256.rs")).unwrap(); + } + + #[test] + fn test_vectors_aes_192_cbc_hmac_384() { + test_aead_all::(&include!("fixtures/aes_192_cbc_hmac_sha_384.rs")).unwrap(); + } + + #[test] + fn test_vectors_aes_256_cbc_hmac_512() { + test_aead_all::(&include!("fixtures/aes_256_cbc_hmac_sha_512.rs")).unwrap(); + } +} + #[cfg(feature = "chacha")] mod chacha { use super::{test_aead_all, TestVector}; diff --git a/tests/ed25519.rs b/tests/ed25519.rs index 2a1a6617..71c79c20 100644 --- a/tests/ed25519.rs +++ b/tests/ed25519.rs @@ -124,7 +124,6 @@ fn test_eq_ord() -> crypto::Result<()> { assert!(pk == pk_eq); assert!(pk != pk_diff); assert!(pk > pk_diff); - assert!(pk <= pk_eq); Ok(()) } diff --git a/tests/fixtures/aes_128_cbc_hmac_sha_256.rs b/tests/fixtures/aes_128_cbc_hmac_sha_256.rs new file mode 100644 index 00000000..fee6eea0 --- /dev/null +++ b/tests/fixtures/aes_128_cbc_hmac_sha_256.rs @@ -0,0 +1,11 @@ +[ + // https://tools.ietf.org/html/rfc7518#appendix-B.1 + TestVector { + key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + nonce: "1af38c2dc2b96ffdd86694092341bc04", + associated_data: "546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673", + plaintext: "41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365", + ciphertext: "c80edfa32ddf39d5ef00c0b468834279a2e46a1b8049f792f76bfe54b903a9c9a94ac9b47ad2655c5f10f9aef71427e2fc6f9b3f399a221489f16362c703233609d45ac69864e3321cf82935ac4096c86e133314c54019e8ca7980dfa4b9cf1b384c486f3a54c51078158ee5d79de59fbd34d848b3d69550a67646344427ade54b8851ffb598f7f80074b9473c82e2db", + tag: "652c3fa36b0a7c5b3219fab3a30bc1c4", + }, +] diff --git a/tests/fixtures/aes_192_cbc_hmac_sha_384.rs b/tests/fixtures/aes_192_cbc_hmac_sha_384.rs new file mode 100644 index 00000000..56840387 --- /dev/null +++ b/tests/fixtures/aes_192_cbc_hmac_sha_384.rs @@ -0,0 +1,11 @@ +[ + // https://tools.ietf.org/html/rfc7518#appendix-B.2 + TestVector { + key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f", + nonce: "1af38c2dc2b96ffdd86694092341bc04", + associated_data: "546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673", + plaintext: "41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365", + ciphertext: "ea65da6b59e61edb419be62d19712ae5d303eeb50052d0dfd6697f77224c8edb000d279bdc14c1072654bd30944230c657bed4ca0c9f4a8466f22b226d1746214bf8cfc2400add9f5126e479663fc90b3bed787a2f0ffcbf3904be2a641d5c2105bfe591bae23b1d7449e532eef60a9ac8bb6c6b01d35d49787bcd57ef484927f280adc91ac0c4e79c7b11efc60054e3", + tag: "8490ac0e58949bfe51875d733f93ac2075168039ccc733d7", + }, +] diff --git a/tests/fixtures/aes_256_cbc_hmac_sha_512.rs b/tests/fixtures/aes_256_cbc_hmac_sha_512.rs new file mode 100644 index 00000000..f927ba59 --- /dev/null +++ b/tests/fixtures/aes_256_cbc_hmac_sha_512.rs @@ -0,0 +1,11 @@ +[ + // https://tools.ietf.org/html/rfc7518#appendix-B.3 + TestVector { + key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f", + nonce: "1af38c2dc2b96ffdd86694092341bc04", + associated_data: "546865207365636f6e64207072696e6369706c65206f662041756775737465204b6572636b686f666673", + plaintext: "41206369706865722073797374656d206d757374206e6f7420626520726571756972656420746f206265207365637265742c20616e64206974206d7573742062652061626c6520746f2066616c6c20696e746f207468652068616e6473206f662074686520656e656d7920776974686f757420696e636f6e76656e69656e6365", + ciphertext: "4affaaadb78c31c5da4b1b590d10ffbd3dd8d5d302423526912da037ecbcc7bd822c301dd67c373bccb584ad3e9279c2e6d12a1374b77f077553df829410446b36ebd97066296ae6427ea75c2e0846a11a09ccf5370dc80bfecbad28c73f09b3a3b75e662a2594410ae496b2e2e6609e31e6e02cc837f053d21f37ff4f51950bbe2638d09dd7a4930930806d0703b1f6", + tag: "4dd3b4c088a7f45c216839645b2012bf2e6269a8c56a816dbc1b267761955bc5", + }, +]