Skip to content

Commit 032ec19

Browse files
authored
ssh-cipher: impl AeadInOut for ChaCha20Poly1305 (#370)
Replaces the inherent method-based API with an `AeadInOut` trait impl. AAD is supported up to lengths of 16 (i.e. the Poly1305 block size). This is sufficient to cover the relevant use cases for OpenSSH, which are empty AAD or AAD with a length of 4 (i.e. encrypted `uint32` length).
1 parent 441343c commit 032ec19

2 files changed

Lines changed: 68 additions & 73 deletions

File tree

ssh-cipher/src/chacha20poly1305.rs

Lines changed: 64 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ pub use chacha20::ChaCha20Legacy as ChaCha20;
44

55
use crate::Tag;
66
use aead::{
7-
AeadCore, Error, KeyInit, KeySizeUser, Result, TagPosition,
7+
AeadCore, AeadInOut, Error, KeyInit, KeySizeUser, Result, TagPosition,
88
array::typenum::{U8, U16, U32},
9+
inout::InOutBuf,
910
};
1011
use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
11-
use poly1305::Poly1305;
12+
use poly1305::{Poly1305, universal_hash::UniversalHash};
1213
use subtle::ConstantTimeEq;
1314

1415
#[cfg(feature = "zeroize")]
@@ -27,6 +28,11 @@ pub type ChaChaNonce = chacha20::LegacyNonce;
2728
/// - Nonce is 64-bit instead of 96-bit (i.e. uses legacy "djb" ChaCha20 variant).
2829
/// - The AAD and ciphertext inputs of Poly1305 are not padded.
2930
/// - The lengths of ciphertext and AAD are not authenticated using Poly1305.
31+
/// - Maximum supported AAD size is 16.
32+
///
33+
/// ## Usage notes
34+
/// - In the context of SSH packet encryption, AAD will be 4 bytes and contain the encrypted length.
35+
/// - In the context of SSH key encryption, AAD will be empty.
3036
///
3137
/// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
3238
/// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439
@@ -52,46 +58,25 @@ impl AeadCore for ChaCha20Poly1305 {
5258
const TAG_POSITION: TagPosition = TagPosition::Postfix;
5359
}
5460

55-
impl ChaCha20Poly1305 {
56-
/// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
57-
///
58-
/// The input `buffer` should contain the concatenation of any additional associated data (AAD)
59-
/// and the plaintext to be encrypted, where in the context of the SSH packet encryption
60-
/// protocol the AAD represents an encrypted packet length, which is itself 4-bytes / 64-bits.
61-
///
62-
/// `aad_len` is the length of the AAD in bytes:
63-
/// - In the context of SSH packet encryption, this should be `4`.
64-
/// - In the context of SSH key encryption, `aad_len` should be `0`.
65-
///
66-
/// The first `aad_len` bytes of `buffer` will be unmodified after encryption is completed.
67-
/// Only the data after `aad_len` will be encrypted.
68-
///
69-
/// The resulting `Tag` authenticates both the AAD and the ciphertext in the buffer.
70-
pub fn encrypt(&self, nonce: &ChaChaNonce, buffer: &mut [u8], aad_len: usize) -> Result<Tag> {
71-
Cipher::new(&self.key, nonce).encrypt(buffer, aad_len)
61+
impl AeadInOut for ChaCha20Poly1305 {
62+
// Required methods
63+
fn encrypt_inout_detached(
64+
&self,
65+
nonce: &ChaChaNonce,
66+
associated_data: &[u8],
67+
buffer: InOutBuf<'_, '_, u8>,
68+
) -> Result<Tag> {
69+
Cipher::new(&self.key, nonce).encrypt(associated_data, buffer)
7270
}
7371

74-
/// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
75-
/// authentication `tag`.
76-
///
77-
/// The input `buffer` should contain the concatenation of any additional associated data (AAD)
78-
/// and the ciphertext to be authenticated, where in the context of the SSH packet encryption
79-
/// protocol the AAD represents an encrypted packet length, which is itself 4-bytes / 64-bits.
80-
///
81-
/// `aad_len` is the length of the AAD in bytes:
82-
/// - In the context of SSH packet encryption, this should be `4`.
83-
/// - In the context of SSH key encryption, `aad_len` should be `0`.
84-
///
85-
/// The first `aad_len` bytes of `buffer` will be unmodified after decryption completes
86-
/// successfully. Only data after `aad_len` will be decrypted.
87-
pub fn decrypt(
72+
fn decrypt_inout_detached(
8873
&self,
8974
nonce: &ChaChaNonce,
90-
buffer: &mut [u8],
91-
tag: Tag,
92-
aad_len: usize,
75+
associated_data: &[u8],
76+
buffer: InOutBuf<'_, '_, u8>,
77+
tag: &Tag,
9378
) -> Result<()> {
94-
Cipher::new(&self.key, nonce).decrypt(buffer, tag, aad_len)
79+
Cipher::new(&self.key, nonce).decrypt(associated_data, buffer, tag)
9580
}
9681
}
9782

@@ -128,37 +113,53 @@ impl Cipher {
128113

129114
/// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
130115
#[inline]
131-
pub fn encrypt(mut self, buffer: &mut [u8], aad_len: usize) -> Result<Tag> {
132-
if buffer.len() < aad_len {
133-
return Err(Error);
134-
}
135-
136-
self.cipher.apply_keystream(&mut buffer[aad_len..]);
137-
Ok(self.mac.compute_unpadded(buffer))
116+
pub fn encrypt(mut self, aad: &[u8], mut buffer: InOutBuf<'_, '_, u8>) -> Result<Tag> {
117+
self.cipher.apply_keystream_inout(buffer.reborrow());
118+
compute_mac(self.mac, aad, buffer.get_out())
138119
}
139120

140121
/// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
141122
/// authentication `tag`.
142123
#[inline]
143-
pub fn decrypt(mut self, buffer: &mut [u8], tag: Tag, aad_len: usize) -> Result<()> {
144-
if buffer.len() < aad_len {
145-
return Err(Error);
146-
}
147-
148-
let expected_tag = self.mac.compute_unpadded(buffer);
124+
pub fn decrypt(mut self, aad: &[u8], buffer: InOutBuf<'_, '_, u8>, tag: &Tag) -> Result<()> {
125+
let expected_tag = compute_mac(self.mac, aad, buffer.get_in())?;
149126

150-
if expected_tag.ct_eq(&tag).into() {
151-
self.cipher.apply_keystream(&mut buffer[aad_len..]);
127+
if expected_tag.ct_eq(tag).into() {
128+
self.cipher.apply_keystream_inout(buffer);
152129
Ok(())
153130
} else {
154131
Err(Error)
155132
}
156133
}
157134
}
158135

136+
/// Compute the MAC for a given input buffer (containing ciphertext).
137+
fn compute_mac(mut mac: Poly1305, aad: &[u8], buffer: &[u8]) -> Result<Tag> {
138+
match aad.len() {
139+
0 => Ok(mac.compute_unpadded(buffer)),
140+
1..poly1305::BLOCK_SIZE => {
141+
let mut block = poly1305::Block::default();
142+
block[..aad.len()].copy_from_slice(aad);
143+
144+
let block_remaining = poly1305::BLOCK_SIZE.checked_sub(aad.len()).ok_or(Error)?;
145+
if buffer.len() > block_remaining {
146+
let (head, tail) = buffer.split_at(block_remaining);
147+
block[aad.len()..].copy_from_slice(head);
148+
mac.update(&[block]);
149+
Ok(mac.compute_unpadded(tail))
150+
} else {
151+
let msg_len = aad.len().checked_add(buffer.len()).ok_or(Error)?;
152+
block[aad.len()..msg_len].copy_from_slice(buffer);
153+
Ok(mac.compute_unpadded(&block[..msg_len]))
154+
}
155+
}
156+
_ => Err(Error),
157+
}
158+
}
159+
159160
#[cfg(test)]
160161
mod tests {
161-
use super::{ChaCha20Poly1305, KeyInit};
162+
use super::{AeadInOut, ChaCha20Poly1305, KeyInit};
162163
use hex_literal::hex;
163164

164165
#[test]
@@ -170,30 +171,24 @@ mod tests {
170171
let ciphertext = hex!("6dcfb03be8a55e7f0220465672edd921489ea0171198e8a7");
171172
let tag = hex!("3e82fe0a2db7128d58ef8d9047963ca3");
172173

173-
const AAD_LEN: usize = 4;
174-
const PT_LEN: usize = 24;
175-
assert_eq!(aad.len(), AAD_LEN);
176-
assert_eq!(plaintext.len(), PT_LEN);
177-
178174
let cipher = ChaCha20Poly1305::new(key.as_ref());
179-
let mut buffer = [0u8; AAD_LEN + PT_LEN];
180-
let (a, p) = buffer.split_at_mut(AAD_LEN);
181-
a.copy_from_slice(&aad);
182-
p.copy_from_slice(&plaintext);
183-
175+
let mut buffer = plaintext.clone();
184176
let actual_tag = cipher
185-
.encrypt(nonce.as_ref(), &mut buffer, AAD_LEN)
177+
.encrypt_inout_detached(nonce.as_ref(), &aad, buffer.as_mut_slice().into())
186178
.unwrap();
187179

188-
assert_eq!(&buffer[..AAD_LEN], aad);
189-
assert_eq!(&buffer[AAD_LEN..], ciphertext);
180+
assert_eq!(buffer, ciphertext);
190181
assert_eq!(actual_tag, tag);
191182

192183
cipher
193-
.decrypt(nonce.as_ref(), &mut buffer, actual_tag, AAD_LEN)
184+
.decrypt_inout_detached(
185+
nonce.as_ref(),
186+
&aad,
187+
buffer.as_mut_slice().into(),
188+
&actual_tag,
189+
)
194190
.unwrap();
195191

196-
assert_eq!(&buffer[..AAD_LEN], aad);
197-
assert_eq!(&buffer[AAD_LEN..], plaintext);
192+
assert_eq!(buffer, plaintext);
198193
}
199194
}

ssh-cipher/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,12 @@ use encoding::{Label, LabelError};
4545

4646
#[cfg(feature = "aes-gcm")]
4747
use {
48-
aead::{AeadInOut, array::typenum::U12},
48+
aead::array::typenum::U12,
4949
aes_gcm::{Aes128Gcm, Aes256Gcm},
5050
};
5151

5252
#[cfg(any(feature = "aes-gcm", feature = "chacha20poly1305"))]
53-
use aead::KeyInit;
53+
use aead::{AeadInOut, KeyInit};
5454

5555
/// AES-128 in block chaining (CBC) mode
5656
const AES128_CBC: &str = "aes128-cbc";
@@ -258,7 +258,7 @@ impl Cipher {
258258
let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
259259
let tag = tag.ok_or(Error::TagSize)?;
260260
ChaCha20Poly1305::new(key)
261-
.decrypt(nonce, buffer, tag, 0)
261+
.decrypt_inout_detached(nonce, &[], buffer.into(), &tag)
262262
.map_err(|_| Error::Crypto)
263263
}
264264
// Use `Decryptor` for non-AEAD modes
@@ -320,7 +320,7 @@ impl Cipher {
320320
let key = key.try_into().map_err(|_| Error::KeySize)?;
321321
let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
322322
let tag = ChaCha20Poly1305::new(key)
323-
.encrypt(nonce, buffer, 0)
323+
.encrypt_inout_detached(nonce, &[], buffer.into())
324324
.map_err(|_| Error::Crypto)?;
325325
Ok(Some(tag))
326326
}

0 commit comments

Comments
 (0)