diff --git a/Cargo.lock b/Cargo.lock index a88a1c9..09dd875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,6 +40,7 @@ dependencies = [ "nom 5.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "num-bigint-dig 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "rsa 0.1.4-alpha.0 (git+https://github.com/lucdew/RSA?branch=oaep)", "scrypt 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 246f463..c282d81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ hkdf = "0.8" hmac = "0.7" nom = "5" num-bigint-dig = "0.4" +rand_os = "0.1" rand = "0.6" rsa = { git = "https://github.com/lucdew/RSA", branch = "oaep" } scrypt = { version = "0.2", default-features = false } diff --git a/src/format.rs b/src/format.rs index d8a3223..d1edad3 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,6 +1,7 @@ //! The age message format. use std::io::{self, Read, Write}; +use x25519_dalek::PublicKey; use crate::primitives::HmacWriter; @@ -16,7 +17,7 @@ const MAC_TAG: &[u8] = b"---"; #[derive(Debug)] pub(crate) struct X25519RecipientLine { - pub(crate) epk: [u8; 32], + pub(crate) epk: PublicKey, pub(crate) encrypted_file_key: [u8; 32], } @@ -48,7 +49,7 @@ pub(crate) enum RecipientLine { } impl RecipientLine { - pub(crate) fn x25519(epk: [u8; 32], encrypted_file_key: [u8; 32]) -> Self { + pub(crate) fn x25519(epk: PublicKey, encrypted_file_key: [u8; 32]) -> Self { RecipientLine::X25519(X25519RecipientLine { epk, encrypted_file_key, @@ -70,7 +71,7 @@ impl RecipientLine { }) } - pub(crate) fn ssh_ed25519(tag: [u8; 4], epk: [u8; 32], encrypted_file_key: [u8; 32]) -> Self { + pub(crate) fn ssh_ed25519(tag: [u8; 4], epk: PublicKey, encrypted_file_key: [u8; 32]) -> Self { RecipientLine::SshEd25519(SshEd25519RecipientLine { tag, rest: X25519RecipientLine { @@ -187,8 +188,8 @@ mod read { } } - fn x25519_epk(input: &[u8]) -> IResult<&[u8], [u8; 32]> { - encoded_data(32, [0; 32])(input) + fn x25519_epk(input: &[u8]) -> IResult<&[u8], PublicKey> { + map(encoded_data(32, [0; 32]), PublicKey::from)(input) } fn x25519_recipient_line<'a, N>( @@ -401,7 +402,7 @@ mod write { ) -> impl SerializeFn + 'a { tuple(( slice(X25519_RECIPIENT_TAG), - encoded_data(&r.epk), + encoded_data(r.epk.as_bytes()), string(line_ending), encoded_data(&r.encrypted_file_key), )) @@ -461,7 +462,7 @@ mod write { slice(SSH_ED25519_RECIPIENT_TAG), encoded_data(&r.tag), string(" "), - encoded_data(&r.rest.epk), + encoded_data(r.rest.epk.as_bytes()), string(line_ending), encoded_data(&r.rest.encrypted_file_key), )) diff --git a/src/keys.rs b/src/keys.rs index 8d01342..f3055da 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1,10 +1,10 @@ //! Key structs and serialization. use curve25519_dalek::edwards::EdwardsPoint; -use getrandom::getrandom; +use rand_os::OsRng; use sha2::{Digest, Sha256, Sha512}; use std::io::{self, BufRead}; -use x25519_dalek::{x25519, X25519_BASEPOINT_BYTES}; +use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; use crate::{ format::RecipientLine, @@ -28,7 +28,7 @@ fn ssh_tag(pubkey: &[u8]) -> [u8; 4] { /// A secret key for decrypting an age message. pub enum SecretKey { /// An X25519 secret key. - X25519([u8; 32]), + X25519(StaticSecret), /// An ssh-rsa private key. SshRsa(Vec, Box), /// An ssh-ed25519 key pair. @@ -38,9 +38,8 @@ pub enum SecretKey { impl SecretKey { /// Generates a new secret key. pub fn generate() -> Self { - let mut sk = [0; 32]; - getrandom(&mut sk).expect("Should not fail"); - SecretKey::X25519(sk) + let mut rng = OsRng::new().expect("can construct OsRng"); + SecretKey::X25519(StaticSecret::new(&mut rng)) } /// Parses a list of secret keys from a string. @@ -73,7 +72,7 @@ impl SecretKey { SecretKey::X25519(sk) => format!( "{}{}", SECRET_KEY_PREFIX, - base64::encode_config(&sk, base64::URL_SAFE_NO_PAD) + base64::encode_config(&sk.to_bytes(), base64::URL_SAFE_NO_PAD) ), SecretKey::SshRsa(_, _) => unimplemented!(), SecretKey::SshEd25519(_, _) => unimplemented!(), @@ -83,7 +82,7 @@ impl SecretKey { /// Returns the recipient key for this secret key. pub fn to_public(&self) -> RecipientKey { match self { - SecretKey::X25519(sk) => RecipientKey::X25519(x25519(*sk, X25519_BASEPOINT_BYTES)), + SecretKey::X25519(sk) => RecipientKey::X25519(sk.into()), SecretKey::SshRsa(_, _) => unimplemented!(), SecretKey::SshEd25519(_, _) => unimplemented!(), } @@ -92,14 +91,14 @@ impl SecretKey { pub(crate) fn unwrap_file_key(&self, line: &RecipientLine) -> Option<[u8; 16]> { match (self, line) { (SecretKey::X25519(sk), RecipientLine::X25519(r)) => { - let pk = x25519(*sk, X25519_BASEPOINT_BYTES); - let shared_secret = x25519(*sk, r.epk); + let pk: PublicKey = sk.into(); + let shared_secret = sk.diffie_hellman(&r.epk); let mut salt = vec![]; - salt.extend_from_slice(&r.epk); - salt.extend_from_slice(&pk); + salt.extend_from_slice(r.epk.as_bytes()); + salt.extend_from_slice(pk.as_bytes()); - let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, &shared_secret); + let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, shared_secret.as_bytes()); aead_decrypt(&enc_key, &r.encrypted_file_key).map(|pt| { // It's ours! let mut file_key = [0; 16]; @@ -135,23 +134,24 @@ impl SecretKey { return None; } - let sk = { + let sk: StaticSecret = { let mut sk = [0; 32]; // privkey format is seed || pubkey sk.copy_from_slice(&Sha512::digest(&privkey[0..32])[0..32]); - sk + sk.into() }; - let tweak = hkdf(&ssh_key, SSH_ED25519_TWEAK_LABEL, &[]); - let pk = x25519(tweak, x25519(sk, X25519_BASEPOINT_BYTES)); + let tweak: StaticSecret = hkdf(&ssh_key, SSH_ED25519_TWEAK_LABEL, &[]).into(); + let pk = tweak.diffie_hellman(&(&sk).into()); - let shared_secret = x25519(tweak, x25519(sk, r.rest.epk)); + let shared_secret = tweak + .diffie_hellman(&PublicKey::from(*sk.diffie_hellman(&r.rest.epk).as_bytes())); let mut salt = vec![]; - salt.extend_from_slice(&r.rest.epk); - salt.extend_from_slice(&pk); + salt.extend_from_slice(r.rest.epk.as_bytes()); + salt.extend_from_slice(pk.as_bytes()); - let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, &shared_secret); + let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, shared_secret.as_bytes()); aead_decrypt(&enc_key, &r.rest.encrypted_file_key).map(|pt| { // It's ours! let mut file_key = [0; 16]; @@ -168,7 +168,7 @@ impl SecretKey { #[derive(Debug)] pub enum RecipientKey { /// An X25519 recipient key. - X25519([u8; 32]), + X25519(PublicKey), /// An ssh-rsa public key. SshRsa(Vec, rsa::RSAPublicKey), /// An ssh-ed25519 public key. @@ -207,7 +207,7 @@ impl RecipientKey { RecipientKey::X25519(pk) => format!( "{}{}", PUBLIC_KEY_PREFIX, - base64::encode_config(&pk, base64::URL_SAFE_NO_PAD) + base64::encode_config(pk.as_bytes(), base64::URL_SAFE_NO_PAD) ), RecipientKey::SshRsa(_, _) => unimplemented!(), RecipientKey::SshEd25519(_, _) => unimplemented!(), @@ -217,16 +217,16 @@ impl RecipientKey { pub(crate) fn wrap_file_key(&self, file_key: &[u8; 16]) -> RecipientLine { match self { RecipientKey::X25519(pk) => { - let mut esk = [0; 32]; - getrandom(&mut esk).expect("Should not fail"); - let epk = x25519(esk, X25519_BASEPOINT_BYTES); - let shared_secret = x25519(esk, *pk); + let mut rng = OsRng::new().expect("can construct OsRng"); + let esk = EphemeralSecret::new(&mut rng); + let epk: PublicKey = (&esk).into(); + let shared_secret = esk.diffie_hellman(pk); let mut salt = vec![]; - salt.extend_from_slice(&epk); - salt.extend_from_slice(pk); + salt.extend_from_slice(epk.as_bytes()); + salt.extend_from_slice(pk.as_bytes()); - let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, &shared_secret); + let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, shared_secret.as_bytes()); let encrypted_file_key = { let mut key = [0; 32]; key.copy_from_slice(&aead_encrypt(&enc_key, file_key)); @@ -251,19 +251,22 @@ impl RecipientKey { RecipientLine::ssh_rsa(ssh_tag(&ssh_key), encrypted_file_key) } RecipientKey::SshEd25519(ssh_key, ed25519_pk) => { - let tweak = hkdf(&ssh_key, SSH_ED25519_TWEAK_LABEL, &[]); - let pk = x25519(tweak, ed25519_pk.to_montgomery().to_bytes()); + let tweak: StaticSecret = hkdf(&ssh_key, SSH_ED25519_TWEAK_LABEL, &[]).into(); + let pk: PublicKey = (*tweak + .diffie_hellman(&ed25519_pk.to_montgomery().to_bytes().into()) + .as_bytes()) + .into(); - let mut esk = [0; 32]; - getrandom(&mut esk).expect("Should not fail"); - let epk = x25519(esk, X25519_BASEPOINT_BYTES); - let shared_secret = x25519(esk, pk); + let mut rng = OsRng::new().expect("can construct OsRng"); + let esk = EphemeralSecret::new(&mut rng); + let epk: PublicKey = (&esk).into(); + let shared_secret = esk.diffie_hellman(&pk); let mut salt = vec![]; - salt.extend_from_slice(&epk); - salt.extend_from_slice(&pk); + salt.extend_from_slice(epk.as_bytes()); + salt.extend_from_slice(pk.as_bytes()); - let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, &shared_secret); + let enc_key = hkdf(&salt, X25519_RECIPIENT_KEY_LABEL, shared_secret.as_bytes()); let encrypted_file_key = { let mut key = [0; 32]; key.copy_from_slice(&aead_encrypt(&enc_key, file_key)); @@ -295,7 +298,7 @@ mod read { map(read_encoded_str(32, base64::URL_SAFE_NO_PAD), |buf| { let mut pk = [0; 32]; pk.copy_from_slice(&buf); - SecretKey::X25519(pk) + SecretKey::X25519(pk.into()) }), )(input) } @@ -335,7 +338,7 @@ mod read { map(read_encoded_str(32, base64::URL_SAFE_NO_PAD), |buf| { let mut pk = [0; 32]; pk.copy_from_slice(&buf); - RecipientKey::X25519(pk) + RecipientKey::X25519(pk.into()) }), )(input) } @@ -347,7 +350,7 @@ pub(crate) mod tests { use super::{RecipientKey, SecretKey}; - const TEST_SK: &str = "AGE_SECRET_KEY_RQvvHYA29yZk8Lelpiz8lW7QdlxkE4djb1NOjLgeUFg"; + const TEST_SK: &str = "AGE_SECRET_KEY_QAvvHYA29yZk8Lelpiz8lW7QdlxkE4djb1NOjLgeUFg"; const TEST_PK: &str = "pubkey:X4ZiZYoURuOqC2_GPISYiWbJn1-j_HECyac7BpD6kHU"; pub(crate) const TEST_SSH_RSA_SK: &str = "-----BEGIN RSA PRIVATE KEY-----