diff --git a/Cargo.toml b/Cargo.toml index 479d279f2f75aed9a07594c840ede2b0e283101b..9e824d09accfe29f5c4e634929180c6e34f00ecb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ digest = "0.10" signature = { version = "1.4", default-features = false } zeroize = { version = "1", default-features = false, features = ["derive"] } cipher = { version = "0.4", features = ["zeroize"] } +subtle = { version = "2.4", default-features = false } # ed25519/x25519 # fork allows hashing by parts (sign/verify from sshwire), and zeroize salty = { version = "0.2", git = "https://github.com/mkj/salty", branch = "sunset" } diff --git a/src/encrypt.rs b/src/encrypt.rs index 8bca668b7d78370ce18037a6ffcbcaeb5ec67871..0a986f74a27e9cbae7bb9367bc341e2c940a2089 100644 --- a/src/encrypt.rs +++ b/src/encrypt.rs @@ -512,7 +512,7 @@ impl Cipher { /// Length in bytes pub fn key_len(&self) -> usize { match self { - Cipher::ChaPoly => SSHChaPoly::key_len(), + Cipher::ChaPoly => SSHChaPoly::KEY_LEN, Cipher::Aes256Ctr => aes::Aes256::key_size(), } } @@ -564,7 +564,7 @@ impl EncKey { ) -> Result<Self, Error> { match cipher { Cipher::ChaPoly => { - Ok(EncKey::ChaPoly(SSHChaPoly::new(key).trap()?)) + Ok(EncKey::ChaPoly(SSHChaPoly::new_from_slice(key).trap()?)) } Cipher::Aes256Ctr => Ok(EncKey::Aes256Ctr( Aes256Ctr32BE::new_from_slices(key, iv).trap()?, @@ -614,7 +614,7 @@ impl DecKey { ) -> Result<Self, Error> { match cipher { Cipher::ChaPoly => { - Ok(DecKey::ChaPoly(SSHChaPoly::new(key).trap()?)) + Ok(DecKey::ChaPoly(SSHChaPoly::new_from_slice(key).trap()?)) } Cipher::Aes256Ctr => Ok(DecKey::Aes256Ctr( Aes256Ctr32BE::new_from_slices(key, iv).trap()?, @@ -702,7 +702,7 @@ impl IntegKey { } pub fn size_out(&self) -> usize { match self { - IntegKey::ChaPoly => SSHChaPoly::tag_len(), + IntegKey::ChaPoly => SSHChaPoly::TAG_LEN, IntegKey::HmacSha256(_) => sha2::Sha256::output_size(), IntegKey::NoInteg => 0, } diff --git a/src/ssh_chapoly.rs b/src/ssh_chapoly.rs index a378a6e3fc142c992c1b952417c8b2bb8fe737cc..41698174e964c65050b21a354ebf87419ff4b666 100644 --- a/src/ssh_chapoly.rs +++ b/src/ssh_chapoly.rs @@ -9,23 +9,29 @@ use chacha20::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek, StreamCipherSe use poly1305::Poly1305; use poly1305::universal_hash::{NewUniversalHash, UniversalHash}; use poly1305::universal_hash::generic_array::GenericArray; +use subtle::ConstantTimeEq; use zeroize::{Zeroize, ZeroizeOnDrop}; use crate::*; use encrypt::SSH_LENGTH_SIZE; #[derive(Clone, ZeroizeOnDrop)] +/// `chacha20-poly1305@openssh.com` authenticated cipher pub struct SSHChaPoly { + /// Length key k1: [u8; 32], + /// Packet key k2: [u8; 32], } -const TAG_LEN: usize = 16; -const KEY_LEN: usize = 64; impl SSHChaPoly { - pub fn new(key: &[u8]) -> Result<Self> { - if key.len() != KEY_LEN { + pub const TAG_LEN: usize = 16; + pub const KEY_LEN: usize = 64; + + /// `key` must be 64 bytes + pub fn new_from_slice(key: &[u8]) -> Result<Self> { + if key.len() != Self::KEY_LEN { return Err(Error::BadKey) } let mut k1 = [0u8; 32]; @@ -38,20 +44,15 @@ impl SSHChaPoly { }) } - pub const fn tag_len() -> usize { - TAG_LEN - } - - pub const fn key_len() -> usize { - KEY_LEN - } - fn cha20(key: &[u8; 32], seq: u32) -> ChaCha20 { let mut nonce = [0u8; 12]; nonce[8..].copy_from_slice(&seq.to_be_bytes()); ChaCha20::new(key.into(), (&nonce).into()) } + /// Decrypts the packet length. + /// + /// `buf` must be at least 4 bytes, extra data is ignored. pub fn packet_length(&self, seq: u32, buf: &[u8]) -> Result<u32> { if buf.len() < SSH_LENGTH_SIZE { return Err(Error::BadDecrypt); @@ -62,27 +63,32 @@ impl SSHChaPoly { Ok(u32::from_be_bytes(b.try_into().unwrap())) } + /// Decrypts in-place and validates the MAC. + /// + /// Length has already been decrypted by `packet_length()`. pub fn decrypt(&self, seq: u32, msg: &mut [u8], mac: &[u8]) -> Result<()> { if msg.len() < SSH_LENGTH_SIZE { return Err(Error::BadDecrypt); } - if mac.len() != TAG_LEN { + if mac.len() != Self::TAG_LEN { return Err(Error::BadDecrypt); } - let msg_tag = poly1305::Tag::new(*GenericArray::from_slice(mac)); let mut c = Self::cha20(&self.k2, seq); let mut poly_key = [0u8; 32]; c.apply_keystream(&mut poly_key); // check tag + let msg_tag = poly1305::Tag::new(*GenericArray::from_slice(mac)); let poly = Poly1305::new((&poly_key).into()); + // compute_unpadded() adds the necessary trailing 1 byte when padding output let tag = poly.compute_unpadded(msg); - if tag != msg_tag { + let good: bool = tag.ct_eq(&msg_tag).into(); + if !good { return Err(Error::BadDecrypt); } - // encrypt payload + // decrypt payload let (_, payload) = msg.split_at_mut(SSH_LENGTH_SIZE); // set block counter to 1 c.seek(64u32); @@ -90,11 +96,12 @@ impl SSHChaPoly { Ok(()) } + /// Encrypt in-place, including length, payload, MAC. pub fn encrypt(&self, seq: u32, msg: &mut [u8], mac: &mut [u8]) -> Result<()> { if msg.len() < SSH_LENGTH_SIZE { return Err(Error::BadDecrypt); } - if mac.len() != TAG_LEN { + if mac.len() != Self::TAG_LEN { return Err(Error::BadDecrypt); }