Skip to content

Commit

Permalink
ssh-cipher: make ChaCha20Poly1305 accept a 256-bit key (#254)
Browse files Browse the repository at this point in the history
In the OpenSSH protocol, ChaCha20Poly1305 uses two different keys: one
for length encryption, and one for payloads.

It wasn't previously possible to use both keys (or at least quite
obtuse) because `ssh-cipher` chopped up `k_1` and `k_2` and only allowed
`k_1` to be used.

This change hoists that into `ssh-key` for file encryption uses, giving
access to the raw `ChaCha20Poly1305` implementation and allowing use of
either key.

In addition, a nonce is now mandatory, but use of the default all-zero
nonce for file encryption purposes is now handled in `ssh-key`.
  • Loading branch information
tarcieri authored Jul 28, 2024
1 parent e64c44f commit 30e70e9
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 37 deletions.
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: `[email protected]`
//!
//! 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 [email protected] 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 [email protected] 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

0 comments on commit 30e70e9

Please sign in to comment.