@@ -25,7 +25,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: 1.56.0
|
toolchain: 1.60.0
|
||||||
override: true
|
override: true
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: sudo apt install ${{ matrix.build_deps }}
|
run: sudo apt install ${{ matrix.build_deps }}
|
||||||
@@ -46,14 +46,14 @@ jobs:
|
|||||||
args: --verbose
|
args: --verbose
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
name: Clippy (1.56.0)
|
name: Clippy (1.60.0)
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: 1.56.0
|
toolchain: 1.60.0
|
||||||
components: clippy
|
components: clippy
|
||||||
override: true
|
override: true
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
uses: actions-rs/clippy-check@v1
|
uses: actions-rs/clippy-check@v1
|
||||||
with:
|
with:
|
||||||
name: Clippy (1.56.0)
|
name: Clippy (1.60.0)
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
args: --all-features --all-targets -- -D warnings
|
args: --all-features --all-targets -- -D warnings
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: 1.56.0
|
toolchain: 1.60.0
|
||||||
override: true
|
override: true
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: sudo apt install libpcsclite-dev
|
run: sudo apt install libpcsclite-dev
|
||||||
@@ -142,7 +142,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: 1.56.0
|
toolchain: 1.60.0
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
override: true
|
override: true
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ and this project adheres to Rust's notion of
|
|||||||
to 0.3.0 are beta releases.
|
to 0.3.0 are beta releases.
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- MSRV is now 1.60.0.
|
||||||
|
|
||||||
## [0.3.2] - 2023-01-01
|
## [0.3.2] - 2023-01-01
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+693
-398
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -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.56" # MSRV
|
rust-version = "1.60" # MSRV
|
||||||
|
|
||||||
[package.metadata.deb]
|
[package.metadata.deb]
|
||||||
extended-description = """\
|
extended-description = """\
|
||||||
@@ -22,24 +22,24 @@ assets = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
age-core = "0.8"
|
age-core = "0.9"
|
||||||
age-plugin = "0.3"
|
age-plugin = "0.4"
|
||||||
base64 = "0.13"
|
base64 = "0.20"
|
||||||
bech32 = "0.8"
|
bech32 = "0.9"
|
||||||
console = { version = "0.15", default-features = false }
|
console = { version = "0.15", default-features = false }
|
||||||
dialoguer = { version = "0.9", default-features = false, features = ["password"] }
|
dialoguer = { version = "0.10", default-features = false, features = ["password"] }
|
||||||
env_logger = "0.9"
|
env_logger = "0.10"
|
||||||
gumdrop = "0.8"
|
gumdrop = "0.8"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
p256 = { version = "0.9", features = ["ecdh"] }
|
p256 = { version = "0.11", features = ["ecdh"] }
|
||||||
pcsc = "2.4"
|
pcsc = "2.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
sha2 = "0.9"
|
sha2 = "0.10"
|
||||||
which = "4.1"
|
which = "4.1"
|
||||||
x509 = "0.2"
|
x509 = "0.2"
|
||||||
x509-parser = "0.12"
|
x509-parser = "0.14"
|
||||||
yubikey = { version = "0.5", features = ["untested"] }
|
yubikey = { version = "0.7", features = ["untested"] }
|
||||||
|
|
||||||
# Translations
|
# Translations
|
||||||
i18n-embed = { version = "0.13", features = ["desktop-requester", "fluent-system"] }
|
i18n-embed = { version = "0.13", features = ["desktop-requester", "fluent-system"] }
|
||||||
@@ -48,7 +48,7 @@ lazy_static = "1"
|
|||||||
rust-embed = "6"
|
rust-embed = "6"
|
||||||
|
|
||||||
# GnuPG coexistence
|
# GnuPG coexistence
|
||||||
sysinfo = ">=0.26, <0.26.4"
|
sysinfo = "0.27"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ which enables files to be encrypted to age identities stored on YubiKeys.
|
|||||||
On Windows, Linux, and macOS, you can use the
|
On Windows, Linux, and macOS, you can use the
|
||||||
[pre-built binaries](https://github.com/str4d/age-plugin-yubikey/releases).
|
[pre-built binaries](https://github.com/str4d/age-plugin-yubikey/releases).
|
||||||
|
|
||||||
If your system has Rust 1.56+ installed (either via `rustup` or a system
|
If your system has Rust 1.60+ installed (either via `rustup` or a system
|
||||||
package), you can build directly from source:
|
package), you can build directly from source:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.56.0
|
1.60.0
|
||||||
|
|||||||
+33
-10
@@ -1,10 +1,14 @@
|
|||||||
use age_core::{
|
use age_core::{
|
||||||
format::{FileKey, Stanza},
|
format::{FileKey, Stanza},
|
||||||
primitives::{aead_encrypt, hkdf},
|
primitives::aead_encrypt,
|
||||||
secrecy::ExposeSecret,
|
secrecy::ExposeSecret,
|
||||||
};
|
};
|
||||||
use p256::{ecdh::EphemeralSecret, elliptic_curve::sec1::ToEncodedPoint};
|
use p256::{
|
||||||
|
ecdh::EphemeralSecret,
|
||||||
|
elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint},
|
||||||
|
};
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
|
use sha2::Sha256;
|
||||||
|
|
||||||
use crate::{p256::Recipient, STANZA_TAG};
|
use crate::{p256::Recipient, STANZA_TAG};
|
||||||
|
|
||||||
@@ -14,6 +18,14 @@ 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;
|
||||||
|
|
||||||
|
const STANDARD_NO_PAD: &base64::engine::fast_portable::FastPortable = {
|
||||||
|
use base64::{
|
||||||
|
alphabet::STANDARD,
|
||||||
|
engine::fast_portable::{FastPortable, NO_PAD},
|
||||||
|
};
|
||||||
|
&FastPortable::from(&STANDARD, NO_PAD)
|
||||||
|
};
|
||||||
|
|
||||||
/// The ephemeral key bytes in a piv-p256 stanza.
|
/// The ephemeral key bytes in a piv-p256 stanza.
|
||||||
///
|
///
|
||||||
/// The bytes contain a compressed SEC-1 encoding of a valid point.
|
/// The bytes contain a compressed SEC-1 encoding of a valid point.
|
||||||
@@ -23,7 +35,11 @@ pub(crate) struct EphemeralKeyBytes(p256::EncodedPoint);
|
|||||||
impl EphemeralKeyBytes {
|
impl EphemeralKeyBytes {
|
||||||
fn from_bytes(bytes: [u8; EPK_BYTES]) -> Option<Self> {
|
fn from_bytes(bytes: [u8; EPK_BYTES]) -> Option<Self> {
|
||||||
let encoded = p256::EncodedPoint::from_bytes(&bytes).ok()?;
|
let encoded = p256::EncodedPoint::from_bytes(&bytes).ok()?;
|
||||||
if encoded.is_compressed() && encoded.decompress().is_some() {
|
if encoded.is_compressed()
|
||||||
|
&& p256::PublicKey::from_encoded_point(&encoded)
|
||||||
|
.is_some()
|
||||||
|
.into()
|
||||||
|
{
|
||||||
Some(EphemeralKeyBytes(encoded))
|
Some(EphemeralKeyBytes(encoded))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -39,9 +55,9 @@ impl EphemeralKeyBytes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn decompress(&self) -> p256::EncodedPoint {
|
pub(crate) fn decompress(&self) -> p256::EncodedPoint {
|
||||||
self.0
|
// EphemeralKeyBytes is a valid compressed encoding by construction.
|
||||||
.decompress()
|
let p = p256::PublicKey::from_encoded_point(&self.0).unwrap();
|
||||||
.expect("EphemeralKeyBytes is a valid compressed encoding by construction")
|
p.to_encoded_point(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +73,8 @@ impl From<RecipientLine> for Stanza {
|
|||||||
Stanza {
|
Stanza {
|
||||||
tag: STANZA_TAG.to_owned(),
|
tag: STANZA_TAG.to_owned(),
|
||||||
args: vec![
|
args: vec![
|
||||||
base64::encode_config(&r.tag, base64::STANDARD_NO_PAD),
|
base64::encode_engine(&r.tag, STANDARD_NO_PAD),
|
||||||
base64::encode_config(r.epk_bytes.as_bytes(), base64::STANDARD_NO_PAD),
|
base64::encode_engine(r.epk_bytes.as_bytes(), STANDARD_NO_PAD),
|
||||||
],
|
],
|
||||||
body: r.encrypted_file_key.to_vec(),
|
body: r.encrypted_file_key.to_vec(),
|
||||||
}
|
}
|
||||||
@@ -76,7 +92,7 @@ impl RecipientLine {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
base64::decode_config_slice(arg, base64::STANDARD_NO_PAD, buf.as_mut())
|
base64::decode_engine_slice(arg, buf.as_mut(), STANDARD_NO_PAD)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|_| buf)
|
.map(|_| buf)
|
||||||
}
|
}
|
||||||
@@ -111,7 +127,14 @@ impl RecipientLine {
|
|||||||
salt.extend_from_slice(epk_bytes.as_bytes());
|
salt.extend_from_slice(epk_bytes.as_bytes());
|
||||||
salt.extend_from_slice(pk.to_encoded().as_bytes());
|
salt.extend_from_slice(pk.to_encoded().as_bytes());
|
||||||
|
|
||||||
let enc_key = hkdf(&salt, STANZA_KEY_LABEL, shared_secret.as_bytes());
|
let enc_key = {
|
||||||
|
let mut okm = [0; 32];
|
||||||
|
shared_secret
|
||||||
|
.extract::<Sha256>(Some(&salt))
|
||||||
|
.expand(STANZA_KEY_LABEL, &mut okm)
|
||||||
|
.expect("okm is the correct length");
|
||||||
|
okm
|
||||||
|
};
|
||||||
|
|
||||||
let encrypted_file_key = {
|
let encrypted_file_key = {
|
||||||
let mut key = [0; ENCRYPTED_FILE_KEY_BYTES];
|
let mut key = [0; ENCRYPTED_FILE_KEY_BYTES];
|
||||||
|
|||||||
@@ -247,6 +247,7 @@ pub(crate) fn manage(yubikey: &mut YubiKey) -> Result<(), Error> {
|
|||||||
yubikey_serial = yubikey.serial().to_string(),
|
yubikey_serial = yubikey.serial().to_string(),
|
||||||
default_pin = DEFAULT_PIN,
|
default_pin = DEFAULT_PIN,
|
||||||
))
|
))
|
||||||
|
.report(true)
|
||||||
.interact()?;
|
.interact()?;
|
||||||
yubikey.verify_pin(pin.as_bytes())?;
|
yubikey.verify_pin(pin.as_bytes())?;
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -286,7 +286,7 @@ fn list(flags: PluginFlags, all: bool) -> Result<(), Error> {
|
|||||||
all,
|
all,
|
||||||
|_, recipient, metadata| {
|
|_, recipient, metadata| {
|
||||||
println!("{}", metadata);
|
println!("{}", metadata);
|
||||||
println!("{}", recipient.to_string());
|
println!("{}", recipient);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -372,6 +372,7 @@ fn main() -> Result<(), Error> {
|
|||||||
.with_prompt(fl!("cli-setup-select-yk"))
|
.with_prompt(fl!("cli-setup-select-yk"))
|
||||||
.items(&reader_names)
|
.items(&reader_names)
|
||||||
.default(0)
|
.default(0)
|
||||||
|
.report(true)
|
||||||
.interact_opt()?
|
.interact_opt()?
|
||||||
{
|
{
|
||||||
Some(yk) => readers_list[yk].open()?,
|
Some(yk) => readers_list[yk].open()?,
|
||||||
@@ -393,7 +394,11 @@ fn main() -> Result<(), Error> {
|
|||||||
x509_parser::parse_x509_certificate(key.certificate().as_ref())
|
x509_parser::parse_x509_certificate(key.certificate().as_ref())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let (name, _) = util::extract_name(&cert, true).unwrap();
|
let (name, _) = util::extract_name(&cert, true).unwrap();
|
||||||
let created = cert.validity().not_before.to_rfc2822();
|
let created = cert
|
||||||
|
.validity()
|
||||||
|
.not_before
|
||||||
|
.to_rfc2822()
|
||||||
|
.unwrap_or_else(|e| format!("Invalid date: {}", e));
|
||||||
|
|
||||||
format!("{}, created: {}", name, created)
|
format!("{}, created: {}", name, created)
|
||||||
})
|
})
|
||||||
@@ -426,6 +431,7 @@ fn main() -> Result<(), Error> {
|
|||||||
.with_prompt(fl!("cli-setup-select-slot"))
|
.with_prompt(fl!("cli-setup-select-slot"))
|
||||||
.items(&slots)
|
.items(&slots)
|
||||||
.default(0)
|
.default(0)
|
||||||
|
.report(true)
|
||||||
.interact_opt()?
|
.interact_opt()?
|
||||||
{
|
{
|
||||||
Some(slot) => {
|
Some(slot) => {
|
||||||
@@ -443,6 +449,7 @@ fn main() -> Result<(), Error> {
|
|||||||
|
|
||||||
if Confirm::new()
|
if Confirm::new()
|
||||||
.with_prompt(fl!("cli-setup-use-existing", slot_index = slot_index))
|
.with_prompt(fl!("cli-setup-use-existing", slot_index = slot_index))
|
||||||
|
.report(true)
|
||||||
.interact()?
|
.interact()?
|
||||||
{
|
{
|
||||||
let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
|
let stub = key::Stub::new(yubikey.serial(), slot, &recipient);
|
||||||
@@ -462,6 +469,7 @@ fn main() -> Result<(), Error> {
|
|||||||
flags.name.as_deref().unwrap_or("age identity TAG_HEX")
|
flags.name.as_deref().unwrap_or("age identity TAG_HEX")
|
||||||
))
|
))
|
||||||
.allow_empty(true)
|
.allow_empty(true)
|
||||||
|
.report(true)
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
|
|
||||||
let pin_policy = match Select::new()
|
let pin_policy = match Select::new()
|
||||||
@@ -479,6 +487,7 @@ fn main() -> Result<(), Error> {
|
|||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
.report(true)
|
||||||
.interact_opt()?
|
.interact_opt()?
|
||||||
{
|
{
|
||||||
Some(0) => PinPolicy::Always,
|
Some(0) => PinPolicy::Always,
|
||||||
@@ -503,6 +512,7 @@ fn main() -> Result<(), Error> {
|
|||||||
})
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
|
.report(true)
|
||||||
.interact_opt()?
|
.interact_opt()?
|
||||||
{
|
{
|
||||||
Some(0) => TouchPolicy::Always,
|
Some(0) => TouchPolicy::Always,
|
||||||
@@ -514,6 +524,7 @@ fn main() -> Result<(), Error> {
|
|||||||
|
|
||||||
if Confirm::new()
|
if Confirm::new()
|
||||||
.with_prompt(fl!("cli-setup-generate-new", slot_index = slot_index))
|
.with_prompt(fl!("cli-setup-generate-new", slot_index = slot_index))
|
||||||
|
.report(true)
|
||||||
.interact()?
|
.interact()?
|
||||||
{
|
{
|
||||||
eprintln!();
|
eprintln!();
|
||||||
@@ -541,6 +552,7 @@ fn main() -> Result<(), Error> {
|
|||||||
"age-yubikey-identity-{}.txt",
|
"age-yubikey-identity-{}.txt",
|
||||||
hex::encode(stub.tag)
|
hex::encode(stub.tag)
|
||||||
))
|
))
|
||||||
|
.report(true)
|
||||||
.interact_text()?;
|
.interact_text()?;
|
||||||
|
|
||||||
let mut file = match OpenOptions::new()
|
let mut file = match OpenOptions::new()
|
||||||
@@ -552,6 +564,7 @@ fn main() -> Result<(), Error> {
|
|||||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
|
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
|
||||||
if Confirm::new()
|
if Confirm::new()
|
||||||
.with_prompt(fl!("cli-setup-identity-file-exists"))
|
.with_prompt(fl!("cli-setup-identity-file-exists"))
|
||||||
|
.report(true)
|
||||||
.interact()?
|
.interact()?
|
||||||
{
|
{
|
||||||
File::create(&file_name)?
|
File::create(&file_name)?
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ impl Recipient {
|
|||||||
/// This accepts both compressed (as used by the plugin) and uncompressed (as used in
|
/// This accepts both compressed (as used by the plugin) and uncompressed (as used in
|
||||||
/// the YubiKey certificate) encodings.
|
/// the YubiKey certificate) encodings.
|
||||||
fn from_encoded(encoded: &p256::EncodedPoint) -> Option<Self> {
|
fn from_encoded(encoded: &p256::EncodedPoint) -> Option<Self> {
|
||||||
p256::PublicKey::from_encoded_point(encoded).map(Recipient)
|
Option::from(p256::PublicKey::from_encoded_point(encoded)).map(Recipient)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the compressed SEC-1 encoding of this recipient.
|
/// Returns the compressed SEC-1 encoding of this recipient.
|
||||||
|
|||||||
+9
-2
@@ -122,7 +122,10 @@ impl Metadata {
|
|||||||
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
// https://developers.yubico.com/PIV/Introduction/PIV_attestation.html
|
||||||
let policies = |c: &X509Certificate| {
|
let policies = |c: &X509Certificate| {
|
||||||
c.tbs_certificate
|
c.tbs_certificate
|
||||||
.find_extension(&Oid::from(POLICY_EXTENSION_OID).unwrap())
|
.get_extension_unique(&Oid::from(POLICY_EXTENSION_OID).unwrap())
|
||||||
|
// If the extension is duplicated, we assume it is invalid.
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
// If the encoded extension doesn't have 2 bytes, we assume it is invalid.
|
// If the encoded extension doesn't have 2 bytes, we assume it is invalid.
|
||||||
.filter(|policy| policy.value.len() >= 2)
|
.filter(|policy| policy.value.len() >= 2)
|
||||||
.map(|policy| {
|
.map(|policy| {
|
||||||
@@ -170,7 +173,11 @@ impl Metadata {
|
|||||||
serial: yubikey.serial(),
|
serial: yubikey.serial(),
|
||||||
slot,
|
slot,
|
||||||
name,
|
name,
|
||||||
created: cert.validity().not_before.to_rfc2822(),
|
created: cert
|
||||||
|
.validity()
|
||||||
|
.not_before
|
||||||
|
.to_rfc2822()
|
||||||
|
.unwrap_or_else(|e| format!("Invalid date: {}", e)),
|
||||||
pin_policy,
|
pin_policy,
|
||||||
touch_policy,
|
touch_policy,
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user