-
Notifications
You must be signed in to change notification settings - Fork 157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ChaCha20Poly1305 AEAD #3
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
//! ChaCha20Poly1305 Authenticated Encryption with Additional Data Algorithm | ||
//! (RFC 8439) | ||
|
||
#![no_std] | ||
|
||
extern crate alloc; | ||
|
||
pub use aead; | ||
|
||
use aead::generic_array::typenum::{U0, U12, U16, U32}; | ||
use aead::{generic_array::GenericArray, StatelessAead, Error, NewAead}; | ||
use alloc::vec::Vec; | ||
use chacha20::stream_cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek}; | ||
use chacha20::ChaCha20; | ||
use core::convert::TryInto; | ||
use poly1305::{Poly1305, Tag}; | ||
use zeroize::{Zeroize, Zeroizing}; | ||
|
||
/// ChaCha20Poly1305 AEAD | ||
#[derive(Clone)] | ||
pub struct ChaCha20Poly1305 { | ||
/// Secret key | ||
key: GenericArray<u8, U32>, | ||
} | ||
|
||
impl NewAead for ChaCha20Poly1305 { | ||
type KeySize = U32; | ||
|
||
fn new(key: GenericArray<u8, U32>) -> Self { | ||
ChaCha20Poly1305 { key } | ||
} | ||
} | ||
|
||
impl StatelessAead for ChaCha20Poly1305 { | ||
type NonceSize = U12; | ||
type TagSize = U16; | ||
type CiphertextOverhead = U0; | ||
|
||
fn encrypt( | ||
&self, | ||
associated_data: &[u8], | ||
nonce: &GenericArray<u8, Self::NonceSize>, | ||
plaintext: &[u8], | ||
) -> Result<Vec<u8>, Error> { | ||
CipherInstance::new(&self.key, nonce).encrypt(associated_data, plaintext) | ||
} | ||
|
||
fn decrypt( | ||
&self, | ||
associated_data: &[u8], | ||
nonce: &GenericArray<u8, Self::NonceSize>, | ||
ciphertext: &[u8], | ||
) -> Result<Vec<u8>, Error> { | ||
CipherInstance::new(&self.key, nonce).decrypt(associated_data, ciphertext) | ||
} | ||
} | ||
|
||
impl Drop for ChaCha20Poly1305 { | ||
fn drop(&mut self) { | ||
self.key.as_mut_slice().zeroize(); | ||
} | ||
} | ||
|
||
/// ChaCha20Poly1305 instantiated with a particular nonce | ||
struct CipherInstance { | ||
chacha20: ChaCha20, | ||
poly1305: Poly1305, | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like it can be made generic over the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a first cut 😉But yeah, good point. It would definitely be nice to reuse the same generic "Poly1305 plus a Salsa20 family stream cipher" core to implement Salsa20Poly1305, ChaCha20Poly1305, and XChaCha20Poly1305. The |
||
|
||
impl CipherInstance { | ||
/// Instantiate the underlying cipher with a particular nonce | ||
fn new(key: &GenericArray<u8, U32>, nonce: &GenericArray<u8, U12>) -> Self { | ||
let mut chacha20 = ChaCha20::new(key, nonce); | ||
|
||
// Derive Poly1305 key from the first 32-bytes of the ChaCha20 keystream | ||
let mut auth_key = Zeroizing::new([0u8; poly1305::KEY_SIZE]); | ||
chacha20.apply_keystream(&mut *auth_key); | ||
|
||
// Set ChaCha20 counter to 1 | ||
chacha20.seek(chacha20::BLOCK_SIZE as u64); | ||
|
||
let poly1305 = Poly1305::new(&auth_key); | ||
Self { chacha20, poly1305 } | ||
} | ||
|
||
/// Encrypt the given message, allocating a vector for the resulting ciphertext | ||
fn encrypt(self, associated_data: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> { | ||
let mut buffer = Vec::with_capacity(plaintext.len() + poly1305::BLOCK_SIZE); | ||
buffer.extend_from_slice(plaintext); | ||
|
||
let tag = self.encrypt_in_place(associated_data, &mut buffer)?; | ||
buffer.extend_from_slice(tag.code().as_slice()); | ||
Ok(buffer) | ||
} | ||
|
||
/// Encrypt the given message in-place, returning the authentication tag | ||
fn encrypt_in_place(mut self, associated_data: &[u8], buffer: &mut [u8]) -> Result<Tag, Error> { | ||
if buffer.len() / chacha20::BLOCK_SIZE >= chacha20::MAX_BLOCKS { | ||
return Err(Error); | ||
} | ||
|
||
self.poly1305.input_padded(associated_data); | ||
self.chacha20.apply_keystream(buffer); | ||
self.poly1305.input_padded(buffer); | ||
self.authenticate_lengths(associated_data, buffer)?; | ||
Ok(self.poly1305.result()) | ||
} | ||
|
||
/// Decrypt the given message, allocating a vector for the resulting plaintext | ||
fn decrypt(self, associated_data: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> { | ||
if ciphertext.len() < poly1305::BLOCK_SIZE { | ||
return Err(Error); | ||
} | ||
|
||
let tag_start = ciphertext.len() - poly1305::BLOCK_SIZE; | ||
let mut buffer = Vec::from(&ciphertext[..tag_start]); | ||
let tag: [u8; poly1305::BLOCK_SIZE] = ciphertext[tag_start..].try_into().unwrap(); | ||
self.decrypt_in_place(associated_data, &mut buffer, &tag)?; | ||
|
||
Ok(buffer) | ||
} | ||
|
||
/// Decrypt the given message, first authenticating ciphertext integrity | ||
/// and returning an error if it's been tampered with. | ||
fn decrypt_in_place( | ||
mut self, | ||
associated_data: &[u8], | ||
buffer: &mut [u8], | ||
tag: &[u8; poly1305::BLOCK_SIZE], | ||
) -> Result<(), Error> { | ||
if buffer.len() / chacha20::BLOCK_SIZE >= chacha20::MAX_BLOCKS { | ||
return Err(Error); | ||
} | ||
|
||
self.poly1305.input_padded(associated_data); | ||
self.poly1305.input_padded(buffer); | ||
self.authenticate_lengths(associated_data, buffer)?; | ||
|
||
// This performs a constant-time comparison using the `subtle` crate | ||
if self.poly1305.result() == Tag::new(*GenericArray::from_slice(tag)) { | ||
self.chacha20.apply_keystream(buffer); | ||
Ok(()) | ||
} else { | ||
Err(Error) | ||
} | ||
} | ||
|
||
/// Authenticate the lengths of the associated data and message | ||
fn authenticate_lengths(&mut self, associated_data: &[u8], buffer: &[u8]) -> Result<(), Error> { | ||
let associated_data_len: u64 = associated_data.len().try_into().map_err(|_| Error)?; | ||
let buffer_len: u64 = buffer.len().try_into().map_err(|_| Error)?; | ||
|
||
self.poly1305.input(&associated_data_len.to_le_bytes()); | ||
self.poly1305.input(&buffer_len.to_le_bytes()); | ||
Ok(()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
//! ChaCha20Poly1305 test vectors. | ||
//! | ||
//! From RFC 8439 Section 2.8.2: | ||
//! <https://tools.ietf.org/html/rfc8439#section-2.8.2> | ||
|
||
use chacha20poly1305::aead::{Aead, NewAead}; | ||
use chacha20poly1305::aead::generic_array::GenericArray; | ||
use chacha20poly1305::ChaCha20Poly1305; | ||
|
||
const KEY: &[u8; 32] = &[ | ||
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, | ||
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, | ||
]; | ||
|
||
const NONCE: &[u8; 12] = &[ | ||
0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, | ||
]; | ||
|
||
const AAD: &[u8; 12] = &[ | ||
0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, | ||
]; | ||
|
||
const PLAINTEXT: &[u8] = b"Ladies and Gentlemen of the class of '99: \ | ||
If I could offer you only one tip for the future, sunscreen would be it."; | ||
|
||
const CIPHERTEXT: &[u8] = &[ | ||
0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86, 0xaf, 0xbc, 0x53, 0xef, 0x7e, 0xc2, | ||
0xa4, 0xad, 0xed, 0x51, 0x29, 0x6e, 0x08, 0xfe, 0xa9, 0xe2, 0xb5, 0xa7, 0x36, 0xee, 0x62, 0xd6, | ||
0x3d, 0xbe, 0xa4, 0x5e, 0x8c, 0xa9, 0x67, 0x12, 0x82, 0xfa, 0xfb, 0x69, 0xda, 0x92, 0x72, 0x8b, | ||
0x1a, 0x71, 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, 0x05, 0xd6, 0xa5, 0xb6, 0x7e, 0xcd, 0x3b, 0x36, | ||
0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77, 0x8b, 0x8c, 0x98, 0x03, 0xae, 0xe3, 0x28, 0x09, 0x1b, 0x58, | ||
0xfa, 0xb3, 0x24, 0xe4, 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85, 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc, | ||
0x3f, 0xf4, 0xde, 0xf0, 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, 0x76, 0xd2, 0x65, 0x86, 0xce, 0xc6, 0x4b, | ||
0x61, 0x16, | ||
]; | ||
|
||
const TAG: &[u8] = &[ | ||
0x1a, 0xe1, 0x0b, 0x59, 0x4f, 0x09, 0xe2, 0x6a, 0x7e, 0x90, 0x2e, 0xcb, 0xd0, 0x60, 0x06, 0x91, | ||
]; | ||
|
||
#[test] | ||
fn encrypt() { | ||
let key = GenericArray::from_slice(KEY); | ||
let nonce = GenericArray::from_slice(NONCE); | ||
|
||
let mut cipher = ChaCha20Poly1305::new(*key); | ||
let ciphertext = cipher.encrypt(AAD, nonce, PLAINTEXT).unwrap(); | ||
|
||
let tag_begins = ciphertext.len() - 16; | ||
assert_eq!(CIPHERTEXT, &ciphertext[..tag_begins]); | ||
assert_eq!(TAG, &ciphertext[tag_begins..]); | ||
} | ||
|
||
#[test] | ||
fn decrypt() { | ||
let key = GenericArray::from_slice(KEY); | ||
let nonce = GenericArray::from_slice(NONCE); | ||
|
||
let mut ciphertext = Vec::from(CIPHERTEXT); | ||
ciphertext.extend_from_slice(TAG); | ||
|
||
let mut cipher = ChaCha20Poly1305::new(*key); | ||
let plaintext = cipher.decrypt(AAD, nonce, &ciphertext).unwrap(); | ||
|
||
assert_eq!(PLAINTEXT, plaintext.as_slice()); | ||
} | ||
|
||
#[test] | ||
fn decrypt_modified() { | ||
let key = GenericArray::from_slice(KEY); | ||
let nonce = GenericArray::from_slice(NONCE); | ||
|
||
let mut ciphertext = Vec::from(CIPHERTEXT); | ||
ciphertext.extend_from_slice(TAG); | ||
|
||
// Tweak the first byte | ||
ciphertext[0] ^= 0xaa; | ||
|
||
let mut cipher = ChaCha20Poly1305::new(*key); | ||
assert!(cipher.decrypt(AAD, nonce, &ciphertext).is_err()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought in other crates
zeroize
was optional?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy to keep it that way if you want. The other crates have a lower MSRV than
zeroize
, so it had to be optional there. Could always be optional but on-by-default as well.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I guess making it optional and enabling it by default will not make substantial difference compared to the non-optional approach, so we can leave it as-is.
BTW I wonder how zeroize will work in case like this:
if both
Foo
andBar
implementZeroize
. Will it zeroizeFoo
space twice?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@newpavlov I've been using explicit
Drop
handlers, and generally trying to push those down to the "leaf" data owner. If there are twoDrop
handlers like in your example, they will zerofoo
twice. I'd suggest onlyFoo
implDrop
in the example above.