mod chachapoly;
mod ctr;
mod gcm;
use chachapoly::ChaChaPoly;
use ctr::AesCtr;
use gcm::GcmState;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy)]
pub struct CipherSpec {
pub name: &'static str,
pub key_len: usize,
pub iv_len: usize,
pub block_size: usize,
pub aead: bool,
pub tag_len: usize,
}
pub const ALL: &[CipherSpec] = &[
CipherSpec {
name: "chacha20-poly1305@openssh.com",
key_len: 64,
iv_len: 0,
block_size: 8,
aead: true,
tag_len: 16,
},
CipherSpec {
name: "aes256-gcm@openssh.com",
key_len: 32,
iv_len: 12,
block_size: 16,
aead: true,
tag_len: 16,
},
CipherSpec {
name: "aes128-gcm@openssh.com",
key_len: 16,
iv_len: 12,
block_size: 16,
aead: true,
tag_len: 16,
},
CipherSpec {
name: "aes256-ctr",
key_len: 32,
iv_len: 16,
block_size: 16,
aead: false,
tag_len: 0,
},
CipherSpec {
name: "aes192-ctr",
key_len: 24,
iv_len: 16,
block_size: 16,
aead: false,
tag_len: 0,
},
CipherSpec {
name: "aes128-ctr",
key_len: 16,
iv_len: 16,
block_size: 16,
aead: false,
tag_len: 0,
},
];
pub fn by_name(name: &str) -> Option<&'static CipherSpec> {
ALL.iter().find(|c| c.name == name)
}
pub enum SshCipher {
Ctr(AesCtr),
Gcm(GcmState),
ChaChaPoly(ChaChaPoly),
}
impl SshCipher {
pub fn new(name: &str, key: &[u8], iv: &[u8]) -> Result<Self> {
match name {
"aes128-ctr" => Ok(SshCipher::Ctr(AesCtr::new_128(key, iv)?)),
"aes192-ctr" => Ok(SshCipher::Ctr(AesCtr::new_192(key, iv)?)),
"aes256-ctr" => Ok(SshCipher::Ctr(AesCtr::new_256(key, iv)?)),
"aes128-gcm@openssh.com" => Ok(SshCipher::Gcm(GcmState::new_128(key, iv)?)),
"aes256-gcm@openssh.com" => Ok(SshCipher::Gcm(GcmState::new_256(key, iv)?)),
"chacha20-poly1305@openssh.com" => Ok(SshCipher::ChaChaPoly(ChaChaPoly::new(key)?)),
_ => Err(Error::Unsupported("cipher")),
}
}
pub fn is_aead(&self) -> bool {
!matches!(self, SshCipher::Ctr(_))
}
pub fn encrypts_length(&self) -> bool {
!matches!(self, SshCipher::Gcm(_))
}
pub fn stream(&mut self, buf: &mut [u8]) -> Result<()> {
match self {
SshCipher::Ctr(c) => {
c.apply_keystream(buf);
Ok(())
}
_ => Err(Error::Unsupported("stream op on AEAD cipher")),
}
}
pub fn aead_seal_len_aad(&mut self, length: &[u8], payload: &mut [u8]) -> Result<[u8; 16]> {
match self {
SshCipher::Gcm(g) => Ok(g.seal(length, payload)),
_ => Err(Error::Unsupported("GCM seal on non-GCM cipher")),
}
}
pub fn aead_open_len_aad(
&mut self,
length: &[u8],
payload: &mut [u8],
tag: &[u8],
) -> Result<()> {
match self {
SshCipher::Gcm(g) => g.open(length, payload, tag),
_ => Err(Error::Unsupported("GCM open on non-GCM cipher")),
}
}
pub fn cp_xor_length(&self, seq: u64, buf: &mut [u8]) -> Result<()> {
match self {
SshCipher::ChaChaPoly(c) => {
c.xor_length(seq, buf);
Ok(())
}
_ => Err(Error::Unsupported("chacha20-poly1305 op on other cipher")),
}
}
pub fn cp_xor_payload(&self, seq: u64, buf: &mut [u8]) -> Result<()> {
match self {
SshCipher::ChaChaPoly(c) => {
c.xor_payload(seq, buf);
Ok(())
}
_ => Err(Error::Unsupported("chacha20-poly1305 op on other cipher")),
}
}
pub fn cp_tag(&self, seq: u64, enc_len: &[u8], enc_payload: &[u8]) -> Result<[u8; 16]> {
match self {
SshCipher::ChaChaPoly(c) => Ok(c.tag(seq, enc_len, enc_payload)),
_ => Err(Error::Unsupported("chacha20-poly1305 op on other cipher")),
}
}
pub fn cp_verify_tag(
&self,
seq: u64,
enc_len: &[u8],
enc_payload: &[u8],
tag: &[u8],
) -> Result<()> {
match self {
SshCipher::ChaChaPoly(c) => c.verify_tag(seq, enc_len, enc_payload, tag),
_ => Err(Error::Unsupported("chacha20-poly1305 op on other cipher")),
}
}
}
pub fn cipher_by_name(name: &str, key: &[u8], iv: &[u8]) -> Option<Result<SshCipher>> {
by_name(name).map(|_| SshCipher::new(name, key, iv))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn factory_dispatches_known_names() {
for spec in ALL {
let key = vec![0u8; spec.key_len];
let iv = vec![0u8; spec.iv_len];
let r = cipher_by_name(spec.name, &key, &iv).expect("known");
r.unwrap();
}
}
#[test]
fn factory_rejects_unknown() {
assert!(cipher_by_name("nope", &[], &[]).is_none());
}
#[test]
fn ctr_roundtrip_via_trait() {
let key = [0x42u8; 16];
let iv = [0x10u8; 16];
let mut enc = SshCipher::new("aes128-ctr", &key, &iv).unwrap();
let mut dec = SshCipher::new("aes128-ctr", &key, &iv).unwrap();
let plain = b"hello ssh world!".to_vec();
let mut buf = plain.clone();
enc.stream(&mut buf).unwrap();
assert_ne!(buf, plain);
dec.stream(&mut buf).unwrap();
assert_eq!(buf, plain);
}
#[test]
fn gcm_roundtrip_via_trait() {
let key = [0x01u8; 16];
let iv = [0x02u8; 12];
let mut enc = SshCipher::new("aes128-gcm@openssh.com", &key, &iv).unwrap();
let mut dec = SshCipher::new("aes128-gcm@openssh.com", &key, &iv).unwrap();
let aad = [0u8, 0, 0, 16];
let plain = [0xabu8; 16];
let mut buf = plain;
let tag = enc.aead_seal_len_aad(&aad, &mut buf).unwrap();
dec.aead_open_len_aad(&aad, &mut buf, &tag).unwrap();
assert_eq!(buf, plain);
}
#[test]
fn chachapoly_roundtrip_via_trait() {
let mut key = [0u8; 64];
for (i, b) in key.iter_mut().enumerate() {
*b = i as u8;
}
let enc = SshCipher::new("chacha20-poly1305@openssh.com", &key, &[]).unwrap();
let dec = SshCipher::new("chacha20-poly1305@openssh.com", &key, &[]).unwrap();
let seq = 42u64;
let mut len_buf = [0u8, 0, 0, 16];
let plain_len = len_buf;
enc.cp_xor_length(seq, &mut len_buf).unwrap();
assert_ne!(len_buf, plain_len);
let mut payload = [0xa5u8; 16];
let plain_payload = payload;
enc.cp_xor_payload(seq, &mut payload).unwrap();
let tag = enc.cp_tag(seq, &len_buf, &payload).unwrap();
dec.cp_verify_tag(seq, &len_buf, &payload, &tag).unwrap();
dec.cp_xor_payload(seq, &mut payload).unwrap();
assert_eq!(payload, plain_payload);
dec.cp_xor_length(seq, &mut len_buf).unwrap();
assert_eq!(len_buf, plain_len);
}
#[test]
fn cross_kind_ops_return_unsupported() {
let mut g = SshCipher::new("aes128-gcm@openssh.com", &[0u8; 16], &[0u8; 12]).unwrap();
assert!(matches!(g.stream(&mut []), Err(Error::Unsupported(_))));
let mut c = SshCipher::new("aes128-ctr", &[0u8; 16], &[0u8; 16]).unwrap();
assert!(matches!(
c.aead_seal_len_aad(&[], &mut []),
Err(Error::Unsupported(_))
));
}
}