use aes::Aes256Ctr;
use bcrypt_pbkdf::bcrypt_pbkdf;
use cipher::{NewCipher, StreamCipher};
use digest::generic_array::GenericArray;
use dryoc::classic::crypto_sign_ed25519::{crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519};
use dryoc::constants::CRYPTO_SIGN_PUBLICKEYBYTES;
use dryoc::dryocbox;
use dryoc::sign;
use num_enum::TryFromPrimitive;
use std::fs::File;
use std::fmt;
use std::io::Read;
use crate::bottle_error::{BottleError, BottleResult};
use crate::header::Header;
const EXPECTED_SSH_SK_START: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
const EXPECTED_SSH_SK_END: &str = "-----END OPENSSH PRIVATE KEY-----";
fn get_ssh_field(data: &[u8]) -> BottleResult<(&[u8], &[u8])> {
let (len, data) = get_ssh_u32(data)?;
if data.len() < len as usize { return Err(BottleError::SshEncodingError); }
Ok(data.split_at(len as usize))
}
fn get_ssh_u32(data: &[u8]) -> BottleResult<(u32, &[u8])> {
if data.len() < 4 { return Err(BottleError::SshEncodingError); }
let rv = u32::from_be_bytes(data[0..4].try_into().unwrap());
Ok((rv, &data[4..]))
}
fn skip_ssh_fields(data: &[u8], count: usize) -> BottleResult<&[u8]> {
let mut rv = data;
for _ in 0..count {
let (_, data) = get_ssh_field(data)?;
rv = data;
}
Ok(rv)
}
const FIELD_RECIPIENT_NAME_STRING: u8 = 0;
const FIELD_PUBLIC_KEY_BYTES: u8 = 0;
const FIELD_ENCRYPTED_KEY_BYTES: u8 = 1;
pub trait BottlePublicKey {
fn algorithm(&self) -> PublicKeyAlgorithm;
fn name(&self) -> &str;
fn as_bytes(&self) -> &[u8];
fn encrypt(&self, data: &[u8]) -> BottleResult<Vec<u8>>;
fn box_clone(&self) -> Box<dyn BottlePublicKey>;
fn to_header(&self, key: &[u8]) -> BottleResult<Header> {
let mut h = Header::new();
h.add_string(FIELD_RECIPIENT_NAME_STRING, self.name())?;
h.add_bytes(FIELD_PUBLIC_KEY_BYTES, self.as_bytes())?;
h.add_bytes(FIELD_ENCRYPTED_KEY_BYTES, &self.encrypt(key)?)?;
Ok(h)
}
}
impl fmt::Debug for dyn BottlePublicKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Ed25519PublicKey({}, {})", self.name(), hex::encode(self.as_bytes()))
}
}
pub trait BottleSecretKey {
fn algorithm(&self) -> PublicKeyAlgorithm;
fn name(&self) -> &str;
fn matches_public_key(&self, public_key: &dyn BottlePublicKey) -> bool;
fn decrypt(&self, data: &[u8]) -> BottleResult<Vec<u8>>;
}
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, Debug, PartialEq, TryFromPrimitive)]
#[repr(u8)]
pub enum PublicKeyAlgorithm {
ED25519_NACL_SEALED = 0,
}
fn public_key_from_bytes(
algorithm: PublicKeyAlgorithm,
data: &[u8],
name: String
) -> Option<Box<dyn BottlePublicKey>> {
match algorithm {
PublicKeyAlgorithm::ED25519_NACL_SEALED => Some(Box::new(Ed25519PublicKey::from_bytes(data, name))),
}
}
#[derive(Debug)]
pub struct EncryptedKeyHeader {
pub public_key: Box<dyn BottlePublicKey>,
pub encrypted_key: Vec<u8>,
pub header: Header,
}
pub(crate) fn public_key_from_header(
algorithm: PublicKeyAlgorithm,
header: Header
) -> Option<EncryptedKeyHeader> {
let encrypted_key = header.get_bytes(FIELD_ENCRYPTED_KEY_BYTES).unwrap_or(&[]).to_vec();
let key_bytes = header.get_bytes(FIELD_PUBLIC_KEY_BYTES).unwrap_or(&[]);
let name = header.get_string(FIELD_RECIPIENT_NAME_STRING).unwrap_or("");
if encrypted_key.is_empty() || key_bytes.is_empty() || name.is_empty() { return None; }
public_key_from_bytes(algorithm, key_bytes, String::from(name)).map(|public_key| {
EncryptedKeyHeader { public_key, encrypted_key, header }
})
}
impl Clone for EncryptedKeyHeader {
fn clone(&self) -> Self {
EncryptedKeyHeader {
public_key: self.public_key.box_clone(),
encrypted_key: self.encrypted_key.clone(),
header: self.header.clone(),
}
}
}
#[derive(Clone)]
pub struct Ed25519PublicKey {
pk: dryocbox::PublicKey,
name: String,
}
impl Ed25519PublicKey {
pub fn from_bytes(data: &[u8], name: String) -> Ed25519PublicKey {
let mut pk = dryocbox::PublicKey::new();
pk.copy_from_slice(data);
Ed25519PublicKey { pk, name }
}
pub fn from_ssh_bytes(data: &[u8], name: String) -> BottleResult<Ed25519PublicKey> {
if data.len() != CRYPTO_SIGN_PUBLICKEYBYTES { return Err(BottleError::NotAnEd25519Key); }
let vpk = sign::PublicKey::from(&data.try_into().unwrap());
let mut pk = dryocbox::PublicKey::new();
crypto_sign_ed25519_pk_to_curve25519(pk.as_mut(), vpk.as_ref()).map_err(|_| BottleError::NotAnEd25519Key)?;
Ok(Ed25519PublicKey { pk, name })
}
pub fn from_ssh_contents(filename: &str, file_contents: String) -> BottleResult<Ed25519PublicKey> {
let first_line = file_contents.split('\n').next().ok_or_else(|| {
BottleError::InvalidSshFile(filename.to_string())
})?;
let segments: Vec<&str> = first_line.split(' ').collect();
if segments.len() < 3 || segments[0] != "ssh-ed25519" {
return Err(BottleError::InvalidSshFile(filename.to_string()));
}
let name = segments[2];
let data = base64::decode(segments[1]).map_err(|_| BottleError::InvalidSshFile(filename.to_string()))?;
if data.len() < 8 { return Err(BottleError::InvalidSshFile(filename.to_string())); }
let (alg_name, data) = get_ssh_field(&data)?;
if alg_name != b"ssh-ed25519" { return Err(BottleError::InvalidSshFile(filename.to_string())); }
let (key_data, _) = get_ssh_field(data)?;
if key_data.len() != CRYPTO_SIGN_PUBLICKEYBYTES {
return Err(BottleError::InvalidSshFile(filename.to_string()));
}
Ed25519PublicKey::from_ssh_bytes(key_data, String::from(name))
}
pub fn from_ssh_file(filename: &str) -> BottleResult<Ed25519PublicKey> {
let mut contents = String::new();
let mut file = File::open(filename).map_err(|e| BottleError::FileError(filename.to_string(), e.kind()))?;
file.read_to_string(&mut contents).map_err(|e| BottleError::FileError(filename.to_string(), e.kind()))?;
Ed25519PublicKey::from_ssh_contents(filename, contents)
}
}
impl BottlePublicKey for Ed25519PublicKey {
fn algorithm(&self) -> PublicKeyAlgorithm {
PublicKeyAlgorithm::ED25519_NACL_SEALED
}
fn name(&self) -> &str {
&self.name
}
fn as_bytes(&self) -> &[u8] {
self.pk.as_ref()
}
fn encrypt(&self, data: &[u8]) -> BottleResult<Vec<u8>> {
let sealed = dryocbox::DryocBox::seal_to_vecbox(data, &self.pk).map_err(|_| BottleError::DryocBoxError)?;
Ok(sealed.to_vec())
}
fn box_clone(&self) -> Box<dyn BottlePublicKey> {
Box::new(self.clone())
}
}
pub struct Ed25519SecretKey {
key_pair: dryocbox::KeyPair,
pub name: String,
}
impl Ed25519SecretKey {
pub fn from_ssh_bytes(data: &[u8], name: String) -> BottleResult<Ed25519SecretKey> {
let key_pair: sign::SigningKeyPair<sign::PublicKey, sign::SecretKey> = sign::SigningKeyPair::from_seed(&data);
let mut sk = dryocbox::SecretKey::new();
crypto_sign_ed25519_sk_to_curve25519(sk.as_mut(), key_pair.secret_key.as_ref());
let mut pk = dryocbox::PublicKey::new();
crypto_sign_ed25519_pk_to_curve25519(pk.as_mut(), key_pair.public_key.as_ref()).map_err(|_| {
BottleError::NotAnEd25519Key
})?;
let key_pair = dryocbox::KeyPair::from_slices(&pk, &sk).map_err(|_| BottleError::NotAnEd25519Key)?;
Ok(Ed25519SecretKey { key_pair, name })
}
pub fn from_ssh_contents(
filename: &str,
file_contents: String,
password: Option<&str>,
) -> BottleResult<Ed25519SecretKey> {
let mut lines: Vec<&str> = file_contents.split('\n').collect();
while !lines.is_empty() && lines[0] != EXPECTED_SSH_SK_START {
lines.remove(0);
}
lines.remove(0);
while !lines.is_empty() && lines.last() != Some(&EXPECTED_SSH_SK_END) {
lines.pop();
}
lines.pop();
let data = base64::decode(lines.join("")).map_err(|_| BottleError::InvalidSshFile(filename.to_string()))?;
let i = data.iter().position(|b| *b == 0).unwrap_or(0);
if &data[0..i] != b"openssh-key-v1" { return Err(BottleError::InvalidSshFile(filename.to_string())); }
let (cipher_name, data) = get_ssh_field(&data[i + 1 ..])?;
let (kdf_name, data) = get_ssh_field(data)?;
let (kdf_options, data) = get_ssh_field(data)?;
let is_encrypted = match (&kdf_name, &cipher_name) {
(&b"none", &b"none") => false,
(&b"bcrypt", &b"aes256-ctr") => true,
_ => {
let kind = format!(
"{}/{}",
String::from_utf8(kdf_name.to_vec()).unwrap_or_else(|_| "?".to_string()),
String::from_utf8(cipher_name.to_vec()).unwrap_or_else(|_| "?".to_string()),
);
return Err(BottleError::UnsupportedSshFileEncryption { filename: filename.to_string(), kind });
}
};
let (public_key_count, data) = get_ssh_u32(data)?;
let data = skip_ssh_fields(data, public_key_count as usize)?;
let (data, _) = get_ssh_field(data)?;
let mut data = data.to_owned();
if is_encrypted {
if password.is_none() {
return Err(BottleError::SshPasswordRequired);
}
let (salt, kdf_options) = get_ssh_field(kdf_options)?;
let (rounds, _) = get_ssh_u32(kdf_options)?;
let mut generated = [0u8; 48];
bcrypt_pbkdf(password.unwrap(), salt, rounds, &mut generated).unwrap();
let (key, iv) = generated.split_at(32);
let mut cipher = Aes256Ctr::new(GenericArray::from_slice(key), GenericArray::from_slice(iv));
cipher.apply_keystream(&mut data);
}
let (alg_name, data) = get_ssh_field(&data[8..])?;
if alg_name != b"ssh-ed25519" { return Err(BottleError::InvalidSshFile(filename.to_string())); }
let (_public_key, data) = get_ssh_field(data)?;
let (secret_key_data, data) = get_ssh_field(data)?;
let (secret_key, _public_key) = secret_key_data.split_at(CRYPTO_SIGN_PUBLICKEYBYTES);
let (name, _data) = get_ssh_field(data)?;
let name = String::from_utf8(name.to_vec()).map_err(|_| BottleError::InvalidSshFile(filename.to_string()))?;
Ed25519SecretKey::from_ssh_bytes(secret_key, name)
}
pub fn from_ssh_file(filename: &str, password: Option<&str>) -> BottleResult<Ed25519SecretKey> {
let mut contents = String::new();
let mut file = File::open(filename).map_err(|e| BottleError::FileError(filename.to_string(), e.kind()))?;
file.read_to_string(&mut contents).map_err(|e| BottleError::FileError(filename.to_string(), e.kind()))?;
Ed25519SecretKey::from_ssh_contents(filename, contents, password)
}
pub fn as_bytes(&self) -> &[u8] {
self.key_pair.secret_key.as_ref()
}
}
impl fmt::Debug for Ed25519SecretKey {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Ed25519SecretKey({}, {})", self.name, hex::encode(&self.key_pair.secret_key))
}
}
impl BottleSecretKey for Ed25519SecretKey {
fn algorithm(&self) -> PublicKeyAlgorithm {
PublicKeyAlgorithm::ED25519_NACL_SEALED
}
fn name(&self) -> &str {
&self.name
}
fn matches_public_key(&self, public_key: &dyn BottlePublicKey) -> bool {
public_key.as_bytes() == &self.key_pair.public_key[..]
}
fn decrypt(&self, data: &[u8]) -> BottleResult<Vec<u8>> {
let sealed = dryocbox::DryocBox::from_sealed_bytes(data).map_err(|_| BottleError::DryocBoxError)?;
sealed.unseal_to_vec(&self.key_pair).map_err(|_| BottleError::DryocBoxError)
}
}
#[cfg(test)]
mod test {
use bcrypt_pbkdf::bcrypt_pbkdf;
use hex;
use std::fs;
use super::{BottlePublicKey, BottleSecretKey, Ed25519PublicKey, Ed25519SecretKey};
const PK_HEX: &str = "112905df7c79c726b6250ec6e87aaa35d9127e2f48736a65c7352e8768d77865";
const SK_HEX: &str = "bc29877fa28b3e03ea6b58d35818e65a1fb4870dae35c4a452de880205108efb";
const PK_NACL_HEX: &str = "34fd22aae3c59072fd6f48147309eb302ea30f6ae5fc6376f683df3e74485a7c";
const SK_NACL_HEX: &str = "c026f16ec964ffc5314fc295d983828cf00f65a53cb9aee1375e7bee2c547e61";
const MESSAGE: &str = "One person's rando is another person's homey.";
#[test]
fn from_ssh_bytes() {
let pk = Ed25519PublicKey::from_ssh_bytes(&hex::decode(PK_HEX).unwrap(), String::new()).unwrap();
assert_eq!(hex::encode(pk.as_bytes()), PK_NACL_HEX);
let sk = Ed25519SecretKey::from_ssh_bytes(&hex::decode(SK_HEX).unwrap(), String::new()).unwrap();
assert_eq!(hex::encode(sk.as_bytes()), SK_NACL_HEX);
}
#[test]
fn encrypt() {
let pk = Ed25519PublicKey::from_ssh_bytes(&hex::decode(PK_HEX).unwrap(), String::new()).unwrap();
let sk = Ed25519SecretKey::from_ssh_bytes(&hex::decode(SK_HEX).unwrap(), String::new()).unwrap();
let encrypted = pk.encrypt(MESSAGE.as_bytes()).unwrap();
let decrypted = sk.decrypt(&encrypted).unwrap();
assert_eq!(String::from_utf8(decrypted).unwrap(), MESSAGE);
}
#[test]
fn read_ssh_pk() {
let data = fs::read_to_string("./tests/data/test-key.pub").unwrap();
let pk = Ed25519PublicKey::from_ssh_contents("test-key.pub", data).unwrap();
assert_eq!(hex::encode(pk.as_bytes()), PK_NACL_HEX);
assert_eq!(
format!("{:?}", &pk as &dyn BottlePublicKey),
"Ed25519PublicKey(robey@togusa, 34fd22aae3c59072fd6f48147309eb302ea30f6ae5fc6376f683df3e74485a7c)"
);
}
#[test]
fn read_ssh_sk() {
let data = fs::read_to_string("./tests/data/test-key").unwrap();
let sk = Ed25519SecretKey::from_ssh_contents("test-key", data, None).unwrap();
assert_eq!(hex::encode(sk.as_bytes()), SK_NACL_HEX);
assert_eq!(
format!("{:?}", &sk),
"Ed25519SecretKey(robey@togusa, c026f16ec964ffc5314fc295d983828cf00f65a53cb9aee1375e7bee2c547e61)"
);
}
#[test]
fn read_ssh_sk_encrypted() {
let mut generated = vec![0u8; 48];
let salt = hex::decode("240d8b1cd1367fd79758b9569b991d8f").unwrap();
bcrypt_pbkdf("password", &salt, 16, &mut generated).unwrap();
assert_eq!(
hex::encode(&generated), "016129df9f55f9a962d0f24bd09c2c9df2081e47663b1835090cfbab32a42b4b2651e759166186e66cd422cd381b5e58"
);
let data = fs::read_to_string("./tests/data/test-key-pw").unwrap();
let bad_sk = Ed25519SecretKey::from_ssh_contents("test-key-pw", data.clone(), None);
assert!(bad_sk.is_err());
assert_eq!(format!("{:?}", bad_sk.unwrap_err()), "SshPasswordRequired");
let sk = Ed25519SecretKey::from_ssh_contents("test-key-pw", data, Some("password")).unwrap();
assert_eq!(
format!("{:?}", &sk),
"Ed25519SecretKey(robey@togusa, e8e78aa6f852348a9ba107beba008979011efce06115376bd7dc72c086acdf67)"
);
}
#[test]
fn encrypt_from_ssh() {
let pk_data = fs::read_to_string("./tests/data/test-key.pub").unwrap();
let pk = Ed25519PublicKey::from_ssh_contents("test-key.pub", pk_data).unwrap();
let sk_data = fs::read_to_string("./tests/data/test-key").unwrap();
let sk = Ed25519SecretKey::from_ssh_contents("test-key", sk_data, None).unwrap();
let encrypted = pk.encrypt(MESSAGE.as_bytes()).unwrap();
let decrypted = sk.decrypt(&encrypted).unwrap();
assert_eq!(String::from_utf8(decrypted).unwrap(), MESSAGE);
}
}