diff --git a/src/format.rs b/src/format.rs index fb9ac4c..19f2284 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,7 +1,7 @@ use age_core::{ - format::{FileKey, Stanza}, - primitives::aead_encrypt, - secrecy::ExposeSecret, + format::{FileKey, Stanza, FILE_KEY_BYTES}, + primitives::{aead_decrypt, aead_encrypt, hkdf}, + secrecy::{zeroize::Zeroize, ExposeSecret}, }; use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; use p256::{ @@ -11,8 +11,9 @@ use p256::{ use rand::rngs::OsRng; use sha2::Sha256; -use crate::{p256::Recipient, STANZA_TAG}; +use crate::{key::Connection, p256::Recipient}; +const STANZA_TAG: &str = "piv-p256"; pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256"; const TAG_BYTES: usize = 4; @@ -117,9 +118,7 @@ impl RecipientLine { let shared_secret = esk.diffie_hellman(pk.public_key()); - let mut salt = vec![]; - salt.extend_from_slice(epk_bytes.as_bytes()); - salt.extend_from_slice(pk.to_encoded().as_bytes()); + let salt = salt(&epk_bytes, pk); let enc_key = { let mut okm = [0; 32]; @@ -142,4 +141,34 @@ impl RecipientLine { encrypted_file_key, } } + + pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result { + assert_eq!(self.tag, conn.recipient().tag()); + + // The YubiKey API for performing scalar multiplication takes the point in its + // uncompressed SEC-1 encoding. + let shared_secret = conn.p256_ecdh(self.epk_bytes.decompress().as_bytes())?; + + let salt = salt(&self.epk_bytes, conn.recipient()); + + let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_ref()); + + // A failure to decrypt is fatal, because we assume that we won't + // encounter 32-bit collisions on the key tag embedded in the header. + aead_decrypt(&enc_key, FILE_KEY_BYTES, &self.encrypted_file_key) + .map_err(|_| ()) + .map(|mut pt| { + FileKey::init_with_mut(|file_key| { + file_key.copy_from_slice(&pt); + pt.zeroize(); + }) + }) + } +} + +fn salt(epk_bytes: &EphemeralKeyBytes, pk: &Recipient) -> Vec { + let mut salt = vec![]; + salt.extend_from_slice(epk_bytes.as_bytes()); + salt.extend_from_slice(pk.to_encoded().as_bytes()); + salt } diff --git a/src/key.rs b/src/key.rs index 9dc501e..3a09dcf 100644 --- a/src/key.rs +++ b/src/key.rs @@ -1,10 +1,6 @@ //! Structs for handling YubiKeys. -use age_core::{ - format::{FileKey, FILE_KEY_BYTES}, - primitives::{aead_decrypt, hkdf}, - secrecy::{zeroize::Zeroize, ExposeSecret, SecretString}, -}; +use age_core::secrecy::{ExposeSecret, SecretString}; use age_plugin::{identity, Callbacks}; use bech32::{ToBase32, Variant}; use dialoguer::Password; @@ -25,7 +21,7 @@ use yubikey::{ use crate::{ error::Error, fl, - format::{RecipientLine, STANZA_KEY_LABEL}, + format::RecipientLine, p256::{Recipient, TAG_BYTES}, util::{otp_serial_prefix, Metadata}, IDENTITY_PREFIX, @@ -623,7 +619,6 @@ impl Stub { cert, pk, slot: self.slot, - tag: self.tag, identity_index: self.identity_index, cached_metadata: None, last_touch: None, @@ -636,7 +631,6 @@ pub(crate) struct Connection { cert: Certificate, pk: Recipient, slot: RetiredSlotId, - tag: [u8; 4], identity_index: usize, cached_metadata: Option, last_touch: Option, @@ -705,8 +699,10 @@ impl Connection { Ok(Ok(())) } - pub(crate) fn unwrap_file_key(&mut self, line: &RecipientLine) -> Result { - assert_eq!(self.tag, line.tag); + pub(crate) fn p256_ecdh(&mut self, epk_bytes: &[u8]) -> Result { + // The YubiKey API for performing scalar multiplication takes the point in its + // uncompressed SEC-1 encoding. + assert_eq!(epk_bytes.len(), 65); // Check if the touch policy requires a touch. let needs_touch = match ( @@ -718,11 +714,9 @@ impl Connection { _ => false, }; - // The YubiKey API for performing scalar multiplication takes the point in its - // uncompressed SEC-1 encoding. let shared_secret = match decrypt_data( &mut self.yubikey, - line.epk_bytes.decompress().as_bytes(), + epk_bytes, AlgorithmId::EccP256, SlotId::Retired(self.slot), ) { @@ -739,22 +733,7 @@ impl Connection { } } - let mut salt = vec![]; - salt.extend_from_slice(line.epk_bytes.as_bytes()); - salt.extend_from_slice(self.pk.to_encoded().as_bytes()); - - let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_ref()); - - // A failure to decrypt is fatal, because we assume that we won't - // encounter 32-bit collisions on the key tag embedded in the header. - aead_decrypt(&enc_key, FILE_KEY_BYTES, &line.encrypted_file_key) - .map_err(|_| ()) - .map(|mut pt| { - FileKey::init_with_mut(|file_key| { - file_key.copy_from_slice(&pt); - pt.zeroize(); - }) - }) + Ok(shared_secret) } /// Close this connection without resetting the YubiKey. diff --git a/src/main.rs b/src/main.rs index aa00930..adf4de4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,6 @@ const PLUGIN_NAME: &str = "yubikey"; const BINARY_NAME: &str = "age-plugin-yubikey"; const RECIPIENT_PREFIX: &str = "age1yubikey"; const IDENTITY_PREFIX: &str = "age-plugin-yubikey-"; -const STANZA_TAG: &str = "piv-p256"; const USABLE_SLOTS: [RetiredSlotId; 20] = [ RetiredSlotId::R1, diff --git a/src/plugin.rs b/src/plugin.rs index 30d7ca0..263e2a6 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -252,7 +252,7 @@ impl IdentityPluginV1 for IdentityPlugin { } for (stanza_index, line) in stanzas.iter().enumerate() { - match conn.unwrap_file_key(line) { + match line.unwrap_file_key(&mut conn) { Ok(file_key) => { // We've managed to decrypt this file! file_keys.entry(file_index).or_insert(Ok(file_key));