Skip to content

Commit

Permalink
ssh-cipher: refactor ChaCha20Poly1305 to use aead API
Browse files Browse the repository at this point in the history
Changes the `ChaCha20Poly1305` type to impl the standard AEAD APIs
exported by the `aead` crate.
  • Loading branch information
tarcieri committed Aug 14, 2024
1 parent ac7f6b7 commit e3c97d5
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 39 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions ssh-cipher/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ cipher = "=0.5.0-pre.6"
encoding = { package = "ssh-encoding", version = "=0.3.0-pre.0", path = "../ssh-encoding" }

# optional dependencies
aead = { version = "0.6.0-rc.0", optional = true, default-features = false }
aes = { version = "=0.9.0-pre.1", optional = true, default-features = false }
aes-gcm = { version = "=0.11.0-pre.1", optional = true, default-features = false, features = ["aes"] }
cbc = { version = "=0.2.0-pre.1", optional = true }
ctr = { version = "=0.10.0-pre.1", optional = true, default-features = false }
chacha20 = { version = "=0.10.0-pre.1", optional = true, default-features = false, features = ["cipher"] }
chacha20 = { version = "=0.10.0-pre.1", optional = true, default-features = false, features = ["cipher", "legacy"] }
des = { version = "=0.9.0-pre.1", optional = true, default-features = false }
poly1305 = { version = "0.9.0-rc.0", optional = true, default-features = false }
subtle = { version = "2", optional = true, default-features = false }
Expand All @@ -37,8 +38,8 @@ std = []

aes-cbc = ["dep:aes", "dep:cbc"]
aes-ctr = ["dep:aes", "dep:ctr"]
aes-gcm = ["dep:aes", "dep:aes-gcm"]
chacha20poly1305 = ["dep:chacha20", "dep:poly1305", "dep:subtle"]
aes-gcm = ["dep:aead", "dep:aes", "dep:aes-gcm"]
chacha20poly1305 = ["dep:aead", "dep:chacha20", "dep:poly1305", "dep:subtle"]
tdes = ["dep:des", "dep:cbc"]

[package.metadata.docs.rs]
Expand Down
93 changes: 77 additions & 16 deletions ssh-cipher/src/chacha20poly1305.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
//! OpenSSH variant of ChaCha20Poly1305.

use crate::{Error, Nonce, Result, Tag};
use chacha20::{ChaCha20, Key};
use cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek};
use crate::Tag;
use aead::{
array::typenum::{U0, U16, U32, U8},
AeadCore, AeadInPlace, Error, KeyInit, KeySizeUser, Result,
};
use chacha20::ChaCha20Legacy as ChaCha20;
use cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
use poly1305::Poly1305;
use subtle::ConstantTimeEq;

/// Key for `[email protected]`.
pub type ChaChaKey = chacha20::Key;

/// Nonce for `[email protected]`.
pub type ChaChaNonce = chacha20::LegacyNonce;

/// OpenSSH variant of ChaCha20Poly1305: `[email protected]`
/// as described in [PROTOCOL.chacha20poly1305].
///
Expand All @@ -16,19 +26,60 @@ use subtle::ConstantTimeEq;
///
/// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
/// [RFC8439]: https://datatracker.ietf.org/doc/html/rfc8439
#[derive(Clone)]
pub struct ChaCha20Poly1305 {
// TODO(tarcieri): zeroize on drop
key: ChaChaKey,
}

impl KeySizeUser for ChaCha20Poly1305 {
type KeySize = U32;
}

impl KeyInit for ChaCha20Poly1305 {
#[inline]
fn new(key: &ChaChaKey) -> Self {
Self { key: *key }
}
}

impl AeadCore for ChaCha20Poly1305 {
type NonceSize = U8;
type TagSize = U16;
type CiphertextOverhead = U0;
}

impl AeadInPlace for ChaCha20Poly1305 {
fn encrypt_in_place_detached(
&self,
nonce: &ChaChaNonce,
associated_data: &[u8],
buffer: &mut [u8],
) -> Result<Tag> {
Cipher::new(&self.key, nonce).encrypt(associated_data, buffer)
}

fn decrypt_in_place_detached(
&self,
nonce: &ChaChaNonce,
associated_data: &[u8],
buffer: &mut [u8],
tag: &Tag,
) -> Result<()> {
Cipher::new(&self.key, nonce).decrypt(associated_data, buffer, *tag)
}
}

/// Internal type representing a cipher instance.
struct Cipher {
cipher: ChaCha20,
mac: Poly1305,
}

impl ChaCha20Poly1305 {
/// Create a new [`ChaCha20Poly1305`] instance with a 32-byte key.
///
/// [PROTOCOL.chacha20poly1305]: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.chacha20poly1305?annotate=HEAD
pub fn new(key: &[u8], nonce: &[u8]) -> Result<Self> {
let key = Key::try_from(key).map_err(|_| Error::KeySize)?;
let nonce = Nonce::try_from(nonce).map_err(|_| Error::IvSize)?;
let mut cipher = ChaCha20::new(&key, &nonce.into());
impl Cipher {
/// Create a new cipher instance.
pub fn new(key: &ChaChaKey, nonce: &ChaChaNonce) -> Self {
let mut cipher = ChaCha20::new(&key, nonce.into());
let mut poly1305_key = poly1305::Key::default();
cipher.apply_keystream(&mut poly1305_key);

Expand All @@ -37,14 +88,19 @@ impl ChaCha20Poly1305 {
// Seek to block 1
cipher.seek(64);

Ok(Self { cipher, mac })
Self { cipher, mac }
}

/// Encrypt the provided `buffer` in-place, returning the Poly1305 authentication tag.
#[inline]
pub fn encrypt(mut self, buffer: &mut [u8]) -> Tag {
pub fn encrypt(mut self, associated_data: &[u8], buffer: &mut [u8]) -> Result<Tag> {
// TODO(tarcieri): support associated data (RustCrypto/SSH#279)
if !associated_data.is_empty() {
return Err(Error);
}

self.cipher.apply_keystream(buffer);
self.mac.compute_unpadded(buffer).into()
Ok(self.mac.compute_unpadded(buffer).into())
}

/// Decrypt the provided `buffer` in-place, verifying it against the provided Poly1305
Expand All @@ -55,14 +111,19 @@ impl ChaCha20Poly1305 {
///
/// Upon success, `Ok(())` is returned and `buffer` is rewritten with the decrypted plaintext.
#[inline]
pub fn decrypt(mut self, buffer: &mut [u8], tag: Tag) -> Result<()> {
pub fn decrypt(mut self, associated_data: &[u8], buffer: &mut [u8], tag: Tag) -> Result<()> {
// TODO(tarcieri): support associated data (RustCrypto/SSH#279)
if !associated_data.is_empty() {
return Err(Error);
}

let expected_tag = self.mac.compute_unpadded(buffer);

if expected_tag.ct_eq(&tag).into() {
self.cipher.apply_keystream(buffer);
Ok(())
} else {
Err(Error::Crypto)
Err(Error)
}
}
}
48 changes: 29 additions & 19 deletions ssh-cipher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,24 @@ mod encryptor;

pub use crate::error::{Error, Result};

#[cfg(feature = "chacha20poly1305")]
pub use crate::chacha20poly1305::ChaCha20Poly1305;

#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))]
pub use crate::{decryptor::Decryptor, encryptor::Encryptor};

#[cfg(feature = "chacha20poly1305")]
pub use crate::chacha20poly1305::{ChaCha20Poly1305, ChaChaKey, ChaChaNonce};

use cipher::array::{typenum::U16, Array};
use core::{fmt, str};
use encoding::{Label, LabelError};

#[cfg(feature = "aes-gcm")]
use aes_gcm::{aead::AeadInPlace, Aes128Gcm, Aes256Gcm};
use {
aead::array::typenum::U12,
aes_gcm::{Aes128Gcm, Aes256Gcm},
};

#[cfg(feature = "aes-gcm")]
use cipher::KeyInit;
#[cfg(any(feature = "aes-gcm", feature = "chacha20poly1305"))]
use aead::{AeadInPlace, KeyInit};

/// AES-128 in block chaining (CBC) mode
const AES128_CBC: &str = "aes128-cbc";
Expand Down Expand Up @@ -80,17 +84,15 @@ const CHACHA20_POLY1305: &str = "[email protected]";
/// Triple-DES in block chaining (CBC) mode
const TDES_CBC: &str = "3des-cbc";

/// Nonce for AEAD modes.
///
/// This is used by e.g. `[email protected]`/`[email protected]` and
/// `[email protected]`.
pub type Nonce = [u8; 12];
/// Nonce for `[email protected]`/`[email protected]`.
#[cfg(feature = "aes-gcm")]
pub type AesGcmNonce = Array<u8, U12>;

/// Authentication tag for ciphertext data.
///
/// This is used by e.g. `[email protected]`/`[email protected]` and
/// `[email protected]`.
pub type Tag = [u8; 16];
pub type Tag = Array<u8, U16>;

/// Counter mode with a 128-bit big endian counter.
#[cfg(feature = "aes-ctr")]
Expand Down Expand Up @@ -172,7 +174,7 @@ impl Cipher {
Self::Aes256Ctr => Some((32, 16)),
Self::Aes128Gcm => Some((16, 12)),
Self::Aes256Gcm => Some((32, 12)),
Self::ChaCha20Poly1305 => Some((32, 12)),
Self::ChaCha20Poly1305 => Some((32, 8)),
Self::TDesCbc => Some((24, 8)),
}
}
Expand Down Expand Up @@ -233,7 +235,7 @@ impl Cipher {
#[cfg(feature = "aes-gcm")]
Self::Aes128Gcm => {
let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?;
let tag = tag.ok_or(Error::TagSize)?;
cipher
.decrypt_in_place_detached(&nonce.into(), &[], buffer, &tag.into())
Expand All @@ -244,7 +246,7 @@ impl Cipher {
#[cfg(feature = "aes-gcm")]
Self::Aes256Gcm => {
let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?;
let tag = tag.ok_or(Error::TagSize)?;
cipher
.decrypt_in_place_detached(&nonce.into(), &[], buffer, &tag.into())
Expand All @@ -254,8 +256,12 @@ impl Cipher {
}
#[cfg(feature = "chacha20poly1305")]
Self::ChaCha20Poly1305 => {
let key = key.try_into().map_err(|_| Error::KeySize)?;
let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
let tag = tag.ok_or(Error::TagSize)?;
ChaCha20Poly1305::new(key, iv)?.decrypt(buffer, tag)
ChaCha20Poly1305::new(key)
.decrypt_in_place_detached(nonce, b"", buffer, &tag)
.map_err(|_| Error::Crypto)
}
// Use `Decryptor` for non-AEAD modes
#[cfg(any(feature = "aes-cbc", feature = "aes-ctr", feature = "tdes"))]
Expand Down Expand Up @@ -294,7 +300,7 @@ impl Cipher {
#[cfg(feature = "aes-gcm")]
Self::Aes128Gcm => {
let cipher = Aes128Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?;
let tag = cipher
.encrypt_in_place_detached(&nonce.into(), &[], buffer)
.map_err(|_| Error::Crypto)?;
Expand All @@ -304,7 +310,7 @@ impl Cipher {
#[cfg(feature = "aes-gcm")]
Self::Aes256Gcm => {
let cipher = Aes256Gcm::new_from_slice(key).map_err(|_| Error::KeySize)?;
let nonce = Nonce::try_from(iv).map_err(|_| Error::IvSize)?;
let nonce = AesGcmNonce::try_from(iv).map_err(|_| Error::IvSize)?;
let tag = cipher
.encrypt_in_place_detached(&nonce.into(), &[], buffer)
.map_err(|_| Error::Crypto)?;
Expand All @@ -313,7 +319,11 @@ impl Cipher {
}
#[cfg(feature = "chacha20poly1305")]
Self::ChaCha20Poly1305 => {
let tag = ChaCha20Poly1305::new(key, iv)?.encrypt(buffer);
let key = key.try_into().map_err(|_| Error::KeySize)?;
let nonce = iv.try_into().map_err(|_| Error::IvSize)?;
let tag = ChaCha20Poly1305::new(key)
.encrypt_in_place_detached(nonce, b"", buffer)
.map_err(|_| Error::Crypto)?;
Ok(Some(tag))
}
// Use `Encryptor` for non-AEAD modes
Expand Down
2 changes: 1 addition & 1 deletion ssh-key/src/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl Kdf {
// key encryption, relying on a unique salt used in the password-based encryption key
// derivation to ensure that each encryption key is only used once.
if cipher == Cipher::ChaCha20Poly1305 {
iv.copy_from_slice(&cipher::Nonce::default());
iv.copy_from_slice(&cipher::ChaChaNonce::default());
}

Ok((okm, iv))
Expand Down

0 comments on commit e3c97d5

Please sign in to comment.