Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ jobs:
matrix:
alice: [rage, age]
bob: [rage, age]
recipient: [x25519, tag, tagpq, ssh-rsa, ssh-ed25519, plugin]
recipient: [pq, x25519, tag, tagpq, ssh-rsa, ssh-ed25519, plugin]
include:
- identity: true
- recipient: tag
Expand Down Expand Up @@ -139,6 +139,12 @@ jobs:
# Prepare the test environment
- name: Install dos2unix for simulating Windows files
run: sudo apt update && sudo apt install dos2unix
- name: Set up the age pq identity and recipient
if: matrix.recipient == 'pq'
run: |
echo "AGE-SECRET-KEY-PQ-1XX76JRALNLXDMEW0CRK45QMCCH4X06SE84UN3VPM33W6HWDX0H3SK3ZQFR" >key.txt
echo "age1pq1x34nzsvr0rxjsgdn8zgyhfe8j7ceq5r9rdelkjuh3y235jzxshfg87pzf5zrqtzdxz95paef6caq5aapdmwjjqpjfdyxnzr2zampc3uxy0dg4z2n2gm9su72p0pc3u0jvev55l694v78snxg3yzvcl7yda0eyytqj6a0ec477lnhcy5hzpz4zq3pxanve4cn62gqj3pjy5lqj9c6kyj4v2z8alktn8zh99970x79gjkv7522hv9kfz35zsnxhsx8wwtmu9cy3ftzjgwcp4sshn3llnylnpdsyz5jm72vefv4x5vfwytrefxg4wq3mv42wcrvkj742479zrxzpvp2p3e9fed9f0739vcu80r7ma28qfhnvlv4gfzel9q654dj3zmuvvz893azhxdvs9fxd0r7jzchzcfcs5mkyyjxhw0n2z6dvp9yn9qfdp29h0azxqyjw6v7fhyuzj7zel0uq6j9rd7wgrpz7mf5dnj43jwsgvrc8qcnhy7tu6dkdujuxzkp9xj43xe8h92ktre2a3u3s8mm5mrp9nr9pwkgtz4mdlq9hgn4fps4k57ff6wddn2fy23t47sm20r8km8sd2pcyyafnet8f0dajsrlyjeah4n3mssr6aseevuuskdvq5lzguyvpgwpta742c6698vgutzqgny8usfg0w2he7kq5vyxjd0f9hqg8xk26y9e4th0gezq92q4cpp5p2y9hf5f2cje5l0c3sa3a2qxmm38pxxvhxh99yzmfz0zk7r2s64nnwjhkfgfr3gf8xnmppcgmaykvh5sh6g7vk9790rf8ws0axmr2t7z8aae5fq2029uvcn2ghgt4fu4wgwdc0k0cz52qkvwmuzj8p8k5jgf3xzk5zmrkavjekjrpeq408xz3zxazwkc6tyfmhayrkfpjhwtz5mp8j8guqe43k2q6m2kte03vrw27y3wmqyu5etmt9dnkwcnnpmu9gz9dekfhdevf42ucshphnrk38ra6hx8w5f8q5ru0xdhrjxmwqf6cused7zc5xvq43r0zscjglpwlptpwydhqw64xz7ptjdyeyzpq2zkxtmzg29gzjpvzva4d3l0cenn9xs297wf4y4ukwrunf57xj6pm7nvrkwvtrt8hwcmgv8x7ajw7258ugf9wvkmk4052ekg87tw5vnx8nq2swyzv77v8yqlwsenvamr0zssknwts8rrhfuwj7ykysnq9jxy0uv3kuyt22djszjdtvpz6d0s0kwh8ryynddzud92emeyvvyqktd0jtj7rvvg5gch25v8smlvny3kvn5gagyz475ze2y6q466xqmz2n3hs77lddeqyta2nch5k2u5yacuk9ywnwfdzvyejnucz724hj77hrrmakm7pr3kxsrxq22ejexlud9fy2kdqmkg5yncz7jm5wv2qjk5w5kvcpqsry2yqffh2la52dxfjkjq5rzhjzeyn6dupn0qwtyv7s4lwg3xdarsdlwe2y3tujy480y7z39q259fzx6jhd2j0f5hagqpcpees7hzc2yrk5cy788uk3s7qvp5cpepx24gvws3m2g433exgwppnkjscec8qu4y9z9r7vccexjcjaen42245lmgmxmuavg9alej92322gvvyy2t6267v09ch64y0m53jff0vjj96s0ypk60hr3jw4myd6m5hpn3xjstx7tl2szhpr5qe8jj08ydjc4wy2rch2fhuy3pdfjax5awe9j99ly5hkntzz9fe5zatgjvzdd0kgtxs25njnajyf6ssekp7gelxquusn4pt25czh3scj68kq79wdn5tgm6yvm9nzavrg043x3msnygf8dweknw5jmqd0uvny6ttsn09508k0c55zfnegrm9efhxpfqdkmhh6gjtqmwze9pyyzk3tlhl53k2ykx3qheyty7saeq0d3fzv49zc0k" >key.txt.pub
echo "AGE_PUBKEY=-r age1pq1x34nzsvr0rxjsgdn8zgyhfe8j7ceq5r9rdelkjuh3y235jzxshfg87pzf5zrqtzdxz95paef6caq5aapdmwjjqpjfdyxnzr2zampc3uxy0dg4z2n2gm9su72p0pc3u0jvev55l694v78snxg3yzvcl7yda0eyytqj6a0ec477lnhcy5hzpz4zq3pxanve4cn62gqj3pjy5lqj9c6kyj4v2z8alktn8zh99970x79gjkv7522hv9kfz35zsnxhsx8wwtmu9cy3ftzjgwcp4sshn3llnylnpdsyz5jm72vefv4x5vfwytrefxg4wq3mv42wcrvkj742479zrxzpvp2p3e9fed9f0739vcu80r7ma28qfhnvlv4gfzel9q654dj3zmuvvz893azhxdvs9fxd0r7jzchzcfcs5mkyyjxhw0n2z6dvp9yn9qfdp29h0azxqyjw6v7fhyuzj7zel0uq6j9rd7wgrpz7mf5dnj43jwsgvrc8qcnhy7tu6dkdujuxzkp9xj43xe8h92ktre2a3u3s8mm5mrp9nr9pwkgtz4mdlq9hgn4fps4k57ff6wddn2fy23t47sm20r8km8sd2pcyyafnet8f0dajsrlyjeah4n3mssr6aseevuuskdvq5lzguyvpgwpta742c6698vgutzqgny8usfg0w2he7kq5vyxjd0f9hqg8xk26y9e4th0gezq92q4cpp5p2y9hf5f2cje5l0c3sa3a2qxmm38pxxvhxh99yzmfz0zk7r2s64nnwjhkfgfr3gf8xnmppcgmaykvh5sh6g7vk9790rf8ws0axmr2t7z8aae5fq2029uvcn2ghgt4fu4wgwdc0k0cz52qkvwmuzj8p8k5jgf3xzk5zmrkavjekjrpeq408xz3zxazwkc6tyfmhayrkfpjhwtz5mp8j8guqe43k2q6m2kte03vrw27y3wmqyu5etmt9dnkwcnnpmu9gz9dekfhdevf42ucshphnrk38ra6hx8w5f8q5ru0xdhrjxmwqf6cused7zc5xvq43r0zscjglpwlptpwydhqw64xz7ptjdyeyzpq2zkxtmzg29gzjpvzva4d3l0cenn9xs297wf4y4ukwrunf57xj6pm7nvrkwvtrt8hwcmgv8x7ajw7258ugf9wvkmk4052ekg87tw5vnx8nq2swyzv77v8yqlwsenvamr0zssknwts8rrhfuwj7ykysnq9jxy0uv3kuyt22djszjdtvpz6d0s0kwh8ryynddzud92emeyvvyqktd0jtj7rvvg5gch25v8smlvny3kvn5gagyz475ze2y6q466xqmz2n3hs77lddeqyta2nch5k2u5yacuk9ywnwfdzvyejnucz724hj77hrrmakm7pr3kxsrxq22ejexlud9fy2kdqmkg5yncz7jm5wv2qjk5w5kvcpqsry2yqffh2la52dxfjkjq5rzhjzeyn6dupn0qwtyv7s4lwg3xdarsdlwe2y3tujy480y7z39q259fzx6jhd2j0f5hagqpcpees7hzc2yrk5cy788uk3s7qvp5cpepx24gvws3m2g433exgwppnkjscec8qu4y9z9r7vccexjcjaen42245lmgmxmuavg9alej92322gvvyy2t6267v09ch64y0m53jff0vjj96s0ypk60hr3jw4myd6m5hpn3xjstx7tl2szhpr5qe8jj08ydjc4wy2rch2fhuy3pdfjax5awe9j99ly5hkntzz9fe5zatgjvzdd0kgtxs25njnajyf6ssekp7gelxquusn4pt25czh3scj68kq79wdn5tgm6yvm9nzavrg043x3msnygf8dweknw5jmqd0uvny6ttsn09508k0c55zfnegrm9efhxpfqdkmhh6gjtqmwze9pyyzk3tlhl53k2ykx3qheyty7saeq0d3fzv49zc0k" >> $GITHUB_ENV
- name: Set up the X25519 identity and recipient
if: matrix.recipient == 'x25519'
run: |
Expand Down Expand Up @@ -279,7 +285,7 @@ jobs:
run: ./${{ matrix.alice }}-keygen | ./${{ matrix.bob }}-keygen -y

- name: Keygen supports conversion from file
if: matrix.recipient == 'x25519'
if: matrix.recipient == 'pq' || matrix.recipient == 'x25519'
run: ./${{ matrix.alice }}-keygen -y key.txt

- name: Update FiloSottile/age status with result
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ to 1.0.0 are beta releases.

## [Unreleased]

### Added
- Support for new native age recipient types:
- `age::pq`

### Changed
- MSRV is now 1.85.0.
- Migrated to `chacha20poly1305 0.11`, `curve25519-dalek 5`, `hmac 0.13`,
Expand Down
2 changes: 1 addition & 1 deletion age/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ bech32.workspace = true
cipher = { version = "0.5", features = ["alloc"] }
cookie-factory.workspace = true
hkdf.workspace = true
hpke = { workspace = true, features = ["mlkem", "nistp"] }
hpke = { workspace = true, features = ["mlkem", "nistp", "x25519"] }
i18n-embed-fl.workspace = true
lazy_static.workspace = true
ml-kem.workspace = true
Expand Down
6 changes: 5 additions & 1 deletion age/src/cli_common/recipients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::io::{self, BufReader};

use super::StdinGuard;
use super::{ReadError, identities::parse_identity_files};
use crate::{Recipient, identity::RecipientsAccumulator, tag, tagpq, util::LimitedReader, x25519};
use crate::{
Recipient, identity::RecipientsAccumulator, pq, tag, tagpq, util::LimitedReader, x25519,
};

#[cfg(feature = "plugin")]
use crate::{cli_common::UiCallbacks, plugin};
Expand Down Expand Up @@ -58,6 +60,8 @@ fn parse_recipient(
) -> Result<(), ReadError> {
if let Ok(pk) = s.parse::<x25519::Recipient>() {
recipients.push(Box::new(pk));
} else if let Ok(pk) = s.parse::<pq::Recipient>() {
recipients.push(Box::new(pk));
} else if let Ok(pk) = s.parse::<tag::Recipient>() {
recipients.push(Box::new(pk));
} else if let Ok(pk) = s.parse::<tagpq::Recipient>() {
Expand Down
105 changes: 55 additions & 50 deletions age/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
use zeroize::Zeroize;

use crate::{
Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks,
Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks, pq,
util::LimitedReader, x25519,
};

Expand All @@ -21,8 +21,10 @@ const IDENTITY_SIZE_LIMIT: usize = 1 << 24; // 16 MiB
/// The supported kinds of identities within an [`IdentityFile`].
#[derive(Clone)]
enum IdentityFileEntry {
/// The standard age identity type.
Native(x25519::Identity),
/// The classic age identity type.
Classic(x25519::Identity),
/// The "pq" age identity type.
Pq(pq::Identity),
/// A plugin-compatible identity.
#[cfg(feature = "plugin")]
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
Expand All @@ -36,7 +38,8 @@ impl IdentityFileEntry {
callbacks: impl Callbacks,
) -> Result<Box<dyn crate::Identity + Send + Sync>, DecryptError> {
match self {
IdentityFileEntry::Native(i) => Ok(Box::new(i)),
IdentityFileEntry::Classic(i) => Ok(Box::new(i)),
IdentityFileEntry::Pq(i) => Ok(Box::new(i)),
#[cfg(feature = "plugin")]
IdentityFileEntry::Plugin(i) => Ok(Box::new(
crate::plugin::Plugin::new(i.plugin())
Expand Down Expand Up @@ -89,50 +92,47 @@ impl IdentityFile<NoCallbacks> {
continue;
}

match line.parse::<x25519::Identity>() {
Ok(identity) => {
identities.push(IdentityFileEntry::Native(identity));
if let Ok(identity) = line.parse::<x25519::Identity>() {
identities.push(IdentityFileEntry::Classic(identity));
} else if let Ok(identity) = line.parse::<pq::Identity>() {
identities.push(IdentityFileEntry::Pq(identity));
} else if let Some(identity) = {
#[cfg(feature = "plugin")]
{
line.parse::<plugin::Identity>().ok()
}
_ => {
if let Some(identity) = {
#[cfg(feature = "plugin")]
{
line.parse::<plugin::Identity>().ok()
}

#[cfg(not(feature = "plugin"))]
None
} {
#[cfg(feature = "plugin")]
{
identities.push(IdentityFileEntry::Plugin(identity));
}

// Add a binding to provide a type when plugins are disabled.
#[cfg(not(feature = "plugin"))]
let _: () = identity;
} else {
line.zeroize();

// Return a line number in place of the line, so we don't leak the file
// contents in error messages.
return Err(io::Error::new(
io::ErrorKind::InvalidData,
if let Some(filename) = filename {
format!(
"identity file {} contains non-identity data on line {}",
filename,
line_number + 1
)
} else {
format!(
"identity file contains non-identity data on line {}",
line_number + 1
)
},
));
}

#[cfg(not(feature = "plugin"))]
None
} {
#[cfg(feature = "plugin")]
{
identities.push(IdentityFileEntry::Plugin(identity));
}

// Add a binding to provide a type when plugins are disabled.
#[cfg(not(feature = "plugin"))]
let _: () = identity;
} else {
line.zeroize();

// Return a line number in place of the line, so we don't leak the file
// contents in error messages.
return Err(io::Error::new(
io::ErrorKind::InvalidData,
if let Some(filename) = filename {
format!(
"identity file {} contains non-identity data on line {}",
filename,
line_number + 1
)
} else {
format!(
"identity file contains non-identity data on line {}",
line_number + 1
)
},
));
}

line.zeroize();
Expand Down Expand Up @@ -174,7 +174,9 @@ impl<C: Callbacks> IdentityFile<C> {

for identity in &self.identities {
match identity {
IdentityFileEntry::Native(sk) => writeln!(output, "{}", sk.to_public())
IdentityFileEntry::Classic(sk) => writeln!(output, "{}", sk.to_public())
.map_err(IdentityFileConvertError::FailedToWriteOutput)?,
IdentityFileEntry::Pq(i) => writeln!(output, "{}", i.to_public())
.map_err(IdentityFileConvertError::FailedToWriteOutput)?,
#[cfg(feature = "plugin")]
IdentityFileEntry::Plugin(id) => {
Expand Down Expand Up @@ -265,7 +267,8 @@ impl RecipientsAccumulator {
pub(crate) fn with_identities<C: Callbacks>(&mut self, identity_file: IdentityFile<C>) {
for entry in identity_file.identities {
match entry {
IdentityFileEntry::Native(i) => self.recipients.push(Box::new(i.to_public())),
IdentityFileEntry::Classic(i) => self.recipients.push(Box::new(i.to_public())),
IdentityFileEntry::Pq(i) => self.recipients.push(Box::new(i.to_public())),
#[cfg(feature = "plugin")]
IdentityFileEntry::Plugin(i) => self.plugin_identities.push(i),
}
Expand All @@ -275,7 +278,8 @@ impl RecipientsAccumulator {
pub(crate) fn with_identities_ref<C: Callbacks>(&mut self, identity_file: &IdentityFile<C>) {
for entry in &identity_file.identities {
match entry {
IdentityFileEntry::Native(i) => self.recipients.push(Box::new(i.to_public())),
IdentityFileEntry::Classic(i) => self.recipients.push(Box::new(i.to_public())),
IdentityFileEntry::Pq(i) => self.recipients.push(Box::new(i.to_public())),
#[cfg(feature = "plugin")]
IdentityFileEntry::Plugin(i) => self.plugin_identities.push(i.clone()),
}
Expand Down Expand Up @@ -332,9 +336,10 @@ pub(crate) mod tests {
let f = IdentityFile::from_buffer(buf).unwrap();
assert_eq!(f.identities.len(), num_keys);
match &f.identities[0] {
IdentityFileEntry::Native(identity) => {
IdentityFileEntry::Classic(identity) => {
assert_eq!(identity.to_string().expose_secret(), TEST_SK)
}
IdentityFileEntry::Pq(_) => panic!(),
#[cfg(feature = "plugin")]
IdentityFileEntry::Plugin(_) => panic!(),
}
Expand Down
5 changes: 3 additions & 2 deletions age/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
//! - For most cases (including programmatic usage):
//! - Use [`Encryptor::with_recipients`] with a compatible set of recipients:
//! - Classic: [`x25519::Recipient`] and/or [`tag::Recipient`].
//! - Post-quantum: [`tagpq::Recipient`].
//! - Use [`Decryptor`] with [`x25519::Identity`].
//! - Post-quantum: [`pq::Recipient`] and/or [`tagpq::Recipient`].
//! - Use [`Decryptor`] with [`pq::Recipient`] and/or [`x25519::Identity`].
//! - For passphrase-based encryption and decryption, use [`scrypt::Recipient`] and
//! [`scrypt::Identity`], or the helper method [`Encryptor::with_user_passphrase`].
//! These should only be used with passphrases that were provided by (or generated for)
Expand Down Expand Up @@ -253,6 +253,7 @@ pub use simple::encrypt_and_armor;
//

mod native;
pub use native::pq;
pub use native::scrypt;
pub use native::tag;
pub use native::tagpq;
Expand Down
1 change: 1 addition & 0 deletions age/src/native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::HashSet;
use hkdf::Hkdf;
use sha2::{Digest, Sha256};

pub mod pq;
pub mod scrypt;
pub mod tag;
pub mod tagpq;
Expand Down
Loading
Loading