Skip to content
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

Merged
merged 2 commits into from
Aug 21, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# RustCrypto: stream ciphers
# RustCrypto: Authenticated Encryption with Associated Data
[![Build Status](https://travis-ci.org/RustCrypto/AEADs.svg?branch=master)](https://travis-ci.org/RustCrypto/AEADs) [![dependency status](https://deps.rs/repo/github/RustCrypto/AEADs/status.svg)](https://deps.rs/repo/github/AEADs/stream-ciphers)

Collection of [Authenticated Encryption with Associated Data (AEAD)][1]
Expand Down
8 changes: 7 additions & 1 deletion chacha20poly1305/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,16 @@ version = "0.0.0"
authors = ["RustCrypto Developers"]
edition = "2018"
license = "MIT OR Apache-2.0"
description = "CTR block mode of operation"
description = """
ChaCha20Poly1305 Authenticated Encryption with Additional Data Algorithm (RFC 8439)
"""
documentation = "chacha20poly1305"
repository = "https://github.com/RustCrypto/AEADs"
keywords = ["crypto", "cipher", "aead"]
categories = ["cryptography", "no-std"]

[dependencies]
aead = { version = "0.1", git = "https://github.com/RustCrypto/traits" }
chacha20 = { version = "0.2.1", features = ["zeroize"] }
poly1305 = "0.2"
zeroize = { version = "0.9", default-features = false }
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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:

struct Foo { .. }

struct Bar { foo: Foo, baz: Baz }

if both Foo and Bar implement Zeroize. Will it zeroize Foo space twice?

Copy link
Member Author

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 two Drop handlers like in your example, they will zero foo twice. I'd suggest only Foo impl Drop in the example above.

157 changes: 157 additions & 0 deletions chacha20poly1305/src/lib.rs
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,
}
Copy link
Member

@newpavlov newpavlov Aug 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it can be made generic over the StreamCipher trait? I wonder if it should not go to the poly1305 crate... Or do you plan to make it even more generic?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 poly1305 crate might make sense as it's the eponymous algorithm's primary use case. I could also see this further justifying the existence of salsa20-core versus trying to abstract it away into a more generic ctr.


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(())
}
}
81 changes: 81 additions & 0 deletions chacha20poly1305/tests/lib.rs
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());
}