-
-
Notifications
You must be signed in to change notification settings - Fork 424
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#474] remove unnecessary Result-wrapping from
crypto.rs
- Loading branch information
1 parent
ab29fed
commit e144dca
Showing
6 changed files
with
277 additions
and
216 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,156 +1,244 @@ | ||
// | ||
// Copyright 2020 Signal Messenger, LLC. | ||
// Copyright 2020-2022 Signal Messenger, LLC. | ||
// SPDX-License-Identifier: AGPL-3.0-only | ||
// | ||
|
||
use std::convert::TryInto; | ||
use std::result::Result; | ||
//! Internal convenience wrappers for symmetric cryptographic primitives. | ||
use aes::cipher::{NewCipher, StreamCipher}; | ||
use aes::{Aes256, Aes256Ctr}; | ||
use arrayref::array_ref; | ||
use block_modes::block_padding::Pkcs7; | ||
use block_modes::{BlockMode, Cbc}; | ||
use displaydoc::Display; | ||
use hmac::{Hmac, Mac, NewMac}; | ||
use sha2::Sha256; | ||
use subtle::ConstantTimeEq; | ||
use thiserror::Error; | ||
|
||
#[derive(Debug)] | ||
pub(crate) enum EncryptionError { | ||
/// The key or IV is the wrong length. | ||
BadKeyOrIv, | ||
} | ||
/// bad ciphertext: {0} | ||
/// | ||
/// Either the input is malformed, or the MAC doesn't match on decryption. | ||
/// | ||
/// These cases should not be distinguished; message corruption can cause either problem. | ||
#[derive(Debug, Display, Error)] | ||
#[ignore_extra_doc_attributes] | ||
pub struct DecryptionError(pub &'static str); | ||
|
||
#[derive(Debug)] | ||
pub(crate) enum DecryptionError { | ||
/// The key or IV is the wrong length. | ||
BadKeyOrIv, | ||
/// Either the input is malformed, or the MAC doesn't match on decryption. | ||
/// | ||
/// These cases should not be distinguished; message corruption can cause either problem. | ||
BadCiphertext(&'static str), | ||
} | ||
// TODO: Could be nice to have a type-safe library for manipulating units of bytes safely. | ||
const BITS_PER_BYTE: usize = std::mem::size_of::<u8>() * 8; | ||
|
||
fn aes_256_ctr_encrypt(ptext: &[u8], key: &[u8]) -> Result<Vec<u8>, EncryptionError> { | ||
let key: [u8; 32] = key.try_into().map_err(|_| EncryptionError::BadKeyOrIv)?; | ||
/// The length of the key we use for AES encryption in this crate. | ||
pub const AES_256_KEY_SIZE: usize = 256 / BITS_PER_BYTE; | ||
|
||
let zero_nonce = [0u8; 16]; | ||
let mut cipher = Aes256Ctr::new(key[..].into(), zero_nonce[..].into()); | ||
/// The size of the generated nonce we use for AES encryption in this crate. | ||
pub const AES_NONCE_SIZE: usize = 128 / BITS_PER_BYTE; | ||
|
||
let mut ctext = ptext.to_vec(); | ||
cipher.apply_keystream(&mut ctext); | ||
Ok(ctext) | ||
} | ||
/// Use AES-256 in CTR mode. | ||
pub mod ctr { | ||
use super::*; | ||
|
||
fn aes_256_ctr_decrypt(ctext: &[u8], key: &[u8]) -> Result<Vec<u8>, DecryptionError> { | ||
aes_256_ctr_encrypt(ctext, key).map_err(|e| match e { | ||
EncryptionError::BadKeyOrIv => DecryptionError::BadKeyOrIv, | ||
}) | ||
} | ||
pub fn aes_256_ctr_encrypt(ptext: &[u8], key: &[u8; AES_256_KEY_SIZE]) -> Vec<u8> { | ||
let zero_nonce = [0u8; AES_NONCE_SIZE]; | ||
let mut cipher = Aes256Ctr::new(key[..].into(), zero_nonce[..].into()); | ||
|
||
pub(crate) fn aes_256_cbc_encrypt( | ||
ptext: &[u8], | ||
key: &[u8], | ||
iv: &[u8], | ||
) -> Result<Vec<u8>, EncryptionError> { | ||
match Cbc::<Aes256, Pkcs7>::new_from_slices(key, iv) { | ||
Ok(mode) => Ok(mode.encrypt_vec(ptext)), | ||
Err(block_modes::InvalidKeyIvLength) => Err(EncryptionError::BadKeyOrIv), | ||
let mut ctext = ptext.to_vec(); | ||
cipher.apply_keystream(&mut ctext); | ||
ctext | ||
} | ||
} | ||
|
||
pub(crate) fn aes_256_cbc_decrypt( | ||
ctext: &[u8], | ||
key: &[u8], | ||
iv: &[u8], | ||
) -> Result<Vec<u8>, DecryptionError> { | ||
if ctext.is_empty() || ctext.len() % 16 != 0 { | ||
return Err(DecryptionError::BadCiphertext( | ||
"ciphertext length must be a non-zero multiple of 16", | ||
)); | ||
pub fn aes_256_ctr_decrypt(ctext: &[u8], key: &[u8; AES_256_KEY_SIZE]) -> Vec<u8> { | ||
aes_256_ctr_encrypt(ctext, key) | ||
} | ||
|
||
let mode = | ||
Cbc::<Aes256, Pkcs7>::new_from_slices(key, iv).map_err(|_| DecryptionError::BadKeyOrIv)?; | ||
mode.decrypt_vec(ctext) | ||
.map_err(|_| DecryptionError::BadCiphertext("failed to decrypt")) | ||
} | ||
/// Length in bytes of the [`Hmac`] key used for [`aes_256_ctr_hmac_encrypt`] and | ||
/// [`aes_256_ctr_hmac_decrypt`]. | ||
pub const MAC_KEY_LENGTH: usize = 80 / BITS_PER_BYTE; | ||
|
||
pub(crate) fn hmac_sha256(key: &[u8], input: &[u8]) -> [u8; 32] { | ||
let mut hmac = | ||
Hmac::<Sha256>::new_from_slice(key).expect("HMAC-SHA256 should accept any size key"); | ||
hmac.update(input); | ||
hmac.finalize().into_bytes().into() | ||
} | ||
|
||
pub(crate) fn aes256_ctr_hmacsha256_encrypt( | ||
msg: &[u8], | ||
cipher_key: &[u8], | ||
mac_key: &[u8], | ||
) -> Result<Vec<u8>, EncryptionError> { | ||
let mut ctext = aes_256_ctr_encrypt(msg, cipher_key)?; | ||
let mac = hmac_sha256(mac_key, &ctext); | ||
ctext.extend_from_slice(&mac[..10]); | ||
Ok(ctext) | ||
} | ||
/// Encrypt plaintext `msg` with AES-256 and embed a computed HMAC into the returned bytes. | ||
/// | ||
/// *Implementation note: within the body of this method, only the first [`MAC_KEY_LENGTH`] | ||
/// bytes of the computed MAC are used.* | ||
pub fn aes_256_ctr_hmac_encrypt( | ||
msg: &[u8], | ||
cipher_key: &[u8; AES_256_KEY_SIZE], | ||
mac_key: &[u8; MAC_KEY_LENGTH], | ||
) -> Vec<u8> { | ||
let mut ctext = aes_256_ctr_encrypt(msg, cipher_key); | ||
let mac = hmac_sha256(mac_key, &ctext); | ||
ctext.extend_from_slice(&mac[..MAC_KEY_LENGTH]); | ||
ctext | ||
} | ||
|
||
pub(crate) fn aes256_ctr_hmacsha256_decrypt( | ||
ctext: &[u8], | ||
cipher_key: &[u8], | ||
mac_key: &[u8], | ||
) -> Result<Vec<u8>, DecryptionError> { | ||
if ctext.len() < 10 { | ||
return Err(DecryptionError::BadCiphertext("truncated ciphertext")); | ||
/// Validate the HMAC `mac_key` against the ciphertext `ctext`, then decrypt `ctext` using | ||
/// AES-256 with `cipher_key` and [`aes_256_ctr_decrypt`]. | ||
/// | ||
/// *Implementation note: the last [`MAC_KEY_LENGTH`] bytes of the `ctext` slice represent the | ||
/// truncated HMAC of the rest of the message, as generated by [`aes_256_ctr_hmac_encrypt`].* | ||
pub fn aes_256_ctr_hmac_decrypt( | ||
ctext: &[u8], | ||
cipher_key: &[u8; AES_256_KEY_SIZE], | ||
mac_key: &[u8; MAC_KEY_LENGTH], | ||
) -> Result<Vec<u8>, DecryptionError> { | ||
if ctext.len() < MAC_KEY_LENGTH { | ||
return Err(DecryptionError("truncated ciphertext")); | ||
} | ||
let (ctext, ctext_mac) = ctext.split_at(ctext.len() - MAC_KEY_LENGTH); | ||
let ctext_mac = array_ref![ctext_mac, 0, MAC_KEY_LENGTH]; | ||
let our_mac = hmac_sha256(mac_key, ctext); | ||
let our_mac = array_ref![&our_mac, 0, MAC_KEY_LENGTH]; | ||
let same: bool = our_mac.ct_eq(ctext_mac).into(); | ||
if !same { | ||
return Err(DecryptionError("MAC verification failed")); | ||
} | ||
Ok(aes_256_ctr_decrypt(ctext, cipher_key)) | ||
} | ||
let ptext_len = ctext.len() - 10; | ||
let our_mac = hmac_sha256(mac_key, &ctext[..ptext_len]); | ||
let same: bool = our_mac[..10].ct_eq(&ctext[ptext_len..]).into(); | ||
if !same { | ||
return Err(DecryptionError::BadCiphertext("MAC verification failed")); | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
||
use std::convert::TryInto; | ||
|
||
use rand::rngs::OsRng; | ||
use rand::Rng; | ||
|
||
#[test] | ||
fn static_test() { | ||
let key: [u8; AES_256_KEY_SIZE] = | ||
hex::decode("603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4") | ||
.expect("valid hex") | ||
.try_into() | ||
.expect("correct array size"); | ||
let ptext = [0u8; 35]; | ||
|
||
let ctext = aes_256_ctr_encrypt(&ptext, &key); | ||
assert_eq!( | ||
hex::encode(ctext), | ||
"e568f68194cf76d6174d4cc04310a85491151e5d0b7a1f1bc0d7acd0ae3e51e4170e23" | ||
); | ||
} | ||
|
||
#[test] | ||
fn no_hmac_test() { | ||
let key: [u8; AES_256_KEY_SIZE] = (&mut OsRng).gen(); | ||
let ptext: [u8; 30] = (&mut OsRng).gen(); | ||
|
||
let ctext = aes_256_ctr_encrypt(&ptext, &key); | ||
let decrypted = aes_256_ctr_decrypt(&ctext, &key); | ||
assert_eq!(ptext.as_ref(), &decrypted); | ||
} | ||
|
||
#[test] | ||
fn hmac_test() { | ||
let key: [u8; AES_256_KEY_SIZE] = (&mut OsRng).gen(); | ||
let mac_key: [u8; MAC_KEY_LENGTH] = (&mut OsRng).gen(); | ||
let ptext: [u8; 30] = (&mut OsRng).gen(); | ||
|
||
let ctext = aes_256_ctr_hmac_encrypt(&ptext, &key, &mac_key); | ||
let decrypted = | ||
aes_256_ctr_hmac_decrypt(&ctext, &key, &mac_key).expect("valid decryption"); | ||
assert_eq!(ptext.as_ref(), &decrypted); | ||
} | ||
} | ||
aes_256_ctr_decrypt(&ctext[..ptext_len], cipher_key) | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
/// Use AES-256 in CBC mode. | ||
pub mod cbc { | ||
use super::*; | ||
|
||
#[test] | ||
fn aes_cbc_test() { | ||
let key = hex::decode("4e22eb16d964779994222e82192ce9f747da72dc4abe49dfdeeb71d0ffe3796e") | ||
.expect("valid hex"); | ||
let iv = hex::decode("6f8a557ddc0a140c878063a6d5f31d3d").expect("valid hex"); | ||
|
||
let ptext = hex::decode("30736294a124482a4159").expect("valid hex"); | ||
|
||
let ctext = aes_256_cbc_encrypt(&ptext, &key, &iv).expect("valid key and IV"); | ||
assert_eq!( | ||
hex::encode(ctext.clone()), | ||
"dd3f573ab4508b9ed0e45e0baf5608f3" | ||
); | ||
|
||
let recovered = aes_256_cbc_decrypt(&ctext, &key, &iv).expect("valid"); | ||
assert_eq!(hex::encode(ptext), hex::encode(recovered.clone())); | ||
|
||
// padding is invalid: | ||
assert!(aes_256_cbc_decrypt(&recovered, &key, &iv).is_err()); | ||
assert!(aes_256_cbc_decrypt(&ctext, &key, &ctext).is_err()); | ||
pub fn aes_256_cbc_encrypt( | ||
ptext: &[u8], | ||
key: &[u8; AES_256_KEY_SIZE], | ||
iv: &[u8; AES_NONCE_SIZE], | ||
) -> Vec<u8> { | ||
let mode = | ||
Cbc::<Aes256, Pkcs7>::new_from_slices(key, iv).expect("key and iv were fixed length"); | ||
mode.encrypt_vec(ptext) | ||
} | ||
|
||
// bitflip the IV to cause a change in the recovered text | ||
let bad_iv = hex::decode("ef8a557ddc0a140c878063a6d5f31d3d").expect("valid hex"); | ||
let recovered = aes_256_cbc_decrypt(&ctext, &key, &bad_iv).expect("still valid"); | ||
assert_eq!(hex::encode(recovered), "b0736294a124482a4159"); | ||
pub fn aes_256_cbc_decrypt( | ||
ctext: &[u8], | ||
key: &[u8; AES_256_KEY_SIZE], | ||
iv: &[u8; AES_NONCE_SIZE], | ||
) -> Result<Vec<u8>, DecryptionError> { | ||
if ctext.is_empty() || ctext.len() % 16 != 0 { | ||
return Err(DecryptionError( | ||
"ciphertext length must be a non-zero multiple of 16", | ||
)); | ||
} | ||
|
||
let mode = Cbc::<Aes256, Pkcs7>::new_from_slices(key.as_ref(), iv.as_ref()) | ||
.expect("key and iv were fixed length"); | ||
mode.decrypt_vec(ctext) | ||
.map_err(|_| DecryptionError("failed to decrypt")) | ||
} | ||
|
||
#[test] | ||
fn aes_ctr_test() { | ||
let key = hex::decode("603DEB1015CA71BE2B73AEF0857D77811F352C073B6108D72D9810A30914DFF4") | ||
.expect("valid hex"); | ||
let ptext = [0u8; 35]; | ||
|
||
let ctext = aes_256_ctr_encrypt(&ptext, &key).expect("valid key"); | ||
assert_eq!( | ||
hex::encode(ctext), | ||
"e568f68194cf76d6174d4cc04310a85491151e5d0b7a1f1bc0d7acd0ae3e51e4170e23" | ||
); | ||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
||
use std::convert::TryInto; | ||
|
||
use rand::rngs::OsRng; | ||
use rand::Rng; | ||
|
||
#[test] | ||
fn static_test() { | ||
let key: [u8; AES_256_KEY_SIZE] = | ||
hex::decode("4e22eb16d964779994222e82192ce9f747da72dc4abe49dfdeeb71d0ffe3796e") | ||
.expect("valid hex") | ||
.try_into() | ||
.expect("correct array size"); | ||
let iv: [u8; AES_NONCE_SIZE] = hex::decode("6f8a557ddc0a140c878063a6d5f31d3d") | ||
.expect("valid hex") | ||
.try_into() | ||
.expect("correct array size"); | ||
|
||
let ptext = hex::decode("30736294a124482a4159").expect("valid hex"); | ||
|
||
let ctext = cbc::aes_256_cbc_encrypt(&ptext, &key, &iv); | ||
assert_eq!( | ||
hex::encode(ctext.clone()), | ||
"dd3f573ab4508b9ed0e45e0baf5608f3" | ||
); | ||
|
||
let recovered = cbc::aes_256_cbc_decrypt(&ctext, &key, &iv).expect("valid"); | ||
assert_eq!(hex::encode(ptext), hex::encode(recovered.clone())); | ||
|
||
// padding is invalid: | ||
assert!(cbc::aes_256_cbc_decrypt(&recovered, &key, &iv).is_err()); | ||
assert!( | ||
cbc::aes_256_cbc_decrypt(&ctext, &key, array_ref![&ctext, 0, AES_NONCE_SIZE]) | ||
.is_err() | ||
); | ||
|
||
// bitflip the IV to cause a change in the recovered text | ||
let bad_iv: [u8; AES_NONCE_SIZE] = hex::decode("ef8a557ddc0a140c878063a6d5f31d3d") | ||
.expect("valid hex") | ||
.try_into() | ||
.expect("correct array size"); | ||
let recovered = cbc::aes_256_cbc_decrypt(&ctext, &key, &bad_iv).expect("still valid"); | ||
assert_eq!(hex::encode(recovered), "b0736294a124482a4159"); | ||
} | ||
|
||
#[test] | ||
fn random_test() { | ||
let key: [u8; AES_256_KEY_SIZE] = (&mut OsRng).gen(); | ||
let iv: [u8; AES_NONCE_SIZE] = (&mut OsRng).gen(); | ||
let ptext: [u8; 30] = (&mut OsRng).gen(); | ||
|
||
let ctext = aes_256_cbc_encrypt(&ptext, &key, &iv); | ||
let decrypted = aes_256_cbc_decrypt(&ctext, &key, &iv).expect("valid decryption"); | ||
assert_eq!(ptext.as_ref(), &decrypted); | ||
} | ||
} | ||
} | ||
|
||
/// Calculate the [`Hmac`]-[`Sha256`] code over `input` using `key`. | ||
pub fn hmac_sha256(key: &[u8], input: &[u8]) -> [u8; 256 / BITS_PER_BYTE] { | ||
let mut hmac = | ||
Hmac::<Sha256>::new_from_slice(key).expect("HMAC-SHA256 should accept any size key"); | ||
hmac.update(input); | ||
hmac.finalize().into_bytes().into() | ||
} |
Oops, something went wrong.