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

ssh-cipher: make ChaCha20Poly1305 accept a 256-bit key #254

Merged
merged 1 commit into from
Jul 28, 2024
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
37 changes: 3 additions & 34 deletions ssh-cipher/src/chacha20poly1305.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
//! OpenSSH variant of ChaCha20Poly1305: `chacha20-poly1305@openssh.com`
//!
//! Differences from ChaCha20Poly1305 as described in RFC8439:
//!
//! - Construction uses two separately keyed instances of ChaCha20: one for data, one for lengths
//! - The input of Poly1305 is not padded
//! - The lengths of ciphertext and AAD are not authenticated using Poly1305
//!
Expand All @@ -14,47 +12,18 @@ use cipher::{KeyInit, KeyIvInit, StreamCipher, StreamCipherSeek};
use poly1305::Poly1305;
use subtle::ConstantTimeEq;

const KEY_SIZE: usize = 32;

pub(crate) struct ChaCha20Poly1305 {
cipher: ChaCha20,
mac: Poly1305,
}

impl ChaCha20Poly1305 {
/// Create a new [`ChaCha20Poly1305`] instance with a 64-byte key.
/// From [PROTOCOL.chacha20poly1305]:
///
/// > The chacha20-poly1305@openssh.com cipher requires 512 bits of key
/// > material as output from the SSH key exchange. This forms two 256 bit
/// > keys (K_1 and K_2), used by two separate instances of chacha20.
/// > The first 256 bits constitute K_2 and the second 256 bits become
/// > K_1.
/// >
/// > The instance keyed by K_1 is a stream cipher that is used only
/// > to encrypt the 4 byte packet length field. The second instance,
/// > keyed by K_2, is used in conjunction with poly1305 to build an AEAD
/// > (Authenticated Encryption with Associated Data) that is used to encrypt
/// > and authenticate the entire packet.
/// 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> {
#[allow(clippy::arithmetic_side_effects)]
if key.len() != KEY_SIZE * 2 {
return Err(Error::KeySize);
}

// TODO(tarcieri): support for using both keys
let (k_2, _k_1) = key.split_at(KEY_SIZE);
let key = Key::try_from(k_2).map_err(|_| Error::KeySize)?;

let nonce = if nonce.is_empty() {
// For key encryption
Nonce::default()
} else {
Nonce::try_from(nonce).map_err(|_| Error::IvSize)?
};

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());
let mut poly1305_key = poly1305::Key::default();
cipher.apply_keystream(&mut poly1305_key);
Expand Down
2 changes: 1 addition & 1 deletion ssh-cipher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ impl Cipher {
Self::Aes256Ctr => Some((32, 16)),
Self::Aes128Gcm => Some((16, 12)),
Self::Aes256Gcm => Some((32, 12)),
Self::ChaCha20Poly1305 => Some((64, 0)),
Self::ChaCha20Poly1305 => Some((32, 12)),
Self::TDesCbc => Some((24, 8)),
}
}
Expand Down
33 changes: 31 additions & 2 deletions ssh-key/src/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,43 @@ impl Kdf {
cipher: Cipher,
password: impl AsRef<[u8]>,
) -> Result<(Zeroizing<Vec<u8>>, Vec<u8>)> {
let (key_size, iv_size) = cipher.key_and_iv_size().ok_or(Error::Decrypted)?;
let (key_size, iv_size) = match cipher {
// Derive two ChaCha20Poly1305 keys, but only use the first.
// In the typical SSH protocol, the second key is used for length encryption.
//
// From `PROTOCOL.chacha20poly1305`:
//
// > The chacha20-poly1305@openssh.com cipher requires 512 bits of key
// > material as output from the SSH key exchange. This forms two 256 bit
// > keys (K_1 and K_2), used by two separate instances of chacha20.
// > The first 256 bits constitute K_2 and the second 256 bits become
// > K_1.
// >
// > The instance keyed by K_1 is a stream cipher that is used only
// > to encrypt the 4 byte packet length field. The second instance,
// > keyed by K_2, is used in conjunction with poly1305 to build an AEAD
// > (Authenticated Encryption with Associated Data) that is used to encrypt
// > and authenticate the entire packet.
Cipher::ChaCha20Poly1305 => (64, 0),
_ => cipher.key_and_iv_size().ok_or(Error::Decrypted)?,
};

let okm_size = key_size
.checked_add(iv_size)
.ok_or(encoding::Error::Length)?;

let mut okm = Zeroizing::new(vec![0u8; okm_size]);
self.derive(password, &mut okm)?;
let iv = okm.split_off(key_size);
let mut iv = okm.split_off(key_size);

if cipher == Cipher::ChaCha20Poly1305 {
// Only use the first ChaCha20 key.
okm.truncate(32);

// Use an all-zero nonce (with a key derived from password + salt providing uniqueness)
iv.extend_from_slice(&cipher::Nonce::default());
}

Ok((okm, iv))
}

Expand Down
Loading