good stuff

This commit is contained in:
nora 2024-08-23 02:14:16 +02:00
parent d340ff0861
commit c4bb37e570
8 changed files with 342 additions and 54 deletions

View file

@ -45,7 +45,7 @@ impl Request {
} => {
p.u8(numbers::SSH_AGENTC_ADD_IDENTITY);
p.string(key_type.as_bytes());
p.write(&key_contents);
p.raw(&key_contents);
p.string(key_comment.as_bytes());
}
Self::RemoveAllIdentities => p.u8(numbers::SSH_AGENTC_REMOVE_ALL_IDENTITIES),

View file

@ -7,5 +7,7 @@ edition = "2021"
aes = "0.8.4"
bcrypt-pbkdf = "0.10.0"
ctr = "0.9.2"
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
pem = "3.0.4"
rand = "0.8.5"
ssh-transport = { path = "../ssh-transport" }

View file

@ -1,7 +1,9 @@
use std::str::FromStr;
use aes::cipher::{KeySizeUser, StreamCipher};
use ssh_transport::parse::{self, Parser};
use ssh_transport::parse::{self, Parser, Writer};
use crate::PrivateKeyType;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cipher {
@ -27,6 +29,13 @@ impl FromStr for Cipher {
}
impl Cipher {
pub fn name(&self) -> &'static str {
match self {
Self::None => "none",
Self::Aes256Ctr => "aes256-ctr",
}
}
pub(crate) fn key_iv_size(&self) -> (usize, usize) {
match self {
Cipher::None => (0, 0),
@ -34,7 +43,8 @@ impl Cipher {
}
}
pub(crate) fn decrypt_in_place(&self, data: &mut [u8], key: &[u8], iv: &[u8]) {
/// Decrypt or encrypt a buffer in place (the same operation due to stream ciphers).
pub(crate) fn crypt_in_place(&self, data: &mut [u8], key: &[u8], iv: &[u8]) {
match self {
Cipher::None => unreachable!("cannot decrypt none cipher"),
Cipher::Aes256Ctr => {
@ -84,6 +94,25 @@ impl Kdf {
Ok(kdf)
}
pub fn name(&self) -> &'static str {
match self {
Self::None => "none",
Self::BCrypt { .. } => "bcrypt",
}
}
pub fn options(&self) -> Vec<u8> {
match self {
Self::None => Vec::new(),
Self::BCrypt { salt, rounds } => {
let mut opts = Writer::new();
opts.string(salt);
opts.u32(*rounds);
opts.finish()
}
}
}
pub(crate) fn derive(&self, passphrase: &str, output: &mut [u8]) -> parse::Result<()> {
match self {
Self::None => unreachable!("should not attempt to derive passphrase from none"),
@ -95,3 +124,24 @@ impl Kdf {
}
}
}
pub enum KeyType {
Ed25519,
}
pub struct KeyGenerationParams {
pub key_type: KeyType,
}
pub(crate) fn generate_private_key(params: KeyGenerationParams) -> PrivateKeyType {
match params.key_type {
KeyType::Ed25519 => {
let private_key = ed25519_dalek::SigningKey::generate(&mut rand::rngs::OsRng);
PrivateKeyType::Ed25519 {
public_key: private_key.verifying_key().to_bytes(),
private_key: private_key.to_bytes(),
}
}
}
}

View file

@ -3,9 +3,13 @@ mod crypto;
use crypto::{Cipher, Kdf};
use ssh_transport::{
key::PublicKey,
parse::{self, Parser},
parse::{self, Parser, Writer},
};
// TODO: good typed error messages so the user knows what's going on
pub use crypto::{KeyGenerationParams, KeyType};
pub struct EncryptedPrivateKeys {
pub public_keys: Vec<PublicKey>,
pub cipher: Cipher,
@ -16,6 +20,7 @@ pub struct EncryptedPrivateKeys {
pub struct PlaintextPrivateKey {
pub private_key: PrivateKeyType,
pub comment: String,
checkint: u32,
}
pub enum PrivateKeyType {
@ -29,7 +34,7 @@ const MAGIC: &[u8; 15] = b"openssh-key-v1\0";
impl EncryptedPrivateKeys {
/// Parse OpenSSH private keys, either armored or not.
pub fn parse_unencrypted(content: &[u8]) -> parse::Result<Self> {
pub fn parse(content: &[u8]) -> parse::Result<Self> {
// https://github.com/openssh/openssh-portable/blob/a76a6b85108e3032c8175611ecc5746e7131f876/PROTOCOL.key
let pem: pem::Pem; // lifetime extension
let content = if content.starts_with(b"openssh-key-v1") {
@ -76,14 +81,35 @@ impl EncryptedPrivateKeys {
})
}
pub fn to_bytes_armored(&self) -> String {
let content = self.to_bytes();
let pem = pem::Pem::new("OPENSSH PRIVATE KEY", content);
pem::encode(&pem)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut p = Writer::new();
p.array(*MAGIC);
p.string(self.cipher.name().as_bytes());
p.string(self.kdf.name().as_bytes());
p.string(&self.kdf.options());
p.u32(self.public_keys.len() as u32);
for pubkey in &self.public_keys {
p.string(&pubkey.to_wire_encoding());
}
p.string(&self.encrypted_private_keys);
p.finish()
}
pub fn requires_passphrase(&self) -> bool {
(!matches!(self.kdf, Kdf::None)) && (!matches!(self.cipher, Cipher::None))
}
pub fn parse_private(
&self,
passphrase: Option<&str>,
) -> parse::Result<Vec<PlaintextPrivateKey>> {
pub fn decrypt_encrypted_part(&self, passphrase: Option<&str>) -> parse::Result<Vec<u8>> {
let mut data = self.encrypted_private_keys.clone();
if self.requires_passphrase() {
let Some(passphrase) = passphrase else {
@ -98,16 +124,22 @@ impl EncryptedPrivateKeys {
let mut output = vec![0; key_size + iv_size];
self.kdf.derive(passphrase, &mut output)?;
let (key, iv) = output.split_at(key_size);
self.cipher.decrypt_in_place(&mut data, &key, &iv);
self.cipher.crypt_in_place(&mut data, &key, &iv);
}
Ok(data)
}
pub fn parse_private(
&self,
passphrase: Option<&str>,
) -> parse::Result<Vec<PlaintextPrivateKey>> {
let data = self.decrypt_encrypted_part(passphrase)?;
let mut p = Parser::new(&data);
let checkint1 = p.u32()?;
let checkint2 = p.u32()?;
if checkint1 != checkint2 {
return Err(parse::ParseError(format!(
"failed sanity check, invalid key or password ({checkint1}!={checkint2})"
)));
return Err(parse::ParseError(format!("invalid key or password")));
}
let mut result_keys = Vec::new();
@ -115,6 +147,7 @@ impl EncryptedPrivateKeys {
for pubkey in &self.public_keys {
let keytype = match pubkey {
PublicKey::Ed25519 { public_key } => {
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-3.2.3>
let alg = p.utf8_string()?;
if alg != "ssh-ed25519" {
return Err(parse::ParseError(format!(
@ -150,6 +183,7 @@ impl EncryptedPrivateKeys {
result_keys.push(PlaintextPrivateKey {
private_key: keytype,
comment: comment.to_owned(),
checkint: checkint1,
});
}
@ -169,6 +203,98 @@ impl EncryptedPrivateKeys {
}
}
pub struct KeyEncryptionParams {
pub cipher: Cipher,
pub kdf: Kdf,
pub passphrase: Option<String>,
}
impl KeyEncryptionParams {
pub fn secure_or_none(passphrase: String) -> Self {
if passphrase.is_empty() {
Self {
cipher: Cipher::None,
kdf: Kdf::None,
passphrase: None,
}
} else {
Self {
cipher: Cipher::Aes256Ctr,
kdf: Kdf::BCrypt {
salt: rand::random(),
rounds: 24,
},
passphrase: Some(passphrase),
}
}
}
}
impl PlaintextPrivateKey {
pub fn generate(comment: String, params: KeyGenerationParams) -> Self {
let keytype = crypto::generate_private_key(params);
Self {
comment,
private_key: keytype,
checkint: rand::random(),
}
}
pub fn encrypt(&self, params: KeyEncryptionParams) -> parse::Result<EncryptedPrivateKeys> {
let public_keys = vec![self.private_key.public_key()];
let mut enc = Writer::new();
enc.u32(self.checkint);
enc.u32(self.checkint);
match self.private_key {
PrivateKeyType::Ed25519 {
public_key,
private_key,
} => {
// <https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent#section-3.2.3>
enc.string(b"ssh-ed25519");
enc.string(&public_key);
let combined = private_key.len() + public_key.len();
enc.u32(combined as u32);
enc.raw(&private_key);
enc.raw(&public_key);
enc.string(&self.comment.as_bytes());
}
}
// uh..., i don't really now how much i need to pad so YOLO this here
// TODO: pad properly.
enc.u8(1);
enc.u8(2);
let mut encrypted_private_keys = enc.finish();
match params.cipher {
Cipher::None => {}
Cipher::Aes256Ctr => {
let (key_size, iv_size) = params.cipher.key_iv_size();
let mut output = vec![0; key_size + iv_size];
params
.kdf
.derive(&params.passphrase.unwrap(), &mut output)?;
let (key, iv) = output.split_at(key_size);
params
.cipher
.crypt_in_place(&mut encrypted_private_keys, &key, &iv);
}
}
Ok(EncryptedPrivateKeys {
public_keys,
cipher: params.cipher,
kdf: params.kdf,
encrypted_private_keys,
})
}
}
impl PrivateKeyType {
pub fn public_key(&self) -> PublicKey {
match *self {
@ -179,7 +305,7 @@ impl PrivateKeyType {
#[cfg(test)]
mod tests {
use crate::{Cipher, EncryptedPrivateKeys, Kdf, PrivateKeyType};
use crate::{Cipher, EncryptedPrivateKeys, Kdf, KeyEncryptionParams, PrivateKeyType};
// ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPaiIO6MePXM/QCJWVge1k4dsiefPr4taP9VJbCtXdx uwu
// Password: 'test'
@ -206,7 +332,7 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
#[test]
fn ed25519_none() {
let keys = EncryptedPrivateKeys::parse_unencrypted(TEST_ED25519_NONE).unwrap();
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap();
assert_eq!(keys.public_keys.len(), 1);
assert_eq!(keys.cipher, Cipher::None);
assert_eq!(keys.kdf, Kdf::None);
@ -218,9 +344,22 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
}
#[test]
fn roundtrip_ed25519_none() {
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap();
let decrypted = keys.parse_private(None).unwrap();
let encrypted = decrypted[0]
.encrypt(KeyEncryptionParams::secure_or_none("".to_owned()))
.unwrap();
let bytes = encrypted.to_bytes();
assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes);
}
#[test]
fn ed25519_aes256ctr() {
let keys = EncryptedPrivateKeys::parse_unencrypted(TEST_ED25519_AES256_CTR).unwrap();
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_AES256_CTR).unwrap();
assert_eq!(keys.public_keys.len(), 1);
assert_eq!(keys.cipher, Cipher::Aes256Ctr);
assert!(matches!(keys.kdf, Kdf::BCrypt { .. }));
@ -231,4 +370,17 @@ zukcSwhnKrg+wzw7/JZQAAAAA3V3dQEC
assert_eq!(key.comment, "uwu");
assert!(matches!(key.private_key, PrivateKeyType::Ed25519 { .. }));
}
#[test]
fn roundtrip_aes256ctr() {
let keys = EncryptedPrivateKeys::parse(TEST_ED25519_NONE).unwrap();
let decrypted = keys.parse_private(None).unwrap();
let encrypted = decrypted[0]
.encrypt(KeyEncryptionParams::secure_or_none("".to_owned()))
.unwrap();
let bytes = encrypted.to_bytes();
assert_eq!(pem::parse(TEST_ED25519_NONE).unwrap().contents(), bytes);
}
}

View file

@ -116,19 +116,19 @@ impl Writer {
}
pub fn u8(&mut self, v: u8) {
self.write(&[v]);
self.raw(&[v]);
}
pub fn u32(&mut self, v: u32) {
self.write(&u32::to_be_bytes(v));
self.raw(&u32::to_be_bytes(v));
}
pub fn write(&mut self, v: &[u8]) {
pub fn raw(&mut self, v: &[u8]) {
self.0.extend_from_slice(v);
}
pub fn array<const N: usize>(&mut self, arr: [u8; N]) {
self.write(&arr);
self.raw(&arr);
}
pub fn name_list(&mut self, list: NameList<'_>) {
@ -146,12 +146,12 @@ impl Writer {
if pad_zero {
self.u8(0);
}
self.write(bytes);
self.raw(bytes);
}
pub fn string(&mut self, data: &[u8]) {
self.u32(data.len() as u32);
self.write(data);
self.raw(data);
}
pub fn bool(&mut self, v: bool) {