From 712c025b40275014e204b60d3ee39c4762cadf86 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 29 Oct 2019 15:34:34 +0000 Subject: [PATCH] Switch from byte-oriented x25519 function to typed DH API This introduces clear-on-drop semantics for X25519 secret keys. As a side effect, it also causes these keys to be written in clamped form (as x25519_dalek::StaticSecret stores the keys in clamped form internally). Unclamped X25519 secret keys will still be read, but reading and then writing the same key is no longer guaranteed to result in the same encoding (and in any case, this is unnecessary for age use cases). --- Cargo.lock | 1 + Cargo.toml | 1 + src/format.rs | 15 ++++----- src/keys.rs | 87 ++++++++++++++++++++++++++------------------------- 4 files changed, 55 insertions(+), 49 deletions(-) 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-----