Refactor piv-p256-specific stanza unwrapping onto RecipientLine

This commit is contained in:
Jack Grigg
2025-12-08 01:48:39 +00:00
parent f3f99a0cbc
commit 144d3088b6
4 changed files with 45 additions and 38 deletions
+36 -7
View File
@@ -1,7 +1,7 @@
use age_core::{ use age_core::{
format::{FileKey, Stanza}, format::{FileKey, Stanza, FILE_KEY_BYTES},
primitives::aead_encrypt, primitives::{aead_decrypt, aead_encrypt, hkdf},
secrecy::ExposeSecret, secrecy::{zeroize::Zeroize, ExposeSecret},
}; };
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine}; use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
use p256::{ use p256::{
@@ -11,8 +11,9 @@ use p256::{
use rand::rngs::OsRng; use rand::rngs::OsRng;
use sha2::Sha256; 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"; pub(crate) const STANZA_KEY_LABEL: &[u8] = b"piv-p256";
const TAG_BYTES: usize = 4; const TAG_BYTES: usize = 4;
@@ -117,9 +118,7 @@ impl RecipientLine {
let shared_secret = esk.diffie_hellman(pk.public_key()); let shared_secret = esk.diffie_hellman(pk.public_key());
let mut salt = vec![]; let salt = salt(&epk_bytes, pk);
salt.extend_from_slice(epk_bytes.as_bytes());
salt.extend_from_slice(pk.to_encoded().as_bytes());
let enc_key = { let enc_key = {
let mut okm = [0; 32]; let mut okm = [0; 32];
@@ -142,4 +141,34 @@ impl RecipientLine {
encrypted_file_key, encrypted_file_key,
} }
} }
pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result<FileKey, ()> {
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<u8> {
let mut salt = vec![];
salt.extend_from_slice(epk_bytes.as_bytes());
salt.extend_from_slice(pk.to_encoded().as_bytes());
salt
} }
+8 -29
View File
@@ -1,10 +1,6 @@
//! Structs for handling YubiKeys. //! Structs for handling YubiKeys.
use age_core::{ use age_core::secrecy::{ExposeSecret, SecretString};
format::{FileKey, FILE_KEY_BYTES},
primitives::{aead_decrypt, hkdf},
secrecy::{zeroize::Zeroize, ExposeSecret, SecretString},
};
use age_plugin::{identity, Callbacks}; use age_plugin::{identity, Callbacks};
use bech32::{ToBase32, Variant}; use bech32::{ToBase32, Variant};
use dialoguer::Password; use dialoguer::Password;
@@ -25,7 +21,7 @@ use yubikey::{
use crate::{ use crate::{
error::Error, error::Error,
fl, fl,
format::{RecipientLine, STANZA_KEY_LABEL}, format::RecipientLine,
p256::{Recipient, TAG_BYTES}, p256::{Recipient, TAG_BYTES},
util::{otp_serial_prefix, Metadata}, util::{otp_serial_prefix, Metadata},
IDENTITY_PREFIX, IDENTITY_PREFIX,
@@ -623,7 +619,6 @@ impl Stub {
cert, cert,
pk, pk,
slot: self.slot, slot: self.slot,
tag: self.tag,
identity_index: self.identity_index, identity_index: self.identity_index,
cached_metadata: None, cached_metadata: None,
last_touch: None, last_touch: None,
@@ -636,7 +631,6 @@ pub(crate) struct Connection {
cert: Certificate, cert: Certificate,
pk: Recipient, pk: Recipient,
slot: RetiredSlotId, slot: RetiredSlotId,
tag: [u8; 4],
identity_index: usize, identity_index: usize,
cached_metadata: Option<Metadata>, cached_metadata: Option<Metadata>,
last_touch: Option<Instant>, last_touch: Option<Instant>,
@@ -705,8 +699,10 @@ impl Connection {
Ok(Ok(())) Ok(Ok(()))
} }
pub(crate) fn unwrap_file_key(&mut self, line: &RecipientLine) -> Result<FileKey, ()> { pub(crate) fn p256_ecdh(&mut self, epk_bytes: &[u8]) -> Result<yubikey::Buffer, ()> {
assert_eq!(self.tag, line.tag); // 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. // Check if the touch policy requires a touch.
let needs_touch = match ( let needs_touch = match (
@@ -718,11 +714,9 @@ impl Connection {
_ => false, _ => false,
}; };
// The YubiKey API for performing scalar multiplication takes the point in its
// uncompressed SEC-1 encoding.
let shared_secret = match decrypt_data( let shared_secret = match decrypt_data(
&mut self.yubikey, &mut self.yubikey,
line.epk_bytes.decompress().as_bytes(), epk_bytes,
AlgorithmId::EccP256, AlgorithmId::EccP256,
SlotId::Retired(self.slot), SlotId::Retired(self.slot),
) { ) {
@@ -739,22 +733,7 @@ impl Connection {
} }
} }
let mut salt = vec![]; Ok(shared_secret)
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();
})
})
} }
/// Close this connection without resetting the YubiKey. /// Close this connection without resetting the YubiKey.
-1
View File
@@ -28,7 +28,6 @@ const PLUGIN_NAME: &str = "yubikey";
const BINARY_NAME: &str = "age-plugin-yubikey"; const BINARY_NAME: &str = "age-plugin-yubikey";
const RECIPIENT_PREFIX: &str = "age1yubikey"; const RECIPIENT_PREFIX: &str = "age1yubikey";
const IDENTITY_PREFIX: &str = "age-plugin-yubikey-"; const IDENTITY_PREFIX: &str = "age-plugin-yubikey-";
const STANZA_TAG: &str = "piv-p256";
const USABLE_SLOTS: [RetiredSlotId; 20] = [ const USABLE_SLOTS: [RetiredSlotId; 20] = [
RetiredSlotId::R1, RetiredSlotId::R1,
+1 -1
View File
@@ -252,7 +252,7 @@ impl IdentityPluginV1 for IdentityPlugin {
} }
for (stanza_index, line) in stanzas.iter().enumerate() { for (stanza_index, line) in stanzas.iter().enumerate() {
match conn.unwrap_file_key(line) { match line.unwrap_file_key(&mut conn) {
Ok(file_key) => { Ok(file_key) => {
// We've managed to decrypt this file! // We've managed to decrypt this file!
file_keys.entry(file_index).or_insert(Ok(file_key)); file_keys.entry(file_index).or_insert(Ok(file_key));