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
8 changes: 4 additions & 4 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@ log = "0.4"

[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }

[patch.crates-io]
hpke = { git = "https://github.com/str4d/rust-hpke.git", rev = "1268205e6bf8080e8139da466fe834a23e954260" }
x-wing = { git = "https://github.com/str4d/RustCrypto-KEMs.git", rev = "f5dd74e238bfebc0b36754923ccc6157d75641e6" }
32 changes: 28 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
# rage: Rust implementation of age

rage is a simple, modern, and secure file encryption tool, using the *age*
format. It features small explicit keys, no config options, and UNIX-style
composability.
format. It features small explicit keys, post-quantum support, no config options,
and UNIX-style composability.

The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1).
age was designed by [@Benjojo](https://benjojo.co.uk/) and
Expand Down Expand Up @@ -112,6 +112,30 @@ $ rage -R recipients.txt example.jpg > example.jpg.age

If the argument to `-R` (or `-i`) is `-`, the file is read from standard input.

### Post-quantum keys

`rage-keygen` generates hybrid post-quantum keys by default, which are secure
against future quantum computer attacks. You can also request this explicitly
with the `--pq` flag. To generate classic keys, use `rage-keygen --pq=false`.

Post-quantum identities start with `AGE-SECRET-KEY-PQ-1...` and recipients with
`age1pq1...`. The recipients are unfortunately ~2000 characters long.

```
$ rage-keygen -o key.txt
$ rage-keygen -y key.txt > recipient.txt
$ rage -R recipient.txt example.jpg > example.jpg.age
$ rage -d -i key.txt example.jpg.age > example.jpg
```

Support for post-quantum keys is built into rage v0.13.0 and later. Alternatively,
the [`age-plugin-pq`] binary can be installed and placed in `$PATH` to add support
to any version and implementation of age that supports plugins. Recipients will
work out of the box, while identities will have to be converted to plugin
identities with `age-plugin-pq -identity`.

[`age-plugin-pq`]: https://github.com/FiloSottile/age/tree/main/extra/age-plugin-pq

### Passphrases

Files can be encrypted with a passphrase by using `-p/--passphrase`. By default
Expand Down Expand Up @@ -139,11 +163,11 @@ file, it will be automatically decrypted.

```
$ rage -p -o key.age <(rage-keygen)
Public key: age1pymw5hyr39qyuc950tget63aq8vfd52dclj8x7xhm08g6ad86dkserumnz
Public key: age1pq1pd[... 1950 more characters ...]
Type passphrase (leave empty to autogenerate a secure one): [hidden]
Using an autogenerated passphrase:
flash-bean-celery-network-curious-flower-salt-amateur-fence-giant
$ rage -r age1pymw5hyr39qyuc950tget63aq8vfd52dclj8x7xhm08g6ad86dkserumnz secrets.txt > secrets.txt.age
$ rage -r age1pq1pd[...] secrets.txt > secrets.txt.age
$ rage -d -i key.age secrets.txt.age > secrets.txt
Type passphrase: [hidden]
```
Expand Down
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
Loading
Loading