Merge pull request #214 from str4d/recipient-refactor
Recipient refactor
This commit is contained in:
@@ -8,6 +8,9 @@ to 0.3.0 are beta releases.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- MSRV is now 1.70.0.
|
||||||
|
|
||||||
## [0.5.0] - 2024-08-04
|
## [0.5.0] - 2024-08-04
|
||||||
### Fixed
|
### Fixed
|
||||||
- `age-plugin-yubikey` can now be compiled with Rust 1.80 and above.
|
- `age-plugin-yubikey` can now be compiled with Rust 1.80 and above.
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ keywords = ["age", "cli", "encryption", "yubikey"]
|
|||||||
categories = ["command-line-utilities", "cryptography"]
|
categories = ["command-line-utilities", "cryptography"]
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.67" # MSRV
|
rust-version = "1.70" # MSRV
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
extended-description = """\
|
extended-description = """\
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ which enables files to be encrypted to age identities stored on YubiKeys.
|
|||||||
|
|
||||||
| Environment | CLI command |
|
| Environment | CLI command |
|
||||||
|-------------|-------------|
|
|-------------|-------------|
|
||||||
| Cargo (Rust 1.67+) | `cargo install age-plugin-yubikey` |
|
| Cargo (Rust 1.70+) | `cargo install age-plugin-yubikey` |
|
||||||
| Homebrew (macOS or Linux) | `brew install age-plugin-yubikey` |
|
| Homebrew (macOS or Linux) | `brew install age-plugin-yubikey` |
|
||||||
| Arch Linux | `pacman -S age-plugin-yubikey` |
|
| Arch Linux | `pacman -S age-plugin-yubikey` |
|
||||||
| Debian | [Debian package](https://github.com/str4d/age-plugin-yubikey/releases) |
|
| Debian | [Debian package](https://github.com/str4d/age-plugin-yubikey/releases) |
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.67.0"
|
channel = "1.70.0"
|
||||||
components = ["clippy", "rustfmt"]
|
components = ["clippy", "rustfmt"]
|
||||||
|
|||||||
+5
-3
@@ -11,9 +11,9 @@ use crate::{
|
|||||||
error::Error,
|
error::Error,
|
||||||
fl,
|
fl,
|
||||||
key::{self, Stub},
|
key::{self, Stub},
|
||||||
p256::Recipient,
|
piv_p256,
|
||||||
util::{Metadata, POLICY_EXTENSION_OID},
|
util::{Metadata, POLICY_EXTENSION_OID},
|
||||||
BINARY_NAME, USABLE_SLOTS,
|
Recipient, BINARY_NAME, USABLE_SLOTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once;
|
pub(crate) const DEFAULT_PIN_POLICY: PinPolicy = PinPolicy::Once;
|
||||||
@@ -104,7 +104,9 @@ impl IdentityBuilder {
|
|||||||
touch_policy,
|
touch_policy,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let recipient = Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey");
|
let recipient = Recipient::PivP256(
|
||||||
|
piv_p256::Recipient::from_spki(&generated).expect("YubiKey generates a valid pubkey"),
|
||||||
|
);
|
||||||
let stub = Stub::new(yubikey.serial(), slot, &recipient);
|
let stub = Stub::new(yubikey.serial(), slot, &recipient);
|
||||||
|
|
||||||
eprintln!();
|
eprintln!();
|
||||||
|
|||||||
+15
-40
@@ -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;
|
||||||
@@ -24,11 +20,10 @@ use yubikey::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error::Error,
|
error::Error,
|
||||||
fl,
|
fl, piv_p256,
|
||||||
format::{RecipientLine, STANZA_KEY_LABEL},
|
recipient::TAG_BYTES,
|
||||||
p256::{Recipient, TAG_BYTES},
|
|
||||||
util::{otp_serial_prefix, Metadata},
|
util::{otp_serial_prefix, Metadata},
|
||||||
IDENTITY_PREFIX,
|
Recipient, IDENTITY_PREFIX,
|
||||||
};
|
};
|
||||||
|
|
||||||
const ONE_SECOND: Duration = Duration::from_secs(1);
|
const ONE_SECOND: Duration = Duration::from_secs(1);
|
||||||
@@ -398,7 +393,8 @@ pub(crate) fn list_slots(
|
|||||||
match key.slot() {
|
match key.slot() {
|
||||||
SlotId::Retired(slot) => {
|
SlotId::Retired(slot) => {
|
||||||
// Only P-256 keys are compatible with us.
|
// Only P-256 keys are compatible with us.
|
||||||
let recipient = Recipient::from_certificate(key.certificate());
|
let recipient = piv_p256::Recipient::from_certificate(key.certificate())
|
||||||
|
.map(Recipient::PivP256);
|
||||||
Some((key, slot, recipient))
|
Some((key, slot, recipient))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -453,7 +449,7 @@ impl Stub {
|
|||||||
Stub {
|
Stub {
|
||||||
serial,
|
serial,
|
||||||
slot,
|
slot,
|
||||||
tag: recipient.tag(),
|
tag: recipient.static_tag(),
|
||||||
identity_index: 0,
|
identity_index: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -480,10 +476,6 @@ impl Stub {
|
|||||||
bytes
|
bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn matches(&self, line: &RecipientLine) -> bool {
|
|
||||||
self.tag == line.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns:
|
/// Returns:
|
||||||
/// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey.
|
/// - `Ok(Ok(Some(connection)))` if we successfully connected to this YubiKey.
|
||||||
/// - `Ok(Ok(None))` if the user told us to skip this YubiKey.
|
/// - `Ok(Ok(None))` if the user told us to skip this YubiKey.
|
||||||
@@ -605,9 +597,9 @@ impl Stub {
|
|||||||
let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot))
|
let (cert, pk) = match Certificate::read(&mut yubikey, SlotId::Retired(self.slot))
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|cert| {
|
.and_then(|cert| {
|
||||||
Recipient::from_certificate(&cert)
|
piv_p256::Recipient::from_certificate(&cert)
|
||||||
.filter(|pk| pk.tag() == self.tag)
|
.filter(|pk| pk.tag() == self.tag)
|
||||||
.map(|pk| (cert, pk))
|
.map(|pk| (cert, Recipient::PivP256(pk)))
|
||||||
}) {
|
}) {
|
||||||
Some(pk) => pk,
|
Some(pk) => pk,
|
||||||
None => {
|
None => {
|
||||||
@@ -623,7 +615,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 +627,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 +695,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 +710,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 +729,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.
|
||||||
|
|||||||
+7
-7
@@ -16,19 +16,19 @@ use yubikey::{piv::RetiredSlotId, reader::Context, PinPolicy, Serial, TouchPolic
|
|||||||
|
|
||||||
mod builder;
|
mod builder;
|
||||||
mod error;
|
mod error;
|
||||||
mod format;
|
|
||||||
mod key;
|
mod key;
|
||||||
mod p256;
|
mod piv_p256;
|
||||||
mod plugin;
|
mod plugin;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
mod recipient;
|
||||||
|
use recipient::Recipient;
|
||||||
|
|
||||||
use error::Error;
|
use error::Error;
|
||||||
|
|
||||||
const PLUGIN_NAME: &str = "yubikey";
|
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 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,
|
||||||
@@ -194,7 +194,7 @@ fn generate(flags: PluginFlags) -> Result<(), Error> {
|
|||||||
fn print_single(
|
fn print_single(
|
||||||
serial: Option<Serial>,
|
serial: Option<Serial>,
|
||||||
slot: RetiredSlotId,
|
slot: RetiredSlotId,
|
||||||
printer: impl Fn(key::Stub, p256::Recipient, util::Metadata),
|
printer: impl Fn(key::Stub, Recipient, util::Metadata),
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut yubikey = key::open(serial)?;
|
let mut yubikey = key::open(serial)?;
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ fn print_multiple(
|
|||||||
kind: &str,
|
kind: &str,
|
||||||
serial: Option<Serial>,
|
serial: Option<Serial>,
|
||||||
all: bool,
|
all: bool,
|
||||||
printer: impl Fn(key::Stub, p256::Recipient, util::Metadata),
|
printer: impl Fn(key::Stub, Recipient, util::Metadata),
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let mut readers = Context::open()?;
|
let mut readers = Context::open()?;
|
||||||
|
|
||||||
@@ -256,7 +256,7 @@ fn print_details(
|
|||||||
kind: &str,
|
kind: &str,
|
||||||
flags: PluginFlags,
|
flags: PluginFlags,
|
||||||
all: bool,
|
all: bool,
|
||||||
printer: impl Fn(key::Stub, p256::Recipient, util::Metadata),
|
printer: impl Fn(key::Stub, Recipient, util::Metadata),
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
if let Some(slot) = flags.slot {
|
if let Some(slot) = flags.slot {
|
||||||
print_single(flags.serial, slot, printer)
|
print_single(flags.serial, slot, printer)
|
||||||
|
|||||||
@@ -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,11 +11,14 @@ 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, recipient::TAG_BYTES, util::base64_arg};
|
||||||
|
|
||||||
|
mod recipient;
|
||||||
|
pub(crate) use recipient::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 EPK_BYTES: usize = 33;
|
const EPK_BYTES: usize = 33;
|
||||||
const ENCRYPTED_FILE_KEY_BYTES: usize = 32;
|
const ENCRYPTED_FILE_KEY_BYTES: usize = 32;
|
||||||
|
|
||||||
@@ -80,17 +83,6 @@ impl RecipientLine {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base64_arg<A: AsRef<[u8]>, B: AsMut<[u8]>>(arg: &A, mut buf: B) -> Option<B> {
|
|
||||||
if arg.as_ref().len() != ((4 * buf.as_mut().len()) + 2) / 3 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
BASE64_STANDARD_NO_PAD
|
|
||||||
.decode_slice_unchecked(arg, buf.as_mut())
|
|
||||||
.ok()
|
|
||||||
.and_then(|len| (len == buf.as_mut().len()).then_some(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
let (tag, epk_bytes) = match &s.args[..] {
|
let (tag, epk_bytes) = match &s.args[..] {
|
||||||
[tag, epk_bytes] => (
|
[tag, epk_bytes] => (
|
||||||
base64_arg(tag, [0; TAG_BYTES]),
|
base64_arg(tag, [0; TAG_BYTES]),
|
||||||
@@ -109,17 +101,17 @@ impl RecipientLine {
|
|||||||
_ => Err(()),
|
_ => Err(()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn wrap_file_key(file_key: &FileKey, pk: &Recipient) -> Self {
|
impl Recipient {
|
||||||
|
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> RecipientLine {
|
||||||
let esk = EphemeralSecret::random(&mut OsRng);
|
let esk = EphemeralSecret::random(&mut OsRng);
|
||||||
let epk = esk.public_key();
|
let epk = esk.public_key();
|
||||||
let epk_bytes = EphemeralKeyBytes::from_public_key(&epk);
|
let epk_bytes = EphemeralKeyBytes::from_public_key(&epk);
|
||||||
|
|
||||||
let shared_secret = esk.diffie_hellman(pk.public_key());
|
let shared_secret = esk.diffie_hellman(self.public_key());
|
||||||
|
|
||||||
let mut salt = vec![];
|
let salt = salt(&epk_bytes, self);
|
||||||
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];
|
||||||
@@ -137,9 +129,42 @@ impl RecipientLine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
RecipientLine {
|
RecipientLine {
|
||||||
tag: pk.tag(),
|
tag: self.tag(),
|
||||||
epk_bytes,
|
epk_bytes,
|
||||||
encrypted_file_key,
|
encrypted_file_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl RecipientLine {
|
||||||
|
pub(crate) fn unwrap_file_key(&self, conn: &mut Connection) -> Result<FileKey, ()> {
|
||||||
|
let crate::recipient::Recipient::PivP256(recipient) = conn.recipient();
|
||||||
|
assert_eq!(self.tag, recipient.tag());
|
||||||
|
|
||||||
|
let salt = salt(&self.epk_bytes, recipient);
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
use bech32::{ToBase32, Variant};
|
use bech32::{ToBase32, Variant};
|
||||||
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
|
use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use yubikey::{certificate::PublicKeyInfo, Certificate};
|
use yubikey::{certificate::PublicKeyInfo, Certificate};
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::RECIPIENT_PREFIX;
|
use crate::recipient::{static_tag, TAG_BYTES};
|
||||||
|
|
||||||
pub(crate) const TAG_BYTES: usize = 4;
|
const RECIPIENT_PREFIX: &str = "age1yubikey";
|
||||||
|
|
||||||
/// Wrapper around a compressed secp256r1 curve point.
|
/// Wrapper around a compressed secp256r1 curve point.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -69,8 +68,7 @@ impl Recipient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn tag(&self) -> [u8; TAG_BYTES] {
|
pub(crate) fn tag(&self) -> [u8; TAG_BYTES] {
|
||||||
let tag = Sha256::digest(self.to_encoded().as_bytes());
|
static_tag(self.to_encoded().as_bytes())
|
||||||
(&tag[0..TAG_BYTES]).try_into().expect("length is correct")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exposes the wrapped public key.
|
/// Exposes the wrapped public key.
|
||||||
+32
-14
@@ -7,7 +7,7 @@ use age_plugin::{
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crate::{fl, format, key, p256::Recipient, PLUGIN_NAME};
|
use crate::{fl, key, piv_p256, Recipient, PLUGIN_NAME};
|
||||||
|
|
||||||
pub(crate) struct Handler;
|
pub(crate) struct Handler;
|
||||||
|
|
||||||
@@ -37,11 +37,7 @@ impl RecipientPluginV1 for RecipientPlugin {
|
|||||||
plugin_name: &str,
|
plugin_name: &str,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
) -> Result<(), recipient::Error> {
|
) -> Result<(), recipient::Error> {
|
||||||
if let Some(pk) = if plugin_name == PLUGIN_NAME {
|
if let Some(pk) = Recipient::from_bytes(plugin_name, bytes) {
|
||||||
Recipient::from_bytes(bytes)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
} {
|
|
||||||
self.recipients.push(pk);
|
self.recipients.push(pk);
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
@@ -114,7 +110,7 @@ impl RecipientPluginV1 for RecipientPlugin {
|
|||||||
self.recipients
|
self.recipients
|
||||||
.iter()
|
.iter()
|
||||||
.chain(yk_recipients.iter())
|
.chain(yk_recipients.iter())
|
||||||
.map(|pk| format::RecipientLine::wrap_file_key(&file_key, pk).into())
|
.map(|pk| pk.wrap_file_key(&file_key))
|
||||||
.collect()
|
.collect()
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
@@ -159,16 +155,16 @@ impl IdentityPluginV1 for IdentityPlugin {
|
|||||||
let mut file_keys = HashMap::with_capacity(files.len());
|
let mut file_keys = HashMap::with_capacity(files.len());
|
||||||
|
|
||||||
// Filter to files / stanzas for which we have matching YubiKeys
|
// Filter to files / stanzas for which we have matching YubiKeys
|
||||||
let mut candidate_stanzas: Vec<(&key::Stub, HashMap<usize, Vec<format::RecipientLine>>)> =
|
let mut candidate_stanzas: Vec<(&key::Stub, HashMap<usize, Vec<SupportedStanza>>)> = self
|
||||||
self.yubikeys
|
.yubikeys
|
||||||
.iter()
|
.iter()
|
||||||
.map(|stub| (stub, HashMap::new()))
|
.map(|stub| (stub, HashMap::new()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (file, stanzas) in files.iter().enumerate() {
|
for (file, stanzas) in files.into_iter().enumerate() {
|
||||||
for (stanza_index, stanza) in stanzas.iter().enumerate() {
|
for (stanza_index, stanza) in stanzas.into_iter().enumerate() {
|
||||||
match (
|
match (
|
||||||
format::RecipientLine::from_stanza(stanza).map(|res| {
|
SupportedStanza::parse(stanza).map(|res| {
|
||||||
res.map_err(|_| identity::Error::Stanza {
|
res.map_err(|_| identity::Error::Stanza {
|
||||||
file_index: file,
|
file_index: file,
|
||||||
stanza_index,
|
stanza_index,
|
||||||
@@ -182,7 +178,7 @@ impl IdentityPluginV1 for IdentityPlugin {
|
|||||||
// A line will match at most one YubiKey.
|
// A line will match at most one YubiKey.
|
||||||
if let Some(files) =
|
if let Some(files) =
|
||||||
candidate_stanzas.iter_mut().find_map(|(stub, files)| {
|
candidate_stanzas.iter_mut().find_map(|(stub, files)| {
|
||||||
if stub.matches(&line) {
|
if line.matches_stub(stub) {
|
||||||
Some(files)
|
Some(files)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -252,7 +248,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));
|
||||||
@@ -274,3 +270,25 @@ impl IdentityPluginV1 for IdentityPlugin {
|
|||||||
Ok(file_keys)
|
Ok(file_keys)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum SupportedStanza {
|
||||||
|
PivP256(piv_p256::RecipientLine),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SupportedStanza {
|
||||||
|
fn parse(stanza: Stanza) -> Option<Result<Self, ()>> {
|
||||||
|
piv_p256::RecipientLine::from_stanza(&stanza).map(|res| res.map(Self::PivP256))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn matches_stub(&self, stub: &key::Stub) -> bool {
|
||||||
|
match self {
|
||||||
|
SupportedStanza::PivP256(line) => stub.tag == line.tag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unwrap_file_key(&self, conn: &mut key::Connection) -> Result<FileKey, ()> {
|
||||||
|
match self {
|
||||||
|
SupportedStanza::PivP256(line) => line.unwrap_file_key(conn),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use age_core::format::{FileKey, Stanza};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::{piv_p256, PLUGIN_NAME};
|
||||||
|
|
||||||
|
pub(crate) const TAG_BYTES: usize = 4;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum Recipient {
|
||||||
|
PivP256(piv_p256::Recipient),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Recipient {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Recipient::PivP256(recipient) => recipient.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Recipient {
|
||||||
|
/// Attempts to parse a supported YubiKey recipient.
|
||||||
|
pub(crate) fn from_bytes(plugin_name: &str, bytes: &[u8]) -> Option<Self> {
|
||||||
|
match plugin_name {
|
||||||
|
PLUGIN_NAME => piv_p256::Recipient::from_bytes(bytes).map(Self::PivP256),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the static tag for this recipient.
|
||||||
|
pub(crate) fn static_tag(&self) -> [u8; TAG_BYTES] {
|
||||||
|
match self {
|
||||||
|
Recipient::PivP256(recipient) => recipient.tag(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn wrap_file_key(&self, file_key: &FileKey) -> Stanza {
|
||||||
|
match self {
|
||||||
|
Recipient::PivP256(recipient) => recipient.wrap_file_key(file_key).into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn static_tag(pk: &[u8]) -> [u8; TAG_BYTES] {
|
||||||
|
Sha256::digest(pk)[0..TAG_BYTES]
|
||||||
|
.try_into()
|
||||||
|
.expect("length is correct")
|
||||||
|
}
|
||||||
+13
-1
@@ -1,6 +1,7 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
|
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||||
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
|
use x509_parser::{certificate::X509Certificate, der_parser::oid::Oid};
|
||||||
use yubikey::{
|
use yubikey::{
|
||||||
piv::{RetiredSlotId, SlotId},
|
piv::{RetiredSlotId, SlotId},
|
||||||
@@ -8,7 +9,7 @@ use yubikey::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::fl;
|
use crate::fl;
|
||||||
use crate::{error::Error, key::Stub, p256::Recipient, BINARY_NAME, USABLE_SLOTS};
|
use crate::{error::Error, key::Stub, Recipient, BINARY_NAME, USABLE_SLOTS};
|
||||||
|
|
||||||
pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
|
pub(crate) const POLICY_EXTENSION_OID: &[u64] = &[1, 3, 6, 1, 4, 1, 41482, 3, 8];
|
||||||
|
|
||||||
@@ -219,3 +220,14 @@ pub(crate) fn print_identity(stub: Stub, recipient: Recipient, metadata: Metadat
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn base64_arg<A: AsRef<[u8]>, B: AsMut<[u8]>>(arg: &A, mut buf: B) -> Option<B> {
|
||||||
|
if arg.as_ref().len() != ((4 * buf.as_mut().len()) + 2) / 3 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
BASE64_STANDARD_NO_PAD
|
||||||
|
.decode_slice_unchecked(arg, buf.as_mut())
|
||||||
|
.ok()
|
||||||
|
.and_then(|len| (len == buf.as_mut().len()).then_some(buf))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user