From 8dc052120efd08bf1ee3f1367ce92ea528995438 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 13 Jul 2026 23:10:06 +0000 Subject: [PATCH 1/3] Add `pq` native recipient and identity --- Cargo.lock | 1 + age/CHANGELOG.md | 4 + age/Cargo.toml | 2 +- age/src/cli_common/recipients.rs | 6 +- age/src/identity.rs | 105 +++++++------ age/src/lib.rs | 5 +- age/src/native.rs | 1 + age/src/native/pq.rs | 253 +++++++++++++++++++++++++++++++ age/src/native/x25519.rs | 8 +- age/src/protocol.rs | 25 ++- age/src/simple.rs | 27 +++- rage/build.rs | 3 + rage/i18n/en-US/rage.ftl | 35 +++-- 13 files changed, 408 insertions(+), 67 deletions(-) create mode 100644 age/src/native/pq.rs diff --git a/Cargo.lock b/Cargo.lock index a0ec1011..c329749c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1379,6 +1379,7 @@ dependencies = [ "subtle", "turboshake", "x-wing", + "x25519-dalek", "zeroize", ] diff --git a/age/CHANGELOG.md b/age/CHANGELOG.md index 65d223b2..6f6d499c 100644 --- a/age/CHANGELOG.md +++ b/age/CHANGELOG.md @@ -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`, diff --git a/age/Cargo.toml b/age/Cargo.toml index afef74b4..fc62aef8 100644 --- a/age/Cargo.toml +++ b/age/Cargo.toml @@ -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 diff --git a/age/src/cli_common/recipients.rs b/age/src/cli_common/recipients.rs index bd39ea00..00902259 100644 --- a/age/src/cli_common/recipients.rs +++ b/age/src/cli_common/recipients.rs @@ -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}; @@ -58,6 +60,8 @@ fn parse_recipient( ) -> Result<(), ReadError> { if let Ok(pk) = s.parse::() { recipients.push(Box::new(pk)); + } else if let Ok(pk) = s.parse::() { + recipients.push(Box::new(pk)); } else if let Ok(pk) = s.parse::() { recipients.push(Box::new(pk)); } else if let Ok(pk) = s.parse::() { diff --git a/age/src/identity.rs b/age/src/identity.rs index f3fec4e0..e84c6754 100644 --- a/age/src/identity.rs +++ b/age/src/identity.rs @@ -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, }; @@ -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")))] @@ -36,7 +38,8 @@ impl IdentityFileEntry { callbacks: impl Callbacks, ) -> Result, 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()) @@ -89,50 +92,47 @@ impl IdentityFile { continue; } - match line.parse::() { - Ok(identity) => { - identities.push(IdentityFileEntry::Native(identity)); + if let Ok(identity) = line.parse::() { + identities.push(IdentityFileEntry::Classic(identity)); + } else if let Ok(identity) = line.parse::() { + identities.push(IdentityFileEntry::Pq(identity)); + } else if let Some(identity) = { + #[cfg(feature = "plugin")] + { + line.parse::().ok() } - _ => { - if let Some(identity) = { - #[cfg(feature = "plugin")] - { - line.parse::().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(); @@ -174,7 +174,9 @@ impl IdentityFile { 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) => { @@ -265,7 +267,8 @@ impl RecipientsAccumulator { pub(crate) fn with_identities(&mut self, identity_file: IdentityFile) { 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), } @@ -275,7 +278,8 @@ impl RecipientsAccumulator { pub(crate) fn with_identities_ref(&mut self, identity_file: &IdentityFile) { 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()), } @@ -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!(), } diff --git a/age/src/lib.rs b/age/src/lib.rs index 229df65b..b519e580 100644 --- a/age/src/lib.rs +++ b/age/src/lib.rs @@ -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) @@ -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; diff --git a/age/src/native.rs b/age/src/native.rs index d7af6795..188ec1a3 100644 --- a/age/src/native.rs +++ b/age/src/native.rs @@ -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; diff --git a/age/src/native/pq.rs b/age/src/native/pq.rs new file mode 100644 index 00000000..29c4401e --- /dev/null +++ b/age/src/native/pq.rs @@ -0,0 +1,253 @@ +//! The "pq" recipient type, native to age. + +use std::collections::HashSet; +use std::fmt; + +use age_core::{ + format::{FILE_KEY_BYTES, FileKey, Stanza}, + primitives::{bech32_decode, bech32_encode, bech32_encode_to_fmt, hpke_open, hpke_seal}, + secrecy::{ExposeSecret, SecretString}, +}; +use base64::{Engine, prelude::BASE64_STANDARD_NO_PAD}; +use hpke::{Deserializable, Kem as _, Serializable}; +use rand::{rand_core::UnwrapErr, rngs::SysRng}; +use zeroize::Zeroize; + +use crate::{ + error::{DecryptError, EncryptError}, + util::read::base64_arg, +}; + +const IDENTITY_PREFIX: bech32::Hrp = bech32::Hrp::parse_unchecked("AGE-SECRET-KEY-PQ-"); +const RECIPIENT_PREFIX: bech32::Hrp = bech32::Hrp::parse_unchecked("age1pq"); + +const MLKEM768X25519_RECIPIENT_TAG: &str = "mlkem768x25519"; +const MLKEM768X25519_SALT: &str = "age-encryption.org/mlkem768x25519"; + +const ENCRYPTED_FILE_KEY_BYTES: usize = FILE_KEY_BYTES + 16; + +type Kem = hpke::kem::XWing; + +/// The hybrid post-quantum age identity type, which can decrypt files encrypted to the +/// corresponding [`Recipient`]. +#[derive(Clone)] +pub struct Identity(::PrivateKey); + +impl std::str::FromStr for Identity { + type Err = &'static str; + + /// Parses an X25519 identity from a string. + fn from_str(s: &str) -> Result { + bech32_decode( + s, + |_| "invalid Bech32 encoding", + |hrp| { + (hrp == IDENTITY_PREFIX) + .then_some(()) + .ok_or("incorrect HRP") + }, + |_, bytes| { + if bytes.len() != 32 { + Err("incorrect identity length") + } else { + let mut buf = Box::new([0; 32]); + for (dest, src) in buf.iter_mut().zip(bytes) { + *dest = src; + } + let identity = ::PrivateKey::from_bytes(buf.as_slice()) + .map_err(|_| "invalid identity") + .map(Self); + + // Clear intermediates + buf.zeroize(); + + identity + } + }, + ) + } +} + +impl Identity { + /// Generates a new identity. + pub fn generate() -> Self { + let (identity, _) = ::gen_keypair_with_rng(&mut UnwrapErr(SysRng)); + Self(identity) + } + + /// Serializes this identity as a string. + pub fn to_string(&self) -> SecretString { + let mut sk_bytes = self.0.to_bytes(); + let mut encoded = bech32_encode(IDENTITY_PREFIX, &sk_bytes); + let ret = SecretString::from(encoded.to_uppercase()); + + // Clear intermediates + sk_bytes.zeroize(); + encoded.zeroize(); + + ret + } + + /// Returns the recipient key for this secret key. + pub fn to_public(&self) -> Recipient { + Recipient(Kem::sk_to_pk(&self.0)) + } +} + +impl crate::Identity for Identity { + fn unwrap_stanza(&self, stanza: &Stanza) -> Option> { + if stanza.tag != MLKEM768X25519_RECIPIENT_TAG { + return None; + } + + // Enforce valid and canonical stanza format. + // https://c2sp.org/age#mlkem768x25519-recipient-stanza + let enc = match &stanza.args[..] { + [arg] => match base64_arg::<_, 1120, 1120>(arg) + .and_then(|encoded| ::EncappedKey::from_bytes(&encoded).ok()) + { + Some(enc) => enc, + None => return Some(Err(DecryptError::InvalidHeader)), + }, + _ => return Some(Err(DecryptError::InvalidHeader)), + }; + if stanza.body.len() != ENCRYPTED_FILE_KEY_BYTES { + return Some(Err(DecryptError::InvalidHeader)); + } + + // A failure to decrypt is non-fatal (we try to decrypt the recipient stanza with + // other `pq` identities), because we cannot tell which identity matches a + // particular stanza. + hpke_open::(&enc, &self.0, MLKEM768X25519_SALT.as_bytes(), &stanza.body) + .ok() + .map(|mut pt| { + // It's ours! + Ok(FileKey::init_with_mut(|file_key| { + file_key.copy_from_slice(&pt); + pt.zeroize(); + })) + }) + } +} + +/// The hybrid post-quantum age recipient type. Files encrypted to this recipient can be +/// decrypted with the corresponding [`Identity`]. +/// +/// This recipient is safe against future cryptographically-relevant quantum computers, +/// and can only be used along with other post-quantum recipients. +/// +/// This recipient type is anonymous, in the sense that an attacker can't tell from the +/// age-encrypted file alone if it is encrypted to a certain recipient. +#[derive(Clone, PartialEq, Eq)] +pub struct Recipient(::PublicKey); + +impl std::str::FromStr for Recipient { + type Err = &'static str; + + /// Parses a recipient key from a string. + fn from_str(s: &str) -> Result { + bech32_decode( + s, + |_| "invalid Bech32 encoding", + |hrp| { + (hrp == RECIPIENT_PREFIX) + .then_some(()) + .ok_or("incorrect HRP") + }, + |_, bytes| { + ::PublicKey::from_bytes(&bytes.collect::>()) + .map_err(|_| "invalid recipient") + .map(Self) + }, + ) + } +} + +impl fmt::Display for Recipient { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + bech32_encode_to_fmt(f, RECIPIENT_PREFIX, &self.0.to_bytes()) + } +} + +impl fmt::Debug for Recipient { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{self}") + } +} + +impl crate::Recipient for Recipient { + fn wrap_file_key( + &self, + file_key: &FileKey, + ) -> Result<(Vec, HashSet), EncryptError> { + let (enc, ct) = hpke_seal::( + &self.0, + MLKEM768X25519_SALT.as_bytes(), + file_key.expose_secret(), + &mut UnwrapErr(SysRng), + ); + + let encoded_enc = BASE64_STANDARD_NO_PAD.encode(enc.to_bytes()); + + Ok(( + vec![Stanza { + tag: MLKEM768X25519_RECIPIENT_TAG.to_owned(), + args: vec![encoded_enc], + body: ct, + }], + super::label_pq_only(), + )) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use age_core::{format::FileKey, secrecy::ExposeSecret}; + use hpke::{Deserializable, Kem as _}; + use proptest::prelude::*; + + use super::{Identity, Kem, Recipient}; + use crate::{Identity as _, Recipient as _, native::label_pq_only}; + + pub(crate) const TEST_IDENTITY: &str = + "AGE-SECRET-KEY-PQ-1XX76JRALNLXDMEW0CRK45QMCCH4X06SE84UN3VPM33W6HWDX0H3SK3ZQFR"; + pub(crate) const TEST_RECIPIENT: &str = "age1pq1x34nzsvr0rxjsgdn8zgyhfe8j7ceq5r9rdelkjuh3y235jzxshfg87pzf5zrqtzdxz95paef6caq5aapdmwjjqpjfdyxnzr2zampc3uxy0dg4z2n2gm9su72p0pc3u0jvev55l694v78snxg3yzvcl7yda0eyytqj6a0ec477lnhcy5hzpz4zq3pxanve4cn62gqj3pjy5lqj9c6kyj4v2z8alktn8zh99970x79gjkv7522hv9kfz35zsnxhsx8wwtmu9cy3ftzjgwcp4sshn3llnylnpdsyz5jm72vefv4x5vfwytrefxg4wq3mv42wcrvkj742479zrxzpvp2p3e9fed9f0739vcu80r7ma28qfhnvlv4gfzel9q654dj3zmuvvz893azhxdvs9fxd0r7jzchzcfcs5mkyyjxhw0n2z6dvp9yn9qfdp29h0azxqyjw6v7fhyuzj7zel0uq6j9rd7wgrpz7mf5dnj43jwsgvrc8qcnhy7tu6dkdujuxzkp9xj43xe8h92ktre2a3u3s8mm5mrp9nr9pwkgtz4mdlq9hgn4fps4k57ff6wddn2fy23t47sm20r8km8sd2pcyyafnet8f0dajsrlyjeah4n3mssr6aseevuuskdvq5lzguyvpgwpta742c6698vgutzqgny8usfg0w2he7kq5vyxjd0f9hqg8xk26y9e4th0gezq92q4cpp5p2y9hf5f2cje5l0c3sa3a2qxmm38pxxvhxh99yzmfz0zk7r2s64nnwjhkfgfr3gf8xnmppcgmaykvh5sh6g7vk9790rf8ws0axmr2t7z8aae5fq2029uvcn2ghgt4fu4wgwdc0k0cz52qkvwmuzj8p8k5jgf3xzk5zmrkavjekjrpeq408xz3zxazwkc6tyfmhayrkfpjhwtz5mp8j8guqe43k2q6m2kte03vrw27y3wmqyu5etmt9dnkwcnnpmu9gz9dekfhdevf42ucshphnrk38ra6hx8w5f8q5ru0xdhrjxmwqf6cused7zc5xvq43r0zscjglpwlptpwydhqw64xz7ptjdyeyzpq2zkxtmzg29gzjpvzva4d3l0cenn9xs297wf4y4ukwrunf57xj6pm7nvrkwvtrt8hwcmgv8x7ajw7258ugf9wvkmk4052ekg87tw5vnx8nq2swyzv77v8yqlwsenvamr0zssknwts8rrhfuwj7ykysnq9jxy0uv3kuyt22djszjdtvpz6d0s0kwh8ryynddzud92emeyvvyqktd0jtj7rvvg5gch25v8smlvny3kvn5gagyz475ze2y6q466xqmz2n3hs77lddeqyta2nch5k2u5yacuk9ywnwfdzvyejnucz724hj77hrrmakm7pr3kxsrxq22ejexlud9fy2kdqmkg5yncz7jm5wv2qjk5w5kvcpqsry2yqffh2la52dxfjkjq5rzhjzeyn6dupn0qwtyv7s4lwg3xdarsdlwe2y3tujy480y7z39q259fzx6jhd2j0f5hagqpcpees7hzc2yrk5cy788uk3s7qvp5cpepx24gvws3m2g433exgwppnkjscec8qu4y9z9r7vccexjcjaen42245lmgmxmuavg9alej92322gvvyy2t6267v09ch64y0m53jff0vjj96s0ypk60hr3jw4myd6m5hpn3xjstx7tl2szhpr5qe8jj08ydjc4wy2rch2fhuy3pdfjax5awe9j99ly5hkntzz9fe5zatgjvzdd0kgtxs25njnajyf6ssekp7gelxquusn4pt25czh3scj68kq79wdn5tgm6yvm9nzavrg043x3msnygf8dweknw5jmqd0uvny6ttsn09508k0c55zfnegrm9efhxpfqdkmhh6gjtqmwze9pyyzk3tlhl53k2ykx3qheyty7saeq0d3fzv49zc0k"; + + #[test] + fn recipient_encoding() { + let pk: Recipient = TEST_RECIPIENT.parse().unwrap(); + assert_eq!(pk.to_string(), TEST_RECIPIENT); + } + + #[test] + fn recipient_from_identity() { + let key = TEST_IDENTITY.parse::().unwrap(); + assert_eq!(key.to_public().to_string(), TEST_RECIPIENT); + } + + proptest! { + #[test] + fn wrap_and_unwrap(sk_bytes in proptest::collection::vec(any::(), ..=32)) { + let file_key = FileKey::new(Box::new([7; 16])); + let sk = { + let mut tmp = [0; 32]; + tmp[..sk_bytes.len()].copy_from_slice(&sk_bytes); + ::PrivateKey::from_bytes(&tmp).unwrap() + }; + + let res = Recipient(Kem::sk_to_pk(&sk)) + .wrap_file_key(&file_key); + prop_assert!(res.is_ok()); + let (stanzas, labels) = res.unwrap(); + prop_assert!(labels == label_pq_only()); + + let res = Identity(sk).unwrap_stanzas(&stanzas); + prop_assert!(res.is_some()); + let res = res.unwrap(); + prop_assert!(res.is_ok()); + let res = res.unwrap(); + + prop_assert_eq!(res.expose_secret(), file_key.expose_secret()); + } + } +} diff --git a/age/src/native/x25519.rs b/age/src/native/x25519.rs index 4da1c503..de3e1f84 100644 --- a/age/src/native/x25519.rs +++ b/age/src/native/x25519.rs @@ -31,7 +31,9 @@ pub(crate) const EPK_LEN_BYTES: usize = 32; pub(crate) const ENCRYPTED_FILE_KEY_BYTES: usize = FILE_KEY_BYTES + 16; /// The classic age identity type, which can decrypt files encrypted to the corresponding -/// [`Recipient`]. +/// [`Recipient`]. For post-quantum resistance, use [pq::Identity]. +/// +/// [pq::Identity]: crate::pq::Identity #[derive(Clone)] pub struct Identity(StaticSecret); @@ -150,10 +152,12 @@ impl crate::Identity for Identity { } /// The classic age recipient type. Files encrypted to this recipient can be decrypted -/// with the corresponding [`Identity`]. +/// with the corresponding [`Identity`]. For post-quantum resistance, use [pq::Recipient]. /// /// This recipient type is anonymous, in the sense that an attacker can't tell from the /// age-encrypted file alone if it is encrypted to a certain recipient. +/// +/// [pq::Recipient]: crate::pq::Recipient #[derive(Clone, PartialEq, Eq, Hash)] pub struct Recipient(PublicKey); diff --git a/age/src/protocol.rs b/age/src/protocol.rs index 359b2f08..a4e281ec 100644 --- a/age/src/protocol.rs +++ b/age/src/protocol.rs @@ -345,7 +345,7 @@ mod tests { use age_core::secrecy::SecretString; use super::{Decryptor, Encryptor}; - use crate::{EncryptError, Identity, Recipient, identity::IdentityFile, scrypt, x25519}; + use crate::{EncryptError, Identity, Recipient, identity::IdentityFile, pq, scrypt, x25519}; #[cfg(feature = "async")] use futures::{ @@ -471,6 +471,29 @@ mod tests { ); } + #[test] + fn pq_round_trip() { + let buf = BufReader::new(crate::pq::tests::TEST_IDENTITY.as_bytes()); + let f = IdentityFile::from_buffer(buf).unwrap(); + let pk: pq::Recipient = crate::pq::tests::TEST_RECIPIENT.parse().unwrap(); + recipient_round_trip( + iter::once(&pk as _), + f.into_identities().unwrap().iter().map(|i| i.as_ref() as _), + ); + } + + #[cfg(feature = "async")] + #[test] + fn pq_async_round_trip() { + let buf = BufReader::new(crate::pq::tests::TEST_IDENTITY.as_bytes()); + let f = IdentityFile::from_buffer(buf).unwrap(); + let pk: pq::Recipient = crate::pq::tests::TEST_RECIPIENT.parse().unwrap(); + recipient_async_round_trip( + iter::once(&pk as _), + f.into_identities().unwrap().iter().map(|i| i.as_ref() as _), + ); + } + #[test] fn scrypt_round_trip() { let test_msg = b"This is a test message. For testing."; diff --git a/age/src/simple.rs b/age/src/simple.rs index 95e4f7e4..143c0d42 100644 --- a/age/src/simple.rs +++ b/age/src/simple.rs @@ -75,7 +75,7 @@ pub fn decrypt(identity: &impl Identity, ciphertext: &[u8]) -> Result, D #[cfg(test)] mod tests { use super::{decrypt, encrypt}; - use crate::x25519; + use crate::{pq, x25519}; #[cfg(feature = "armor")] use super::encrypt_and_armor; @@ -104,4 +104,29 @@ mod tests { let decrypted = decrypt(&sk, encrypted.as_bytes()).unwrap(); assert_eq!(&decrypted[..], &test_msg[..]); } + + #[test] + fn pq_round_trip() { + let sk: pq::Identity = crate::pq::tests::TEST_IDENTITY.parse().unwrap(); + let pk: pq::Recipient = crate::pq::tests::TEST_RECIPIENT.parse().unwrap(); + let test_msg = b"This is a test message. For testing."; + + let encrypted = encrypt(&pk, test_msg).unwrap(); + let decrypted = decrypt(&sk, &encrypted).unwrap(); + assert_eq!(&decrypted[..], &test_msg[..]); + } + + #[cfg(feature = "armor")] + #[test] + fn pq_round_trip_armor() { + let sk: pq::Identity = crate::pq::tests::TEST_IDENTITY.parse().unwrap(); + let pk: pq::Recipient = crate::pq::tests::TEST_RECIPIENT.parse().unwrap(); + let test_msg = b"This is a test message. For testing."; + + let encrypted = encrypt_and_armor(&pk, test_msg).unwrap(); + assert!(encrypted.starts_with("-----BEGIN AGE ENCRYPTED FILE-----")); + + let decrypted = decrypt(&sk, encrypted.as_bytes()).unwrap(); + assert_eq!(&decrypted[..], &test_msg[..]); + } } diff --git a/rage/build.rs b/rage/build.rs index 9594a963..be0bf32d 100644 --- a/rage/build.rs +++ b/rage/build.rs @@ -258,6 +258,9 @@ impl Cli { .subheading(fl!("man-rage-native-x25519-keys-heading")) .paragraph(fl!( "man-rage-native-x25519-keys", + example_age_pq_recipient = "age1pq167[... 1950 more characters ...]", + example_age_pq_identity = + "AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T", example_age_recipient = "age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh", example_age_identity = "AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ", diff --git a/rage/i18n/en-US/rage.ftl b/rage/i18n/en-US/rage.ftl index ccc83682..9c6fb89e 100644 --- a/rage/i18n/en-US/rage.ftl +++ b/rage/i18n/en-US/rage.ftl @@ -15,8 +15,10 @@ -rage = rage -rage-keygen = rage-keygen -stdin = "-" --recipient-prefix = age1 --identity-prefix = AGE-SECRET-KEY-1 +-pq-recipient-prefix = age1pq1 +-pq-identity-prefix = AGE-SECRET-KEY-PQ-1 +-x25519-recipient-prefix = age1 +-x25519-identity-prefix = AGE-SECRET-KEY-1 -armor-pem-type = AGE ENCRYPTED FILE -rage-mount = rage-mount @@ -356,22 +358,37 @@ man-rage-recipients-and-identities = to. {identities} are private values, like a private key, that allow decrypting a file encrypted to the corresponding {recipient}. -man-rage-native-x25519-keys-heading = Native X25519 keys +man-rage-native-x25519-keys-heading = Native keys man-rage-native-x25519-keys = Native {-age} key pairs are generated with {-rage-keygen}(1), and provide small - encodings and strong encryption based on X25519. They are the recommended - recipient type for most applications. + encodings and strong encryption based on X25519 for classic keys, and X25519 + + ML-KEM-768 for post-quantum hybrid keys. The post-quantum hybrid keys are secure + against future quantum computers and are the recommended recipient type for most + applications. - A {recipient} encoding begins with "{-recipient-prefix}" and looks like the - following: + A hybrid {recipient} encoding begins with "{-pq-recipient-prefix}" and looks like + the following: + + {" "}{$example_age_pq_recipient} + + A hybrid {identity} encoding begins with "{-pq-identity-prefix}" and looks like + the following: + + {" "}{$example_age_pq_identity} + + A classic {recipient} encoding begins with "{-x25519-recipient-prefix}" and looks + like the following: {" "}{$example_age_recipient} - An {identity} encoding begins with "{-identity-prefix}" and looks like the - following: + A classic {identity} encoding begins with "{-x25519-identity-prefix}" and looks + like the following: {" "}{$example_age_identity} + A file can't be encrypted to both post-quantum and classic keys, as that would + defeat the post-quantum security of the encryption. + An encrypted file can't be linked to the native recipient it's encrypted to without access to the corresponding identity. From ce4df21bd66ed6b81bd6a68eae825db5fb402b08 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Mon, 13 Jul 2026 23:32:56 +0000 Subject: [PATCH 2/3] CI: Update interop test to check `pq` recipients and identities --- .github/workflows/interop.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/interop.yml b/.github/workflows/interop.yml index 6e8d3ba5..477f2379 100644 --- a/.github/workflows/interop.yml +++ b/.github/workflows/interop.yml @@ -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 @@ -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: | @@ -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 From 86323d01b0759787c6a4068f344492e4580dd762 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Tue, 14 Jul 2026 03:14:33 +0000 Subject: [PATCH 3/3] age: Update testkit test files Source: https://github.com/C2SP/CCTV/tree/e9274a7bdbfdcb89623a03c6308aecb1a029095d/age/testdata --- age/tests/testdata/testkit/armor_hybrid | Bin 0 -> 1951 bytes age/tests/testdata/testkit/armor_scrypt | 12 ++ .../testdata/testkit/{armor => armor_x25519} | 0 age/tests/testdata/testkit/hybrid | 10 ++ age/tests/testdata/testkit/hybrid_and_x25519 | 12 ++ age/tests/testdata/testkit/hybrid_bad_tag | 10 ++ .../testkit/hybrid_currupted_enc_mlkem | 10 ++ .../testkit/hybrid_currupted_enc_x25519 | 10 ++ .../testdata/testkit/hybrid_extra_argument | 10 ++ age/tests/testdata/testkit/hybrid_grease | 14 +++ age/tests/testdata/testkit/hybrid_identity | 10 ++ .../testdata/testkit/hybrid_long_file_key | 10 ++ age/tests/testdata/testkit/hybrid_long_share | 10 ++ age/tests/testdata/testkit/hybrid_low_order | 10 ++ .../testkit/hybrid_multiple_recipients | Bin 0 -> 2739 bytes age/tests/testdata/testkit/hybrid_no_match | 9 ++ .../testkit/hybrid_not_canonical_body | 10 ++ .../testdata/testkit/hybrid_not_canonical_enc | 10 ++ age/tests/testdata/testkit/hybrid_short_share | 10 ++ age/tests/testdata/testkit/hybrid_uppercase | 10 ++ age/tests/testdata/testkit/hybrid_x25519_arg | 11 ++ .../testdata/testkit/x25519_extra_argument | 2 +- age/tests/testdata/testkit/x25519_identity | 2 +- age/tests/testdata/testkit/x25519_long_share | 2 +- age/tests/testkit.rs | 111 ++++++++++++++++-- 25 files changed, 291 insertions(+), 14 deletions(-) create mode 100644 age/tests/testdata/testkit/armor_hybrid create mode 100644 age/tests/testdata/testkit/armor_scrypt rename age/tests/testdata/testkit/{armor => armor_x25519} (100%) create mode 100644 age/tests/testdata/testkit/hybrid create mode 100644 age/tests/testdata/testkit/hybrid_and_x25519 create mode 100644 age/tests/testdata/testkit/hybrid_bad_tag create mode 100644 age/tests/testdata/testkit/hybrid_currupted_enc_mlkem create mode 100644 age/tests/testdata/testkit/hybrid_currupted_enc_x25519 create mode 100644 age/tests/testdata/testkit/hybrid_extra_argument create mode 100644 age/tests/testdata/testkit/hybrid_grease create mode 100644 age/tests/testdata/testkit/hybrid_identity create mode 100644 age/tests/testdata/testkit/hybrid_long_file_key create mode 100644 age/tests/testdata/testkit/hybrid_long_share create mode 100644 age/tests/testdata/testkit/hybrid_low_order create mode 100644 age/tests/testdata/testkit/hybrid_multiple_recipients create mode 100644 age/tests/testdata/testkit/hybrid_no_match create mode 100644 age/tests/testdata/testkit/hybrid_not_canonical_body create mode 100644 age/tests/testdata/testkit/hybrid_not_canonical_enc create mode 100644 age/tests/testdata/testkit/hybrid_short_share create mode 100644 age/tests/testdata/testkit/hybrid_uppercase create mode 100644 age/tests/testdata/testkit/hybrid_x25519_arg diff --git a/age/tests/testdata/testkit/armor_hybrid b/age/tests/testdata/testkit/armor_hybrid new file mode 100644 index 0000000000000000000000000000000000000000..6c344c84e9ee139e61fc68e6f8411af4f790bdba GIT binary patch literal 1951 zcmV;Q2VnSRcyMK7bUGk&bz@^?b8`xCVR>wCVPrZWFflV`H8eCZFk?6}WMMEkFf%td zH928qVL322VK`zqHZ)~9H)LfvF=H?|IAJk3GB7qWV>DtlHZn41WHMo53TA0+Wgu&1 zc{(69IW#piV>DwlW;Hi5Ff}tZH8e6bWHd1~GBi0fWi&MkX=G(?bZK;XIv_zuMJ-cB zLsCUlElWjNEl^P{F-TfWM_N@-R5V3YP)kHbRyasgM@B?LSz0tjH8L_+FhMs^RWeLS zR5(RQIafAWOin{HQ&%@QSW8C=VRCJ6a%E&XAbDkT3S)0=aB^jHb7f>YAbMIm8~E##!Y_;D zLN~p;CMQTaj!rllc}k~JWp9BSqRI4eC&m*Hm=xt?B6egM!;Do(?g?fT(3NN`}yg4kTO21GukrxBmT?_CsM9wmCI_FMEj#u6L zyOy&R$C(6Ly^tEZlwrtCZAL5S&8Dw!fn47bch8UHyG@M)+Q zbT=_}H=`n}h&*3O%yW^yTJW`_m@0=BTqQ2A#`&fM{kCBTf*e8b)N^OSQd`y87E++R zA1<4@HR1fy;rvG>?v3}d)FUx0A-W|#$>qYPGtar7n zJKhMRs>G3N>P?kU%f}3t()0$nut^sYuo22e5mBP-!Qd=h-_bLYkES-<5ppts0+9}Y z0aqD3r)^J9*U41Z5g+@;uC6ou3&b%63!4J!$NIpWlW%vBpeW_#2 zf>ym2hS{kF*`E@=Zx?7*UM+?4vTDWfDaO5UiuDe%uY$0tQQfiiBywrAx=Gdn8yI5T zpA3xScGICDaaC>_TsB%UnQs>BpK{Zy=*d$E@?CC@=cDIV&F5zFY0#tuSKf^p&d3eX zsug@%%n+2S&AVty-*q^=UTF$@VuT~?`R z(JI2W7O4999ob=@IF%VbvO>e|$9nSFj4xz2@)@v^Jyf6Vl!_AS32X(1=a8JZ$et$5 zrhI#U3G(rxoN0;|slzVLVZ5@*8$QZiphb4cQS`839ts@on#7&1a?T{0A1r;nQ6AT= zC*Q0l2RlA@!W-pO~X$@^!UxLS%5=2Lbz#GI(; zO6486Wa!kDLKf?1yw!C%|ufAZ5mO>>gZ}C2Is;jO9uO;eq3yp2PITwUh zEy%w3#xB7b&~x6muXgAjMf%+FLEgn6e)!McR-oHl(j83o8`)m=g`~2PS=H7iJ8$Nu zQTq(ak-jK&GezaII2tT!@+h~g=V*B4JWE)ZbyUIbI+iORXjX!Jm#e+8050;_fq-*A zT1Cf7b;u?DyGn@_klgd$f$MPJt|Y5Dl9LN(%n>Q%6PeP??VZ?vr0_r z(2`Ox7l9t=i(|>0lGOV6t~cK|(+SZ@rf29tF8t;sd*pq)!bUB1!O2^2 zmF~Cde^tr{|M9kZE*9pkDyp11BzjbwJI?+-VyT&njlaDYw~%7II|&kDgy ziTu!R3@mv{}<^HPT!pm|lP&nnH~zv23E ly#42|6JF5KABlVmSNV1d{s9f9$^W&^{{jF2|Noxb_fSbbr(Xa7 literal 0 HcmV?d00001 diff --git a/age/tests/testdata/testkit/armor_scrypt b/age/tests/testdata/testkit/armor_scrypt new file mode 100644 index 00000000..51e40922 --- /dev/null +++ b/age/tests/testdata/testkit/armor_scrypt @@ -0,0 +1,12 @@ +expect: success +payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab +file key: 59454c4c4f57205355424d4152494e45 +passphrase: password +armored: yes + +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNjcnlwdCByRjAvTndibFVISFRwZ1Fn +UnBlNUNRIDEwCmdVakV5bUZLTVZYUUVLZE1NSEwyNG9ZZXhqRTNUSUMwTzB6R1Nx +SjJhVVkKLS0tIElPWGlRWVN0a29UMW12WlcydEZPcVpkaFJWdmo1OGVnQUJ4L3NX +ZlpRYmMKGzXG5ofdANo6w3msn3QsIf0YWhuePe1znRSsappQEk24Ztg= +-----END AGE ENCRYPTED FILE----- diff --git a/age/tests/testdata/testkit/armor b/age/tests/testdata/testkit/armor_x25519 similarity index 100% rename from age/tests/testdata/testkit/armor rename to age/tests/testdata/testkit/armor_x25519 diff --git a/age/tests/testdata/testkit/hybrid b/age/tests/testdata/testkit/hybrid new file mode 100644 index 00000000..94d67823 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid @@ -0,0 +1,10 @@ +expect: success +payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- 7wCgKc4t8kKmKJTNrYs7MoLKHk8Sqt8Y3oTZc08sQjM +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_and_x25519 b/age/tests/testdata/testkit/hybrid_and_x25519 new file mode 100644 index 00000000..3a3f6754 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_and_x25519 @@ -0,0 +1,12 @@ +expect: success +payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG + +age-encryption.org/v1 +-> X25519 ajtqAvDEkVNr2B7zUOtq2mAQXDSBlNrVAuM/dKb5sT4 +0evrK/HQXVsQ4YaDe+659l5OQzvAzD2ytLGHQLQiqxg +-> mlkem768x25519 uXnW4tbM61OOw02EWIFqJWjxciCCRr3Q/opLVulsPFrawg07AVzaGWs+bXvljyF1LAbJluKZPUHRlvLkWfW83QjDWJmeKJzLOK0qv1ped9DG5FqunlQmtEr7sfBgPKTP45tNOynYJ8+2syITKkuVtbpkRW+WGZH++GPuTTZd8jn21flaod6Hitc8fSJlVZo8/26pQEA5Q3JRqfah8I1r/Q8RuyXs4ZC/bF4WEFo2oAodBCcCOjPDC8tvvTQ3Unoo5m+JCpnvsKHDqpaFr2Ycmvz6S+3s+e1nItXJiuk4rs5ykihaHiGv96woe8fYoAGkj3v71+d1uicGKwWFVeOMYQq6XjbQsyc2947q3DdnMuj5LGMju+LFDn4JCiJouHTxMcSmeLdIlLH706LptsqzLIcqtCa2ee+hyBa9uVKotxg8SI6HyCrJDmDwo5LDC4c8WY7t95b4zNQXrutpqvnTKwhDNsHlkufd7qrLaF8sNKAKgde0Gytills1gesKNgZ+xyWs+Mq//zTdwFVVw0dexauKiqAYWtFLSJW43g4BHoeR1iHoF972ThRr2jq48o9UjFZ4HV9md0u3bvNBOoY/xs1wuzCu4XtFmfckQfChvMySzVYCRt4UQFpGlZ48RAFvchEzQDw/deRlTCmTySSAN9xwFs6ODvzHPPSVhAk4EstP6uLouGTc2waKSOKhY0Obt2BgZFWYBH7xDsc8py9Vzmc51ZI5OAB/LkNTjMsl505zu3CJ5MJZC3rW5cF6XqD21gE/8aJuQaEO0huDnKKw87hXlqnWbz946BQZrQyt2Raz9Z0s89vAuQANClXiOm0jU0tfi2MiTXGnQU3xmcyQH547ySRSbXDIV+dgYAzj7yMipG3JmiTRFoMsNezyf+/XtFM+l9rV3dYqImlvh2v2z/nl/JBHeLjJEpuEMW3Z7kVBGRyNq8RZdeI1quby2sBXX9u7baUOivwWPpPK+1cVHOSKcPTD4mwVDagBXqcVNtoUjmHjWN7+MQPGq0Vz1NqxB7dQ0UVmOkKAExZ2vl+8C83eZPKe8cRFGh17MedSV5rwSIkSVMXHhR6ByfiGIMaohxy7MtcpWjqkgGYg1TFgjwEeRbAzBMzbRRnlA9CekgcTpInbCM9ltIbBlNqQjiw4HpxDXbDzgDHIPmd3cQrK1n/vJ3ozSBSPKqjDEN1KuAdLvCssViTAwvWboAYXay4BOevSgaTabj3SSaGMZ2TIEBN9TdB65+eXmkX8BGoIi1ljODUuq0A5Qbz9rb8DGg4TWd3hjVN8hJtaqGEI8Tb1UCURNCRTES/ot2YRYH2q5Xq0x1UJ2Lx6+CLnpPP9nVXYE34kw+oxWgWCtwW+7lZkkUiaoC5InY6+6d1S2JpYbY04yCxbEVcAtBpNawnEs2n7EjW8724aFWtpA7+mMBxIVGrMN3X4LPGhFQ7bwL2nmlTSoLu7oBsvG7Cczy08U5ZW1Zmf9ll7vPuByw/mSADQUg +pfgqxYNs/L5bIyyt+4KNib+WTYBQBaQ9k1NNjOdyBFg +--- s/KrBf0KMZqiuFTHVgLDk9UoNKRy96zb2abbyvW7mvA +W<{,GA3]KQqcg \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_bad_tag b/age/tests/testdata/testkit/hybrid_bad_tag new file mode 100644 index 00000000..69bf2cd5 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_bad_tag @@ -0,0 +1,10 @@ +expect: no match +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the ChaCha20Poly1305 authentication tag on the body of the mlkem768x25519 stanza is wrong + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaittg +--- ozjlzjWDSbqxs/Ku3FHncEh/ZnP97YhwfPvt7ushZHk +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_currupted_enc_mlkem b/age/tests/testdata/testkit/hybrid_currupted_enc_mlkem new file mode 100644 index 00000000..d40a7b03 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_currupted_enc_mlkem @@ -0,0 +1,10 @@ +expect: no match +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the ML-KEM part of enc is corrupted + +age-encryption.org/v1 +-> mlkem768x25519 yvLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- tklCMe2Oh3oULc36hD4ts54f9XOLyt4TNAvE6QKfFW4 +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_currupted_enc_x25519 b/age/tests/testdata/testkit/hybrid_currupted_enc_x25519 new file mode 100644 index 00000000..1a70230b --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_currupted_enc_x25519 @@ -0,0 +1,10 @@ +expect: no match +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the X25519 part of enc is corrupted + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91ow8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- MMSCj7ztQRFh/udPB22vPUrYbAdVoJbacI1oo3+bCfw +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_extra_argument b/age/tests/testdata/testkit/hybrid_extra_argument new file mode 100644 index 00000000..4b1a4d15 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_extra_argument @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the mlkem768x25519 stanza has an unexpected extra argument + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw 1234 +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- PfN7obQkWwEc6uTHyCAApxtUHGtkOQdJkEPPif1tVhs +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_grease b/age/tests/testdata/testkit/hybrid_grease new file mode 100644 index 00000000..7f206104 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_grease @@ -0,0 +1,14 @@ +expect: success +payload: 013f54400c82da08037759ada907a8b864e97de81c088a182062c4b5622fd2ab +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG + +age-encryption.org/v1 +-> grease + +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +-> grease + +--- l+j2R1qVDedq7DAoNfV1wyrt72rmw3BfegGQdRb6iDk +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_identity b/age/tests/testdata/testkit/hybrid_identity new file mode 100644 index 00000000..8b17db6b --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_identity @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the X25519 part of enc is the identity point, so the shared secretis the disallowed all-zero value + +age-encryption.org/v1 +-> mlkem768x25519 pXfvK9UJ8Kxx4w2RxolbquqxGtY4esGgRs9Wo4YSPJK+rQsymoCShoU7q6yFTmQYO4uWjxN4yTkvnCnm5DbBooXMz22zl0/z/v7SMtrlc588XrJ+uT1388En/tB5GoRlqeDmK986caJ35RwzrBdZMSmAi5jiHHcXYevHq9tQPShb7RzUGSWE+O2Pvq7q1MPEGikr8b+HeYGTFc/dGccT4G0aa5RKK7Zc5eBcNUaNHbl5ZPqPfmiDyVAZ0y2rIPhtWVCAIL4DpFLHpm1f2GdYLor86REzhekpUr40/FeZt+3wdhdVsFjYuF7Rc/m5Yyg5xs4H9ZApWcqPxKuHYWLnX/w50+AiEP8fB+L9F2He0SyWBcfcrY7yOnEKwMEcsUs6yfjK0nvYmse2zZyAGRteBBP96yfngFMSTx5OQ3b3PVwWe41a7URAw8/GYohic7HH1FTTsrXAGVTOE3Zru72MmB9zkqcS+RXmBgjdjjKjlWEPN/449jv1cMcVMoplp/w4DaGQbhYq9Qd8o4sT8rQHl0xzZmah4H5KNkFPKk8a33cOBho0XzeJBHxsFNMuLIFkQ3xAvIDmIrOyNCTulPn/W8oOQqPmQe3xouglOzHk5oI4KX2bQK5d3osaUSoQK4TT+nt9yoGnTzNr563IYfsiwC8dgOcaEvna3hiai36c8YmkWsorFncQ9qZQwvv/H9HalT1WM9iUsj/xmxMnoXZaXorMHEB8c5gbtS39dtxTfsWbgUtH/Dj20rvVRRHOdgqqMl/e2ovxglpFBrJqarVVcPRGTHHkmO3RmXtYDdPqe5V8rmeHYLpgigkQsQ/5uDVqRNnc97obxw4bDUvBqCCCJqnRLDu6LLJmisk5GdLM4AD4PfN4E674chI929DWU/XUckOsB2Nd8G9lWbPyfHWZ8F/Fn3ohogQU4AwxbIZzp+MICNOVEvnLDAhw2gQji4f7xA39MS+aBq2Ws+/cWkN5kVyRNMPYwSdjEPMU2hLsoKHn9ZyWNNughDmJeCMXtoZavR1DMC90IntjGSxR4TpkfFo05sVq0xOnJcz+QS/uCeX+pzsFfQ5vJsM7SslNgWyvRXeDdE21qKzZ6NVTKq75nlzoJ8ZrwNLZasvyDFFHcU9hB+o7UpDpj++0aya/krdIsuafTGaAGh3+psoOK/QEEyQAMdCFOVF6LI5Pv6bRevw5nRWZI7JycxOS25iGyEWAGe88h4qRVPqaA4wFC2j52vWbufHPQdLKO1gnuWUJ/5ZUiIAZYIH0KCrp1Y9U27rJ87yjYJS83ePqwrXhUSulN+KvMY2IVsLWwdLTmtjXpGdHh1q3U9sYoOge3Dp3iqdGf921uPVFLfxxAPfRF2WUgOuMgvQmcbyJXUT+VdvB5wj0RPllCKIk4QNnV60/OlU3ATafN0VtYnyfk5Jw2i0PaQC24v8qzWhO2tOOLd3R7EUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +crw0lPntHMqnP7wuZREq3+1Hhv5eGesnWjR1oR13ozI +--- 9rFRTsB9R6F2QByisnbvPRshhXV2y3b3YMT2Lta5Q5w +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_long_file_key b/age/tests/testdata/testkit/hybrid_long_file_key new file mode 100644 index 00000000..30ba3cfc --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_long_file_key @@ -0,0 +1,10 @@ +expect: header failure +file key: 41204c4f4e4745522059454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the file key must be checked to be 16 bytes before decrypting it + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +lebfiVJQVzAB12xVL4RzIq7pdYrA3UjzR4iUFOaVXY7833xSygaeuP8 +--- hvc89H9wB3gby3kEBYeG+yPVY+lf3GJF0N9yOs76GE0 +roW= 1$!|Phr@A%; \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_long_share b/age/tests/testdata/testkit/hybrid_long_share new file mode 100644 index 00000000..f625c7e7 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_long_share @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: an extra most-significant zero byte is appended to the X25519 part of enc + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4JwA +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- +yfTwzKPrHWCwp4y7vFiEZwnE6N9QVBXno1ETNg95pU +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_low_order b/age/tests/testdata/testkit/hybrid_low_order new file mode 100644 index 00000000..c19e4c27 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_low_order @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the X25519 part of enc is a low-order point, so the shared secretis the disallowed all-zero value + +age-encryption.org/v1 +-> mlkem768x25519 pXfvK9UJ8Kxx4w2RxolbquqxGtY4esGgRs9Wo4YSPJK+rQsymoCShoU7q6yFTmQYO4uWjxN4yTkvnCnm5DbBooXMz22zl0/z/v7SMtrlc588XrJ+uT1388En/tB5GoRlqeDmK986caJ35RwzrBdZMSmAi5jiHHcXYevHq9tQPShb7RzUGSWE+O2Pvq7q1MPEGikr8b+HeYGTFc/dGccT4G0aa5RKK7Zc5eBcNUaNHbl5ZPqPfmiDyVAZ0y2rIPhtWVCAIL4DpFLHpm1f2GdYLor86REzhekpUr40/FeZt+3wdhdVsFjYuF7Rc/m5Yyg5xs4H9ZApWcqPxKuHYWLnX/w50+AiEP8fB+L9F2He0SyWBcfcrY7yOnEKwMEcsUs6yfjK0nvYmse2zZyAGRteBBP96yfngFMSTx5OQ3b3PVwWe41a7URAw8/GYohic7HH1FTTsrXAGVTOE3Zru72MmB9zkqcS+RXmBgjdjjKjlWEPN/449jv1cMcVMoplp/w4DaGQbhYq9Qd8o4sT8rQHl0xzZmah4H5KNkFPKk8a33cOBho0XzeJBHxsFNMuLIFkQ3xAvIDmIrOyNCTulPn/W8oOQqPmQe3xouglOzHk5oI4KX2bQK5d3osaUSoQK4TT+nt9yoGnTzNr563IYfsiwC8dgOcaEvna3hiai36c8YmkWsorFncQ9qZQwvv/H9HalT1WM9iUsj/xmxMnoXZaXorMHEB8c5gbtS39dtxTfsWbgUtH/Dj20rvVRRHOdgqqMl/e2ovxglpFBrJqarVVcPRGTHHkmO3RmXtYDdPqe5V8rmeHYLpgigkQsQ/5uDVqRNnc97obxw4bDUvBqCCCJqnRLDu6LLJmisk5GdLM4AD4PfN4E674chI929DWU/XUckOsB2Nd8G9lWbPyfHWZ8F/Fn3ohogQU4AwxbIZzp+MICNOVEvnLDAhw2gQji4f7xA39MS+aBq2Ws+/cWkN5kVyRNMPYwSdjEPMU2hLsoKHn9ZyWNNughDmJeCMXtoZavR1DMC90IntjGSxR4TpkfFo05sVq0xOnJcz+QS/uCeX+pzsFfQ5vJsM7SslNgWyvRXeDdE21qKzZ6NVTKq75nlzoJ8ZrwNLZasvyDFFHcU9hB+o7UpDpj++0aya/krdIsuafTGaAGh3+psoOK/QEEyQAMdCFOVF6LI5Pv6bRevw5nRWZI7JycxOS25iGyEWAGe88h4qRVPqaA4wFC2j52vWbufHPQdLKO1gnuWUJ/5ZUiIAZYIH0KCrp1Y9U27rJ87yjYJS83ePqwrXhUSulN+KvMY2IVsLWwdLTmtjXpGdHh1q3U9sYoOge3Dp3iqdGf921uPVFLfxxAPfRF2WUgOuMgvQmcbyJXUT+VdvB5wj0RPllCKIk4QNnV60/OlU3ATafN0VtYnyfk5Jw2i0PaQC24v8qzWhO2tOOLd3R7EVfnJW8o1CMJLHQsVWcg+9bBERcxFgcjobYIk7d0J8R1w +NdTIRdTiX20fj4qzePEX93+zxwjj09PorkmubTa/ruw +--- jgIovQ3Xih1NEN3q/5x3/gQ0RT/l0+x8m76cgBZ25pE +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_multiple_recipients b/age/tests/testdata/testkit/hybrid_multiple_recipients new file mode 100644 index 0000000000000000000000000000000000000000..367fecdb67495dcc08cf82ddaa8610c469347a63 GIT binary patch literal 2739 zcmV;k3QYB7cyMK7bUGk&bz@^?b8`xCVR>wCVPrZWFflV`H8eCZFk?6}WMMEkFf%td zH928qVL322VK`zqHZ)~9H)LfvF=H?|IAJk3GB7qWV>DtlHZn41WHMo53TA0+Wgu&1 zc{(69IW#piV>DwlW;Hi5Ff}tZH8e6bWHd1~GBi0fWi&MkX=G(?bZK;XIv_zuMJ-cB zLsCUlElWjNEl^P{F-TfWM_N@-R5V3YP)kHbRyasgM@B?LSz0tjH8L_+FhMs^RWeLS zR5(RQIafAWOin{HQ&%@QSW8C=V{dJ6a%FRKWn?-adTeQ83JQ4IY}Pjp_6%VF;5GV` zL;Sn-#l0<&+`Ij{mrU-jd#_QZ{|r%v%uxmwn2->hf`f1X7!W(Bc%HN_f0kpuKJ(s8 zS0zOgEJlBpXBtYTQ96i^RHm_)4uiFl zC_OeS?&)x9FWGw}S-tw!SnA!eiURAsy(*EfB=<0sSSJpBZfP3z7xpFIt*FU7=Cg%Y zJc7-&zudkfoTtqL|HAU8=b8wE1wZ#{z!VTkE9)`wW_;m1zOkw4vOeFle1g&DmzZN1yP zB=-%sd1B@Z>PfQ921gv#McTb*J}rBuIb>POi^Q+e2nfBfA1nRnsW>nGDdF&j?r=W8 zhBD%6@`WTd6y0tHhId%3kJYdES_z<21b0M-v|%GLon6*@#uLGVdk3)bYjK`u6DDeU)1+>iPb+2w9vN(OZ%FnZF_2_t$+8U%q7p0^W^fZ6{CvlvJ-$X zs_S;8X^Uj~$#*!pKnO|eYVTSE*)9ypo7YutGg%~9)tf7A9I;RbdFbhK8eSdQ1Xx@@ zgd&qECMovSa&Em4UqeKBJ8o+icfG{bOhW_-BsZV%$s7{AGx#SW-Lm}pmIEnY zEgo2xe@6o}F1m>7s_>?((_&O~yXxF0VOl%j-1VX-f@T{StcTuyt}Q(QK&kRs`%Cil zmp5*gtLvPf=6!)dMC1S&^Vb)WO>1{v(++dxUB^b!;X&Mu){D>gx&;qm3$im*M6kQQw-8{7C7)?)IV{N-vTEK^=OyS)38X?h#z! zR1~Q!GKT}sBke$6UdszESsB(>likOqiTr=I-4No>|)>s`k*)sgxu$G$2c z5gq4dv{nZ;u2mnaI>&u|`p95wTcG$@#e*48Nw%TIBdLlrMRzFjcAZCQ+Cez(osF;Dy-LhRi z_XU;n(_c_mX}!xOwBs?77!y|`iN0G!6fi}DSd`MCzl>(KaY6^E?;Mahj^odh7Dl4n zh&50vYGvtJZDRPf!HkPkW|T3RjZ!pw8;{$vqT$pAP;{Q4I#i#V?DfknPk?OZtw5?G ze@6viX;G>Jr5a1u(_&RDmHn1Z2=9UMYePK_0;w2|8g&bC-A7PiV8%E(6KqUYT5jTRHgidLk#@9;ABLE=s z!`n7*|7FDw#5gZ9p^UjbLdbn^VDMS62oli93@snrkubZyyg%$7=s@DbShOQcPtAx@ z^>W$BfcyCtgEhh9dM7DwVWKhmvU9A&JEs?m}b`r4eUf#gr1U$+` zWJyqlguYu@Wv4!9ZpQd?6pLc7DV&NE%-BTZ-T7Jpo;cUxIx%*L7%?4IWT=$4zheA9 z`qgUqv!=pB2W-y3E0hMK7av*9`n#MKT18v!bE7d7>(|nE_J4LF3>{25wKq1I7 zxm+`D5h23w(NlWgZ$V+4*C?%UQkmbR!q|zCtFOWGXnDEt#BrSW_Y=8V_GVX|a5-)1 z^kdoUIpEC(BO#1fc3N8;Z{bBQg+k?D?_+ukD0IL4OqBy0cH`(a7nP#h%Gyh zv#R$3u!Aww?8`C-$=B^}*CrUp*N^tL@Z!+Ts-s;(PodGCvFfcAkwO&~i%U%vi`<)~ zYO8 mlkem768x25519 7RdNaxaTStWCtLTQO/GrzScQIrRVFs2ErMPvqi/DXHYKuBeOmbawr00mWvLmvgKpHJxSCSR3ohZQBJPQ/VMeTvN6g8MejH+zHW3EBHRnvzoKD4RVNUqq8yZ8ACVqbURg8CsDvg/mcesPyLbNXBf3Itj/IaXEweig0Skak8qrCsgX418kH4Hr9ne0zQ2kj48Ea74W9Dz1oimJFq7X9rFxI61rUWd0v4Izm5yBUaX4NofifQ5aSwZhQxiOcLLqgSTWJjXnCU3sD5GYT4DCPORy8izZ+amat89hvHPojpwW1xSwJ9PYgA/+8nSXyHp/TwrZrn1cUjq4qsqzvZc81RqIpRSS678mBGBUVQ2ODwdEBBGm73zfWxLi/7Da6nSl3EuObkQSqODErF67gN3Pi9YJAGTiJt28fbUWw7ObBh4jS0UVpck9ZbbTsMaGeCLIaBGFdyG8cpExFuqt3oCuBuozN/nQDxjikPnUTrZsVUKdk5j5MCnRS+hgxcO4qOll05hBQkih38eZXqhiYOcyWb4R6xz7GKq31ATbBiJZgBnxwxfadxqp/jc1APJrkrwmcEQ3ZxveQ/ijUVcIQ/c+4DDqr5zwu4T307LEn8eI8MRQGeCMcjYTXSvc+hCpWibjHd686GlE4t4C8rchrxISjkb/lRI9BWDjdiE8/8iEc6OgwpRGIwndqpd20TmumETJMSyu3drKuz0H95IhJ6iAJHrgHJcAMOZFKDJsR58fHeMC4pNkBsG7VY1AVNBNcCkfLSacQtZD1k41kgGkycVlpfN4sY0PKqMjsszUuOxBg+3YodYl5b7R0k651/syIksPeBYtj4DD/PE1oijA08FBh436Oi83X6aRn84XWGwbjAvD8hKoiinjfiu107am8TYqBvKE8jcr6b3ydONFs+iYzI3AcS+SJ3BkT5ZLpc9pR9plZi9iI/N7PB+Ch9W74S5EyR/X4eyAMrDSnvxhGxPcmhbAFxWv3JlYcLMm/jQ1Do1pfUiWU6JK/LkUCQrl8VT6rZOMFiEI4M9foCmdP+lb7K0TF71d4Wa0ZQ5nxgLjQ0o/2lQLHvxS3DqRsiZIVDr4O+VDKRePjD6bDntEN4fLQVSpRFHOPGxVtDzjrUOxfEYaqNiUKjWIhXPYTIQf/8tu2Py5/EqI4ufDLbAVuP8H83afDXvrBKdl8b80ct/A1ngHBDZdBK09NShMCqU6RLp1i6BIKAHWjEsSA5WxDRdFiwTZMN0KDpkl8AIgH9Ge4dAB5PjxaACtBmEuEdBOz2aVxWNCS7lCkzFiRTNWrBdD8uE/dMvaY9kFhvk/DNQTeZiJdS/DTJCth9YWT32mw0W8D+tnH6W1074wJOYVnM+iqbzTp/9AhzkbVefTeyE7l3KSoAtBz5tKzotNV6KsvSntFe9i4xGG9ljbDQmFMHoLxddRuTEb6kOJzxl1sooaL5tmU95qO2oC8MSRU2vYHvNQ62raYBBcNIGU2tUC4z90pvmxPg +zIeijirgXivysIdzTEN9KzrtGjB10Dc8W0cFriNI3L8 +--- g1cq2rVS7EAf8Nu3o3SZb/b4ozq2O9ssPxzYjq2FdU4 +5TB9 Kom^OY mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitid +--- ts0obP14kZSisWlitsstd5XmDxOZTWIwlMnELJpSjwM +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_not_canonical_enc b/age/tests/testdata/testkit/hybrid_not_canonical_enc new file mode 100644 index 00000000..aa0fe38a --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_not_canonical_enc @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the base64 encoding of enc is not canonical + +age-encryption.org/v1 +-> mlkem768x25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jx +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- DX3pziWrwt9Mw2VKEgDIGLUDqtr/9D26V8+jtDPshsQ +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_short_share b/age/tests/testdata/testkit/hybrid_short_share new file mode 100644 index 00000000..e65bd175 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_short_share @@ -0,0 +1,10 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: a trailing zero is missing from the X25519 part of enc + +age-encryption.org/v1 +-> mlkem768x25519 pXfvK9UJ8Kxx4w2RxolbquqxGtY4esGgRs9Wo4YSPJK+rQsymoCShoU7q6yFTmQYO4uWjxN4yTkvnCnm5DbBooXMz22zl0/z/v7SMtrlc588XrJ+uT1388En/tB5GoRlqeDmK986caJ35RwzrBdZMSmAi5jiHHcXYevHq9tQPShb7RzUGSWE+O2Pvq7q1MPEGikr8b+HeYGTFc/dGccT4G0aa5RKK7Zc5eBcNUaNHbl5ZPqPfmiDyVAZ0y2rIPhtWVCAIL4DpFLHpm1f2GdYLor86REzhekpUr40/FeZt+3wdhdVsFjYuF7Rc/m5Yyg5xs4H9ZApWcqPxKuHYWLnX/w50+AiEP8fB+L9F2He0SyWBcfcrY7yOnEKwMEcsUs6yfjK0nvYmse2zZyAGRteBBP96yfngFMSTx5OQ3b3PVwWe41a7URAw8/GYohic7HH1FTTsrXAGVTOE3Zru72MmB9zkqcS+RXmBgjdjjKjlWEPN/449jv1cMcVMoplp/w4DaGQbhYq9Qd8o4sT8rQHl0xzZmah4H5KNkFPKk8a33cOBho0XzeJBHxsFNMuLIFkQ3xAvIDmIrOyNCTulPn/W8oOQqPmQe3xouglOzHk5oI4KX2bQK5d3osaUSoQK4TT+nt9yoGnTzNr563IYfsiwC8dgOcaEvna3hiai36c8YmkWsorFncQ9qZQwvv/H9HalT1WM9iUsj/xmxMnoXZaXorMHEB8c5gbtS39dtxTfsWbgUtH/Dj20rvVRRHOdgqqMl/e2ovxglpFBrJqarVVcPRGTHHkmO3RmXtYDdPqe5V8rmeHYLpgigkQsQ/5uDVqRNnc97obxw4bDUvBqCCCJqnRLDu6LLJmisk5GdLM4AD4PfN4E674chI929DWU/XUckOsB2Nd8G9lWbPyfHWZ8F/Fn3ohogQU4AwxbIZzp+MICNOVEvnLDAhw2gQji4f7xA39MS+aBq2Ws+/cWkN5kVyRNMPYwSdjEPMU2hLsoKHn9ZyWNNughDmJeCMXtoZavR1DMC90IntjGSxR4TpkfFo05sVq0xOnJcz+QS/uCeX+pzsFfQ5vJsM7SslNgWyvRXeDdE21qKzZ6NVTKq75nlzoJ8ZrwNLZasvyDFFHcU9hB+o7UpDpj++0aya/krdIsuafTGaAGh3+psoOK/QEEyQAMdCFOVF6LI5Pv6bRevw5nRWZI7JycxOS25iGyEWAGe88h4qRVPqaA4wFC2j52vWbufHPQdLKO1gnuWUJ/5ZUiIAZYIH0KCrp1Y9U27rJ87yjYJS83ePqwrXhUSulN+KvMY2IVsLWwdLTmtjXpGdHh1q3U9sYoOge3Dp3iqdGf921uPVFLfxxAPfRF2WUgOuMgvQmcbyJXUT+VdvB5wj0RPllCKIk4QNnV60/OlU3ATafN0VtYnyfk5Jw2i0PaQC24v8qzWhO2tOOLd3R7EWXujihNf1fkTf8o4Nr/sJDQKsD18oxayb0gmNjNKUm +/AcBJHSdDhKN4If3uC+yVgx153/h2oLBjPene6bpOgY +--- 4Tm64/hfaUnnYkfyQ1ewpynY0hlhVJKfDFUUYXC6AG4 +bΑ3'NhLL[R,1f \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_uppercase b/age/tests/testdata/testkit/hybrid_uppercase new file mode 100644 index 00000000..5a7ffecf --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_uppercase @@ -0,0 +1,10 @@ +expect: no match +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the first argument in the mlkem768x25519 stanza is uppercase + +age-encryption.org/v1 +-> MLKEM768X25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- 6MKi/lecrcOnE355MnEX88njSwsX8wzDxAi4S/akrcM +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/hybrid_x25519_arg b/age/tests/testdata/testkit/hybrid_x25519_arg new file mode 100644 index 00000000..996e7f62 --- /dev/null +++ b/age/tests/testdata/testkit/hybrid_x25519_arg @@ -0,0 +1,11 @@ +expect: header failure +file key: 59454c4c4f57205355424d4152494e45 +identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 +identity: AGE-SECRET-KEY-PQ-1HZLGZUPT4ETPKDEV8HSGFDCYZ4E522W0A7PU2LHT8EH9W6YLNC3SW78XKG +comment: the X25519 stanza has a hybrid enc + +age-encryption.org/v1 +-> X25519 NfLcgAbzvNgf0aRb4PANBvyDtIDDQKf84JhhFlnvT1NUAcbGNrArRZ/T+bc9l4xmK1DSl+PXk6nqqGBhaM1dUiT7X17TU1/b9haZZPzEalvZHFDMSevfiZshlnSgcpWh0qnpgTyboWTU+zbrH6YD2uhshbJoiuqh+PpXtDMstXx4CgxASrNVlfSl/caRTi24QjIXpCNwEE4FwHrmAwUqSHLUzGOHfiW/chOCTtDX591x41o6eZ4/Dt92VhoYKFcpiaWbRhbenZXUJxPS1C1sK84CVwkDH7LJjbYCnkxt3meul8kKWihZsStZYd/6bozqczOX7zN5PbaYD1XpYwMwedzWmPmQzxBybD8ZodcR7hF7WUxSmFVH7ExiYH3ZNbDAVcgGwlkTFmUmaTCbjGZTv8M+ejmvStQHgCtPi4GGRdJFr3HzvRzm6bvrdF/rdSPRFtRVM7D9o7ZAAquD1PByE0Y5YCR4/vmTlIlRwFXk4be+TsAI+Gotujou6nrigwnfqoGiNSvi/ZVvSKnDoZPIE5qwONCeeJB8CeRhqXEjvgtwc2zcWUBtjSJgNL+j887k2h0xlXpQqlmDDHsBOCP3/VoM+NZURqI+RoTudcwTl8TgmhGrQytnovuWFDvE9HgPAs67dPRC64/3RES3hF2C6/R1pQAnC7S2iSijJnyFlaDJWfcZvvNoamphDv63kUNb7b9V8E+xwqF8GkuXJnBFDo0PuJz8qWH7sDMEOhIInmanDiu6K/9jOCubYNdNcXrjQknlvk1kFdjZ2xYnC6QuqA5+qHMBpgrg301PX154X4KUcxA3PoFyYNexaBK3njks7PNxl7HyZIWjjDlz50WrQnEqBjl8RVVuneL3vgVyU45GSVWQVOH4K6aepWP0+p7WUD52rhdVH5HPZWQL3v8TjEz80Aeb87+1s1wgM+5skNX4LpOyuhRxQ/ChXsVZrVJ8SRsBlO4K+CJ271rHj8l8NEhRE2O/ZKHst8Be/6j5c9SUmRRqvI+6bcg0Bcq7Wi7d08vDQqjC2cOi112CGra5mazd/NCICC81oYkMmTxtM9ficfIhvt9nHaZSQh27tzJ3xRWOegwzDOaNjrtJ7yvCXbV9iQ6boiCl6wdmIn7k9sI30wIuHcVU1Cr+ENWhqVyRiAKktgvxegDnqvRB2n1aHKovp60Fs7YIDrclscRFikV45x0RNBdVtUkWD430vZgekkZdnwpeHxGV9TIe2FCNooQzUzx6v4ft0sZ5SYI490F2sYZu/sig4IB/KOzVfPXBX9dkftLgZTWtfP7GI9NjEitLn/lYTh2jfSKTYZSM+BQt16m2yg/4X7xftA2P3fSyU1zWineocz4DKyilWmVhPRjy9LrTPtQWVVNGrVfUsNYwWHJX6FwkF7JNbCqLEQueMjPhc9cnr66uF+Wt1IsuTj278MgyZqYlr7mkW91zyWJMSIXTKmqv5um9ypc3IJUmkvs67A91XA8vspARsUM4Jw +jYPfilNAMjF0zGRYMYJqR/cTTzbiGxQMhG+8zZaitic +--- 4xEwzZi8DlgfpbbRheEXM1EBtbw9b2O99QFT78xpGOE +roW= 1$!ox-yG^^ \ No newline at end of file diff --git a/age/tests/testdata/testkit/x25519_extra_argument b/age/tests/testdata/testkit/x25519_extra_argument index 251c3a6d..fca49cc7 100644 --- a/age/tests/testdata/testkit/x25519_extra_argument +++ b/age/tests/testdata/testkit/x25519_extra_argument @@ -1,7 +1,7 @@ expect: header failure file key: 59454c4c4f57205355424d4152494e45 identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 -comment: the base64 encoding of the share is not canonical +comment: the X25519 stanza has an unexpected extra argument age-encryption.org/v1 -> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCc 1234 diff --git a/age/tests/testdata/testkit/x25519_identity b/age/tests/testdata/testkit/x25519_identity index eb254e70..909c0341 100644 --- a/age/tests/testdata/testkit/x25519_identity +++ b/age/tests/testdata/testkit/x25519_identity @@ -1,7 +1,7 @@ expect: header failure file key: 59454c4c4f57205355424d4152494e45 identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 -comment: the X25519 share is a low-order point, so the shared secret is the disallowed all-zero value +comment: the X25519 share is the identity point, so the shared secretis the disallowed all-zero value age-encryption.org/v1 -> X25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA diff --git a/age/tests/testdata/testkit/x25519_long_share b/age/tests/testdata/testkit/x25519_long_share index a2f3a047..0e807a6b 100644 --- a/age/tests/testdata/testkit/x25519_long_share +++ b/age/tests/testdata/testkit/x25519_long_share @@ -1,7 +1,7 @@ expect: header failure file key: 59454c4c4f57205355424d4152494e45 identity: AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 -comment: a trailing zero is missing from the X25519 share +comment: an extra most-significant zero byte is appended to the X25519 share age-encryption.org/v1 -> X25519 TEiF0ypqr+bpvcqXNyCVJpL7OuwPdVwPL7KQEbFDOCcA diff --git a/age/tests/testkit.rs b/age/tests/testkit.rs index 4cf7fc54..26b4d499 100644 --- a/age/tests/testkit.rs +++ b/age/tests/testkit.rs @@ -10,7 +10,7 @@ use std::{ use age::{ DecryptError, Decryptor, Identity, armor::{ArmoredReadError, ArmoredReader}, - scrypt, + pq, scrypt, secrecy::SecretString, x25519, }; @@ -18,7 +18,6 @@ use futures::AsyncReadExt; use sha2::{Digest, Sha256}; use test_case::test_case; -#[test_case("armor")] #[test_case("armor_crlf")] #[test_case("armor_empty")] #[test_case("armor_empty_last_line")] @@ -31,6 +30,7 @@ use test_case::test_case; #[test_case("armor_garbage_trailing")] #[test_case("armor_header_crlf")] #[test_case("armor_headers")] +#[test_case("armor_hybrid")] #[test_case("armor_invalid_character_header")] #[test_case("armor_invalid_character_payload")] #[test_case("armor_long_line")] @@ -41,6 +41,7 @@ use test_case::test_case; #[test_case("armor_no_padding")] #[test_case("armor_not_canonical")] #[test_case("armor_pgp_checksum")] +#[test_case("armor_scrypt")] #[test_case("armor_short_line")] #[test_case("armor_whitespace_begin")] #[test_case("armor_whitespace_end")] @@ -49,6 +50,7 @@ use test_case::test_case; #[test_case("armor_whitespace_line_start")] #[test_case("armor_whitespace_outside")] #[test_case("armor_wrong_type")] +#[test_case("armor_x25519")] #[test_case("empty")] #[test_case("header_crlf")] #[test_case("hmac_bad")] @@ -59,6 +61,24 @@ use test_case::test_case; #[test_case("hmac_not_canonical")] #[test_case("hmac_trailing_space")] #[test_case("hmac_truncated")] +#[test_case("hybrid")] +#[test_case("hybrid_and_x25519")] +#[test_case("hybrid_bad_tag")] +#[test_case("hybrid_currupted_enc_mlkem")] +#[test_case("hybrid_currupted_enc_x25519")] +#[test_case("hybrid_extra_argument")] +#[test_case("hybrid_grease")] +#[test_case("hybrid_identity")] +#[test_case("hybrid_long_file_key")] +#[test_case("hybrid_long_share")] +#[test_case("hybrid_low_order")] +#[test_case("hybrid_multiple_recipients")] +#[test_case("hybrid_no_match")] +#[test_case("hybrid_not_canonical_body")] +#[test_case("hybrid_not_canonical_enc")] +#[test_case("hybrid_short_share")] +#[test_case("hybrid_uppercase")] +#[test_case("hybrid_x25519_arg")] #[test_case("scrypt")] #[test_case("scrypt_and_x25519")] #[test_case("scrypt_bad_tag")] @@ -148,7 +168,7 @@ fn testkit(filename: &str) { match Decryptor::new(ArmoredReader::new(&testfile.age_file[..])).and_then(|d| { if !d.is_scrypt() { let identities = get_testkit_identities(filename, &testfile); - d.decrypt(identities.iter().map(|i| i as &dyn Identity)) + d.decrypt(identities.iter().map(|i| i.as_ref() as _)) } else { let passphrase = get_testkit_passphrase(&testfile, &comment); let mut identity = scrypt::Identity::new(passphrase); @@ -165,7 +185,6 @@ fn testkit(filename: &str) { } } -#[test_case("armor")] #[test_case("armor_crlf")] #[test_case("armor_empty")] #[test_case("armor_empty_last_line")] @@ -178,6 +197,7 @@ fn testkit(filename: &str) { #[test_case("armor_garbage_trailing")] #[test_case("armor_header_crlf")] #[test_case("armor_headers")] +#[test_case("armor_hybrid")] #[test_case("armor_invalid_character_header")] #[test_case("armor_invalid_character_payload")] #[test_case("armor_long_line")] @@ -188,6 +208,7 @@ fn testkit(filename: &str) { #[test_case("armor_no_padding")] #[test_case("armor_not_canonical")] #[test_case("armor_pgp_checksum")] +#[test_case("armor_scrypt")] #[test_case("armor_short_line")] #[test_case("armor_whitespace_begin")] #[test_case("armor_whitespace_end")] @@ -196,6 +217,7 @@ fn testkit(filename: &str) { #[test_case("armor_whitespace_line_start")] #[test_case("armor_whitespace_outside")] #[test_case("armor_wrong_type")] +#[test_case("armor_x25519")] #[test_case("empty")] #[test_case("header_crlf")] #[test_case("hmac_bad")] @@ -206,6 +228,24 @@ fn testkit(filename: &str) { #[test_case("hmac_not_canonical")] #[test_case("hmac_trailing_space")] #[test_case("hmac_truncated")] +#[test_case("hybrid")] +#[test_case("hybrid_and_x25519")] +#[test_case("hybrid_bad_tag")] +#[test_case("hybrid_currupted_enc_mlkem")] +#[test_case("hybrid_currupted_enc_x25519")] +#[test_case("hybrid_extra_argument")] +#[test_case("hybrid_grease")] +#[test_case("hybrid_identity")] +#[test_case("hybrid_long_file_key")] +#[test_case("hybrid_long_share")] +#[test_case("hybrid_low_order")] +#[test_case("hybrid_multiple_recipients")] +#[test_case("hybrid_no_match")] +#[test_case("hybrid_not_canonical_body")] +#[test_case("hybrid_not_canonical_enc")] +#[test_case("hybrid_short_share")] +#[test_case("hybrid_uppercase")] +#[test_case("hybrid_x25519_arg")] #[test_case("scrypt")] #[test_case("scrypt_and_x25519")] #[test_case("scrypt_bad_tag")] @@ -295,7 +335,7 @@ fn testkit_buffered(filename: &str) { match Decryptor::new_buffered(ArmoredReader::new(&testfile.age_file[..])).and_then(|d| { if !d.is_scrypt() { let identities = get_testkit_identities(filename, &testfile); - d.decrypt(identities.iter().map(|i| i as &dyn Identity)) + d.decrypt(identities.iter().map(|i| i.as_ref() as _)) } else { let passphrase = get_testkit_passphrase(&testfile, &comment); let mut identity = scrypt::Identity::new(passphrase); @@ -312,7 +352,6 @@ fn testkit_buffered(filename: &str) { } } -#[test_case("armor")] #[test_case("armor_crlf")] #[test_case("armor_empty")] #[test_case("armor_empty_last_line")] @@ -325,6 +364,7 @@ fn testkit_buffered(filename: &str) { #[test_case("armor_garbage_trailing")] #[test_case("armor_header_crlf")] #[test_case("armor_headers")] +#[test_case("armor_hybrid")] #[test_case("armor_invalid_character_header")] #[test_case("armor_invalid_character_payload")] #[test_case("armor_long_line")] @@ -335,6 +375,7 @@ fn testkit_buffered(filename: &str) { #[test_case("armor_no_padding")] #[test_case("armor_not_canonical")] #[test_case("armor_pgp_checksum")] +#[test_case("armor_scrypt")] #[test_case("armor_short_line")] #[test_case("armor_whitespace_begin")] #[test_case("armor_whitespace_end")] @@ -343,6 +384,7 @@ fn testkit_buffered(filename: &str) { #[test_case("armor_whitespace_line_start")] #[test_case("armor_whitespace_outside")] #[test_case("armor_wrong_type")] +#[test_case("armor_x25519")] #[test_case("empty")] #[test_case("header_crlf")] #[test_case("hmac_bad")] @@ -353,6 +395,24 @@ fn testkit_buffered(filename: &str) { #[test_case("hmac_not_canonical")] #[test_case("hmac_trailing_space")] #[test_case("hmac_truncated")] +#[test_case("hybrid")] +#[test_case("hybrid_and_x25519")] +#[test_case("hybrid_bad_tag")] +#[test_case("hybrid_currupted_enc_mlkem")] +#[test_case("hybrid_currupted_enc_x25519")] +#[test_case("hybrid_extra_argument")] +#[test_case("hybrid_grease")] +#[test_case("hybrid_identity")] +#[test_case("hybrid_long_file_key")] +#[test_case("hybrid_long_share")] +#[test_case("hybrid_low_order")] +#[test_case("hybrid_multiple_recipients")] +#[test_case("hybrid_no_match")] +#[test_case("hybrid_not_canonical_body")] +#[test_case("hybrid_not_canonical_enc")] +#[test_case("hybrid_short_share")] +#[test_case("hybrid_uppercase")] +#[test_case("hybrid_x25519_arg")] #[test_case("scrypt")] #[test_case("scrypt_and_x25519")] #[test_case("scrypt_bad_tag")] @@ -445,7 +505,7 @@ async fn testkit_async(filename: &str) { .and_then(|d| { if !d.is_scrypt() { let identities = get_testkit_identities(filename, &testfile); - d.decrypt_async(identities.iter().map(|i| i as &dyn Identity)) + d.decrypt_async(identities.iter().map(|i| i.as_ref() as _)) } else { let passphrase = get_testkit_passphrase(&testfile, &comment); let mut identity = scrypt::Identity::new(passphrase); @@ -462,7 +522,6 @@ async fn testkit_async(filename: &str) { } } -#[test_case("armor")] #[test_case("armor_crlf")] #[test_case("armor_empty")] #[test_case("armor_empty_last_line")] @@ -475,6 +534,7 @@ async fn testkit_async(filename: &str) { #[test_case("armor_garbage_trailing")] #[test_case("armor_header_crlf")] #[test_case("armor_headers")] +#[test_case("armor_hybrid")] #[test_case("armor_invalid_character_header")] #[test_case("armor_invalid_character_payload")] #[test_case("armor_long_line")] @@ -485,6 +545,7 @@ async fn testkit_async(filename: &str) { #[test_case("armor_no_padding")] #[test_case("armor_not_canonical")] #[test_case("armor_pgp_checksum")] +#[test_case("armor_scrypt")] #[test_case("armor_short_line")] #[test_case("armor_whitespace_begin")] #[test_case("armor_whitespace_end")] @@ -493,6 +554,7 @@ async fn testkit_async(filename: &str) { #[test_case("armor_whitespace_line_start")] #[test_case("armor_whitespace_outside")] #[test_case("armor_wrong_type")] +#[test_case("armor_x25519")] #[test_case("empty")] #[test_case("header_crlf")] #[test_case("hmac_bad")] @@ -503,6 +565,24 @@ async fn testkit_async(filename: &str) { #[test_case("hmac_not_canonical")] #[test_case("hmac_trailing_space")] #[test_case("hmac_truncated")] +#[test_case("hybrid")] +#[test_case("hybrid_and_x25519")] +#[test_case("hybrid_bad_tag")] +#[test_case("hybrid_currupted_enc_mlkem")] +#[test_case("hybrid_currupted_enc_x25519")] +#[test_case("hybrid_extra_argument")] +#[test_case("hybrid_grease")] +#[test_case("hybrid_identity")] +#[test_case("hybrid_long_file_key")] +#[test_case("hybrid_long_share")] +#[test_case("hybrid_low_order")] +#[test_case("hybrid_multiple_recipients")] +#[test_case("hybrid_no_match")] +#[test_case("hybrid_not_canonical_body")] +#[test_case("hybrid_not_canonical_enc")] +#[test_case("hybrid_short_share")] +#[test_case("hybrid_uppercase")] +#[test_case("hybrid_x25519_arg")] #[test_case("scrypt")] #[test_case("scrypt_and_x25519")] #[test_case("scrypt_bad_tag")] @@ -595,7 +675,7 @@ async fn testkit_async_buffered(filename: &str) { .and_then(|d| { if !d.is_scrypt() { let identities = get_testkit_identities(filename, &testfile); - d.decrypt_async(identities.iter().map(|i| i as &dyn Identity)) + d.decrypt_async(identities.iter().map(|i| i.as_ref() as _)) } else { let passphrase = get_testkit_passphrase(&testfile, &comment); let mut identity = scrypt::Identity::new(passphrase); @@ -620,7 +700,7 @@ fn format_testkit_comment(testfile: &TestFile) -> String { .unwrap_or_default() } -fn get_testkit_identities(filename: &str, testfile: &TestFile) -> Vec { +fn get_testkit_identities(filename: &str, testfile: &TestFile) -> Vec> { assert_eq!( testfile.passphrases.len(), // `scrypt_uppercase` uses the stanza tag `Scrypt` instead of `scrypt`, so @@ -632,7 +712,16 @@ fn get_testkit_identities(filename: &str, testfile: &TestFile) -> Vec) + .or_else(|_| { + pq::Identity::from_str(s) + .map(Box::new) + .map(|b| b as Box) + }) + }) .collect::>() .unwrap() }