Skip to content

Commit 602b190

Browse files
committed
[WIP] ChaCha20Poly1305 AEAD
Implements the ChaCha20Poly1305 AEAD described in RFC 8439 using the `chacha20` and `poly1305` crates.
1 parent 13f4031 commit 602b190

File tree

5 files changed

+267
-2
lines changed

5 files changed

+267
-2
lines changed

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22
members = [
33
"chacha20poly1305",
44
]
5+
6+
[patch.crates-io]
7+
aead = { git = "https://github.com/mobilecoinofficial/traits" }

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# RustCrypto: stream ciphers
1+
# RustCrypto: Authenticated Encryption with Associated Data
22
[![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)
33

44
Collection of [Authenticated Encryption with Associated Data (AEAD)][1]

chacha20poly1305/Cargo.toml

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ version = "0.0.0"
44
authors = ["RustCrypto Developers"]
55
edition = "2018"
66
license = "MIT OR Apache-2.0"
7-
description = "CTR block mode of operation"
7+
description = """
8+
ChaCha20Poly1305 Authenticated Encryption with Additional Data Algorithm (RFC 8439)
9+
"""
810
documentation = "chacha20poly1305"
911
repository = "https://github.com/RustCrypto/AEADs"
1012
keywords = ["crypto", "cipher", "aead"]
1113
categories = ["cryptography", "no-std"]
1214

1315
[dependencies]
16+
aead = "0"
17+
chacha20 = { version = "0.2", features = ["zeroize"] }
18+
generic-array = { version = "0.12", default-features = false }
19+
poly1305 = "0.2"
20+
zeroize = { version = "0.9", default-features = false }

chacha20poly1305/src/lib.rs

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//! ChaCha20Poly1305 Authenticated Encryption with Additional Data Algorithm
2+
//! (RFC 8439)
3+
4+
#![no_std]
5+
6+
extern crate alloc;
7+
8+
pub use aead;
9+
// TODO(tarcieri): re-export this from the AEAD crate
10+
pub use generic_array;
11+
12+
use aead::{Aead, Error, NewAead};
13+
use alloc::vec::Vec;
14+
use chacha20::stream_cipher::{NewStreamCipher, SyncStreamCipher, SyncStreamCipherSeek};
15+
use chacha20::ChaCha20;
16+
use core::convert::TryInto;
17+
use generic_array::{
18+
typenum::{U0, U12, U16, U32},
19+
GenericArray,
20+
};
21+
use poly1305::{Poly1305, Tag};
22+
use zeroize::{Zeroize, Zeroizing};
23+
24+
/// Maximum number of blocks that can be encrypted with ChaCha20 before the
25+
/// counter overflows.
26+
// TODO(tarcieri): move this to the `chacha20` crate?
27+
const CHACHA20_MAX_BLOCKS: usize = core::u32::MAX as usize;
28+
29+
/// Size of a ChaCha20 block in bytes
30+
// TODO(tarcieri): move this to the `chacha20` crate?
31+
const CHACHA20_BLOCK_SIZE: usize = 64;
32+
33+
/// ChaCha20Poly1305 AEAD
34+
#[derive(Clone)]
35+
pub struct ChaCha20Poly1305 {
36+
/// Secret key
37+
key: GenericArray<u8, U32>,
38+
}
39+
40+
impl NewAead for ChaCha20Poly1305 {
41+
type KeySize = U32;
42+
43+
fn new(key: GenericArray<u8, U32>) -> Self {
44+
ChaCha20Poly1305 { key }
45+
}
46+
}
47+
48+
impl Aead for ChaCha20Poly1305 {
49+
type NonceSize = U12;
50+
type TagSize = U16;
51+
type CiphertextOverhead = U0;
52+
53+
fn encrypt(
54+
&mut self,
55+
associated_data: &[u8],
56+
nonce: &GenericArray<u8, Self::NonceSize>,
57+
plaintext: &[u8],
58+
) -> Result<Vec<u8>, Error> {
59+
CipherInstance::new(&self.key, nonce).encrypt(associated_data, plaintext)
60+
}
61+
62+
fn decrypt(
63+
&mut self,
64+
associated_data: &[u8],
65+
nonce: &GenericArray<u8, Self::NonceSize>,
66+
ciphertext: &[u8],
67+
) -> Result<Vec<u8>, Error> {
68+
CipherInstance::new(&self.key, nonce).decrypt(associated_data, ciphertext)
69+
}
70+
}
71+
72+
impl Drop for ChaCha20Poly1305 {
73+
fn drop(&mut self) {
74+
self.key.as_mut_slice().zeroize();
75+
}
76+
}
77+
78+
/// ChaCha20Poly1305 instantiated with a particular nonce
79+
struct CipherInstance {
80+
chacha20: ChaCha20,
81+
poly1305: Poly1305,
82+
}
83+
84+
impl CipherInstance {
85+
/// Instantiate the underlying cipher with a particular nonce
86+
fn new(key: &GenericArray<u8, U32>, nonce: &GenericArray<u8, U12>) -> Self {
87+
let mut chacha20 = ChaCha20::new(key, nonce);
88+
89+
// Derive Poly1305 key from the first 32-bytes of the ChaCha20 keystream
90+
let mut auth_key = Zeroizing::new([0u8; poly1305::KEY_SIZE]);
91+
chacha20.apply_keystream(&mut *auth_key);
92+
93+
// Set ChaCha20 counter to 1
94+
chacha20.seek(CHACHA20_BLOCK_SIZE as u64);
95+
96+
let poly1305 = Poly1305::new(&auth_key);
97+
Self { chacha20, poly1305 }
98+
}
99+
100+
/// Encrypt the given message, allocating a vector for the resulting ciphertext
101+
fn encrypt(self, associated_data: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, Error> {
102+
let mut buffer = Vec::with_capacity(plaintext.len() + poly1305::BLOCK_SIZE);
103+
buffer.extend_from_slice(plaintext);
104+
105+
let tag = self.encrypt_in_place(associated_data, &mut buffer)?;
106+
buffer.extend_from_slice(tag.code().as_slice());
107+
Ok(buffer)
108+
}
109+
110+
/// Encrypt the given message in-place, returning the authentication tag
111+
fn encrypt_in_place(
112+
mut self,
113+
associated_data: &[u8],
114+
buffer: &mut [u8],
115+
) -> Result<Tag, Error> {
116+
if buffer.len() / CHACHA20_BLOCK_SIZE >= CHACHA20_MAX_BLOCKS {
117+
return Err(Error);
118+
}
119+
120+
self.poly1305.input_padded(associated_data);
121+
self.chacha20.apply_keystream(buffer);
122+
self.poly1305.input_padded(buffer);
123+
self.authenticate_lengths(associated_data, buffer)?;
124+
Ok(self.poly1305.result())
125+
}
126+
127+
/// Decrypt the given message, allocating a vector for the resulting plaintext
128+
fn decrypt(self, associated_data: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, Error> {
129+
if ciphertext.len() < poly1305::BLOCK_SIZE {
130+
return Err(Error);
131+
}
132+
133+
let tag_start = ciphertext.len() - poly1305::BLOCK_SIZE;
134+
let mut buffer = Vec::from(&ciphertext[..tag_start]);
135+
let tag: [u8; poly1305::BLOCK_SIZE] = ciphertext[tag_start..].try_into().unwrap();
136+
137+
self.decrypt_in_place(associated_data, &mut buffer, &tag)?;
138+
Ok(buffer)
139+
}
140+
141+
/// Decrypt the given message, first authenticating ciphertext integrity
142+
/// and returning an error if it's been tampered with.
143+
fn decrypt_in_place(
144+
mut self,
145+
associated_data: &[u8],
146+
buffer: &mut [u8],
147+
tag: &[u8; poly1305::BLOCK_SIZE],
148+
) -> Result<(), Error> {
149+
if buffer.len() / CHACHA20_BLOCK_SIZE >= CHACHA20_MAX_BLOCKS {
150+
return Err(Error);
151+
}
152+
153+
self.poly1305.input_padded(associated_data);
154+
self.poly1305.input_padded(buffer);
155+
self.authenticate_lengths(associated_data, buffer)?;
156+
157+
if self.poly1305.result() == Tag::new(*GenericArray::from_slice(tag)) {
158+
self.chacha20.apply_keystream(buffer);
159+
Ok(())
160+
} else {
161+
Err(Error)
162+
}
163+
}
164+
165+
/// Authenticate the lengths of the associated data and message
166+
fn authenticate_lengths(&mut self, associated_data: &[u8], buffer: &[u8]) -> Result<(), Error> {
167+
let associated_data_len: u64 = associated_data.len().try_into().map_err(|_| Error)?;
168+
let buffer_len: u64 = buffer.len().try_into().map_err(|_| Error)?;
169+
170+
self.poly1305.input(&associated_data_len.to_le_bytes());
171+
self.poly1305.input(&buffer_len.to_le_bytes());
172+
Ok(())
173+
}
174+
}

chacha20poly1305/tests/lib.rs

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//! ChaCha20Poly1305 test vectors.
2+
//!
3+
//! From RFC 8439 Section 2.8.2:
4+
//! <https://tools.ietf.org/html/rfc8439#section-2.8.2>
5+
6+
use chacha20poly1305::aead::{Aead, NewAead};
7+
use chacha20poly1305::generic_array::GenericArray;
8+
use chacha20poly1305::ChaCha20Poly1305;
9+
10+
const KEY: &[u8; 32] = &[
11+
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
12+
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
13+
];
14+
15+
const NONCE: &[u8; 12] = &[
16+
0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
17+
];
18+
19+
const AAD: &[u8; 12] = &[
20+
0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
21+
];
22+
23+
const PLAINTEXT: &[u8] = b"Ladies and Gentlemen of the class of '99: \
24+
If I could offer you only one tip for the future, sunscreen would be it.";
25+
26+
const CIPHERTEXT: &[u8] = &[
27+
0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86, 0xaf, 0xbc, 0x53, 0xef, 0x7e, 0xc2,
28+
0xa4, 0xad, 0xed, 0x51, 0x29, 0x6e, 0x08, 0xfe, 0xa9, 0xe2, 0xb5, 0xa7, 0x36, 0xee, 0x62, 0xd6,
29+
0x3d, 0xbe, 0xa4, 0x5e, 0x8c, 0xa9, 0x67, 0x12, 0x82, 0xfa, 0xfb, 0x69, 0xda, 0x92, 0x72, 0x8b,
30+
0x1a, 0x71, 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, 0x05, 0xd6, 0xa5, 0xb6, 0x7e, 0xcd, 0x3b, 0x36,
31+
0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77, 0x8b, 0x8c, 0x98, 0x03, 0xae, 0xe3, 0x28, 0x09, 0x1b, 0x58,
32+
0xfa, 0xb3, 0x24, 0xe4, 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85, 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc,
33+
0x3f, 0xf4, 0xde, 0xf0, 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, 0x76, 0xd2, 0x65, 0x86, 0xce, 0xc6, 0x4b,
34+
0x61, 0x16,
35+
];
36+
37+
const TAG: &[u8] = &[
38+
0x1a, 0xe1, 0x0b, 0x59, 0x4f, 0x09, 0xe2, 0x6a, 0x7e, 0x90, 0x2e, 0xcb, 0xd0, 0x60, 0x06, 0x91,
39+
];
40+
41+
#[test]
42+
fn encrypt() {
43+
let key = GenericArray::from_slice(KEY);
44+
let nonce = GenericArray::from_slice(NONCE);
45+
46+
let mut cipher = ChaCha20Poly1305::new(*key);
47+
let ciphertext = cipher.encrypt(AAD, nonce, PLAINTEXT).unwrap();
48+
49+
let tag_begins = ciphertext.len() - 16;
50+
assert_eq!(CIPHERTEXT, &ciphertext[..tag_begins]);
51+
assert_eq!(TAG, &ciphertext[tag_begins..]);
52+
}
53+
54+
#[test]
55+
fn decrypt() {
56+
let key = GenericArray::from_slice(KEY);
57+
let nonce = GenericArray::from_slice(NONCE);
58+
59+
let mut ciphertext = Vec::from(CIPHERTEXT);
60+
ciphertext.extend_from_slice(TAG);
61+
62+
let mut cipher = ChaCha20Poly1305::new(*key);
63+
let plaintext = cipher.decrypt(AAD, nonce, &ciphertext).unwrap();
64+
65+
assert_eq!(PLAINTEXT, plaintext.as_slice());
66+
}
67+
68+
#[test]
69+
fn decrypt_modified() {
70+
let key = GenericArray::from_slice(KEY);
71+
let nonce = GenericArray::from_slice(NONCE);
72+
73+
let mut ciphertext = Vec::from(CIPHERTEXT);
74+
ciphertext.extend_from_slice(TAG);
75+
76+
// Tweak the first byte
77+
ciphertext[0] ^= 0xaa;
78+
79+
let mut cipher = ChaCha20Poly1305::new(*key);
80+
assert!(cipher.decrypt(AAD, nonce, &ciphertext).is_err());
81+
}

0 commit comments

Comments
 (0)