mirror of
https://github.com/str4d/rage.git
synced 2025-04-04 03:17:42 +03:00
Compare commits
105 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d7c727aef9 | ||
|
0780882307 | ||
|
a82a76a849 | ||
|
383b6f52aa | ||
|
741de973ee | ||
|
17446612f8 | ||
|
d35d442f91 | ||
|
e3a5c5fe8c | ||
|
25e050362c | ||
|
597f1aa05f | ||
|
ae5a392925 | ||
|
1d2b3bfa37 | ||
|
bca6916bac | ||
|
d0889c90af | ||
|
93fa28ad78 | ||
|
a59f0479d0 | ||
|
baf277a749 | ||
|
e8f14448e4 | ||
|
5bae3f1eae | ||
|
bdec23fe8d | ||
|
a5661495f6 | ||
|
504d784931 | ||
|
deb59935c1 | ||
|
5237281929 | ||
|
5955e489b7 | ||
|
53d018a9c2 | ||
|
195b86b6bc | ||
|
9af479086b | ||
|
9ab26bf360 | ||
|
8b0b65e98a | ||
|
9343af9324 | ||
|
05f996c919 | ||
|
e67f4016dc | ||
|
0cdde60315 | ||
|
917fc489f6 | ||
|
c5f0d61400 | ||
|
a709c93c92 | ||
|
51760e34ba | ||
|
d483e0b069 | ||
|
5eb44a157e | ||
|
b179b7cedd | ||
|
67a539791b | ||
|
e84159365d | ||
|
84eacb7271 | ||
|
5a57e120a2 | ||
|
303fa6ebe1 | ||
|
d76c85d585 | ||
|
5e57ef07ca | ||
|
f243d63c31 | ||
|
ae2434216d | ||
|
8dcdacc1ac | ||
|
52fd675bbd | ||
|
2f9cf3f86f | ||
|
d31fb568b7 | ||
|
5086bd65d9 | ||
|
5e88d75195 | ||
|
8688929723 | ||
|
cb36c4cd53 | ||
|
f64f110f3e | ||
|
dc885d86a1 | ||
|
cf96347fbe | ||
|
7e3c62b98b | ||
|
0cf17d916a | ||
|
3c9483f78f | ||
|
9476af8e1f | ||
|
2d29668712 | ||
|
8f1d6af149 | ||
|
8091015514 | ||
|
26ebfbfc88 | ||
|
daf0829142 | ||
|
4ff5e01ae9 | ||
|
d2c2e895bf | ||
|
2eec45718c | ||
|
2f79c8201b | ||
|
18b27b377d | ||
|
0689e95927 | ||
|
a510e76bf8 | ||
|
f69c29bf6f | ||
|
944f56a4a9 | ||
|
219ac41b60 | ||
|
a1f16094b8 | ||
|
f253ff2ff1 | ||
|
4ba982254c | ||
|
0c2acd5306 | ||
|
e568a640ba | ||
|
6b46ada5e8 | ||
|
d4eb811ef9 | ||
|
ef5112fedd | ||
|
3aa3fca8a7 | ||
|
e47cf49b3e | ||
|
a552210939 | ||
|
6d8d1515fc | ||
|
b22b60ff7f | ||
|
b9de00a29a | ||
|
67ee02b47e | ||
|
0e4d3e1163 | ||
|
f93244565f | ||
|
ce3aa6dc9f | ||
|
818e5e5e1a | ||
|
fec672d5c7 | ||
|
96f89b3400 | ||
|
058c56d6b2 | ||
|
daac4c9a9a | ||
|
948adca150 | ||
|
b17c81008b |
92 changed files with 5651 additions and 3182 deletions
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
@ -70,6 +70,9 @@ jobs:
|
|||
codecov:
|
||||
name: Code coverage
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: xd009642/tarpaulin:develop-nightly
|
||||
options: --security-opt seccomp=unconfined
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -77,14 +80,18 @@ jobs:
|
|||
id: toolchain
|
||||
- run: rustup override set ${{steps.toolchain.outputs.name}}
|
||||
- name: Install linux build dependencies
|
||||
run: sudo apt update && sudo apt install libfuse-dev
|
||||
run: apt update && apt -y install libfuse-dev
|
||||
- name: Generate coverage report
|
||||
uses: actions-rs/tarpaulin@v0.1
|
||||
with:
|
||||
version: '0.19.1'
|
||||
args: --workspace --release --all-features --timeout 180 --out Xml
|
||||
run: >
|
||||
cargo tarpaulin
|
||||
--engine llvm
|
||||
--workspace
|
||||
--release
|
||||
--all-features
|
||||
--timeout 180
|
||||
--out xml
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4.0.1
|
||||
uses: codecov/codecov-action@v4.6.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
53
.github/workflows/release.yml
vendored
53
.github/workflows/release.yml
vendored
|
@ -11,13 +11,25 @@ on:
|
|||
required: true
|
||||
default: 'true'
|
||||
|
||||
permissions:
|
||||
attestations: write
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Publish for ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
name: [linux, armv7, arm64, windows, macos]
|
||||
name:
|
||||
- linux
|
||||
- armv7
|
||||
- arm64
|
||||
- windows
|
||||
- macos-arm64
|
||||
- macos-x86_64
|
||||
|
||||
include:
|
||||
- name: linux
|
||||
os: ubuntu-20.04
|
||||
|
@ -56,9 +68,14 @@ jobs:
|
|||
archive_name: rage.zip
|
||||
asset_suffix: x86_64-windows.zip
|
||||
|
||||
- name: macos
|
||||
- name: macos-arm64
|
||||
os: macos-latest
|
||||
archive_name: rage.tar.gz
|
||||
asset_suffix: arm64-darwin.tar.gz
|
||||
|
||||
- name: macos-x86_64
|
||||
os: macos-13
|
||||
archive_name: rage.tar.gz
|
||||
asset_suffix: x86_64-darwin.tar.gz
|
||||
|
||||
steps:
|
||||
|
@ -101,6 +118,11 @@ jobs:
|
|||
shell: bash
|
||||
if: matrix.name == 'windows'
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: 'release/rage/*'
|
||||
|
||||
- name: Upload archive as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
@ -109,7 +131,7 @@ jobs:
|
|||
if: github.event.inputs.test == 'true'
|
||||
|
||||
- name: Upload archive to release
|
||||
uses: svenstaro/upload-release-action@2.7.0
|
||||
uses: svenstaro/upload-release-action@2.9.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: ${{ matrix.archive_name }}
|
||||
|
@ -129,11 +151,12 @@ jobs:
|
|||
os:
|
||||
- ubuntu-20.04
|
||||
- ubuntu-22.04
|
||||
- ubuntu-24.04
|
||||
- windows-2019
|
||||
- windows-2022
|
||||
- macos-11
|
||||
- macos-12
|
||||
- macos-13
|
||||
- macos-14
|
||||
|
||||
include:
|
||||
- os: ubuntu-20.04
|
||||
|
@ -146,6 +169,11 @@ jobs:
|
|||
archive_name: rage.tar.gz
|
||||
asset_suffix: x86_64-linux.tar.gz
|
||||
|
||||
- os: ubuntu-24.04
|
||||
name: linux
|
||||
archive_name: rage.tar.gz
|
||||
asset_suffix: x86_64-linux.tar.gz
|
||||
|
||||
- os: windows-2019
|
||||
name: windows
|
||||
archive_name: rage.zip
|
||||
|
@ -156,11 +184,6 @@ jobs:
|
|||
archive_name: rage.zip
|
||||
asset_suffix: x86_64-windows.zip
|
||||
|
||||
- os: macos-11
|
||||
name: macos
|
||||
archive_name: rage.tar.gz
|
||||
asset_suffix: x86_64-darwin.tar.gz
|
||||
|
||||
- os: macos-12
|
||||
name: macos
|
||||
archive_name: rage.tar.gz
|
||||
|
@ -171,6 +194,11 @@ jobs:
|
|||
archive_name: rage.tar.gz
|
||||
asset_suffix: x86_64-darwin.tar.gz
|
||||
|
||||
- os: macos-14
|
||||
name: macos
|
||||
archive_name: rage.tar.gz
|
||||
asset_suffix: arm64-darwin.tar.gz
|
||||
|
||||
steps:
|
||||
- name: Download archive
|
||||
uses: actions/download-artifact@v4
|
||||
|
@ -281,6 +309,11 @@ jobs:
|
|||
- name: cargo deb
|
||||
run: cargo deb --package rage --no-build --target ${{ matrix.target }} ${{ matrix.deb_flags }}
|
||||
|
||||
- name: Generate artifact attestation
|
||||
uses: actions/attest-build-provenance@v1
|
||||
with:
|
||||
subject-path: 'target/${{ matrix.target }}/debian/*.deb'
|
||||
|
||||
- name: Upload Debian package as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
@ -289,7 +322,7 @@ jobs:
|
|||
if: github.event.inputs.test == 'true'
|
||||
|
||||
- name: Upload Debian package to release
|
||||
uses: svenstaro/upload-release-action@2.7.0
|
||||
uses: svenstaro/upload-release-action@2.9.0
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
file: target/${{ matrix.target }}/debian/*.deb
|
||||
|
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.server.extraEnv": { "RUSTUP_TOOLCHAIN": "stable" }
|
||||
}
|
833
Cargo.lock
generated
833
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
12
Cargo.toml
12
Cargo.toml
|
@ -15,8 +15,8 @@ repository = "https://github.com/str4d/rage"
|
|||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
age = { version = "0.10.1", path = "age" }
|
||||
age-core = { version = "0.10.0", path = "age-core" }
|
||||
age = { version = "0.11.1", path = "age" }
|
||||
age-core = { version = "0.11.0", path = "age-core" }
|
||||
|
||||
# Dependencies required by the age specification:
|
||||
# - Base64 from RFC 4648
|
||||
|
@ -48,14 +48,14 @@ cookie-factory = "0.3.1"
|
|||
nom = { version = "7", default-features = false, features = ["alloc"] }
|
||||
|
||||
# Secret management
|
||||
pinentry = "0.5"
|
||||
secrecy = "0.8"
|
||||
pinentry = "0.6"
|
||||
secrecy = "0.10"
|
||||
subtle = "2"
|
||||
zeroize = "1"
|
||||
|
||||
# Localization
|
||||
i18n-embed = { version = "0.14", features = ["fluent-system"] }
|
||||
i18n-embed-fl = "0.7"
|
||||
i18n-embed = { version = "0.15", features = ["fluent-system"] }
|
||||
i18n-embed-fl = "0.9"
|
||||
lazy_static = "1"
|
||||
rust-embed = "8"
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
class Rage < Formula
|
||||
desc "[BETA] A simple, secure, and modern encryption tool."
|
||||
homepage "https://str4d.xyz/rage"
|
||||
url "https://github.com/str4d/rage/archive/refs/tags/v0.9.2.tar.gz"
|
||||
sha256 "3bd287372eb6226b246459c1b5c39ecdb36b3495d7af4d2bee93bb3aad9ccf65"
|
||||
version "0.9.2"
|
||||
|
||||
depends_on "rust" => :build
|
||||
|
||||
def install
|
||||
system "cargo", "install", *std_cargo_args(path: './rage')
|
||||
end
|
||||
|
||||
test do
|
||||
# Test key generation
|
||||
system "#{bin}/rage-keygen -o #{testpath}/output.txt"
|
||||
assert_predicate testpath/"output.txt", :exist?
|
||||
|
||||
# Test encryption
|
||||
(testpath/"test.txt").write("Hello World!\n")
|
||||
system "#{bin}/rage -r age1y8m84r6pwd4da5d45zzk03rlgv2xr7fn9px80suw3psrahul44ashl0usm -o #{testpath}/test.txt.age #{testpath}/test.txt"
|
||||
assert_predicate testpath/"test.txt.age", :exist?
|
||||
assert File.read(testpath/"test.txt.age").start_with?("age-encryption.org")
|
||||
|
||||
# Test decryption
|
||||
(testpath/"test.key").write("AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX")
|
||||
assert_equal "Hello World!", shell_output("#{bin}/rage -d -i #{testpath}/test.key #{testpath}/test.txt.age").strip
|
||||
end
|
||||
end
|
|
@ -7,8 +7,8 @@ format. It features small explicit keys, 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 [@Benjojo12](https://twitter.com/Benjojo12) and
|
||||
[@FiloSottile](https://twitter.com/FiloSottile).
|
||||
age was designed by [@Benjojo](https://benjojo.co.uk/) and
|
||||
[@FiloSottile](https://bsky.app/profile/did:plc:x2nsupeeo52oznrmplwapppl).
|
||||
|
||||
The reference interoperable Go implementation is available at
|
||||
[filippo.io/age](https://filippo.io/age).
|
||||
|
@ -24,7 +24,8 @@ For more plugins, implementations, tools, and integrations, check out the
|
|||
| Environment | CLI command |
|
||||
|-------------|-------------|
|
||||
| Cargo (Rust 1.65+) | `cargo install rage` |
|
||||
| Homebrew (macOS or Linux) | `brew tap str4d.xyz/rage https://str4d.xyz/rage`<br>`brew install rage` |
|
||||
| Homebrew (macOS or Linux) | `brew install rage` |
|
||||
| MacPorts | `port install rage` |
|
||||
| Alpine Linux (edge) | `apk add rage` |
|
||||
| Arch Linux | `pacman -S rage-encryption` |
|
||||
| Debian | [Debian packages](https://github.com/str4d/rage/releases) |
|
||||
|
|
|
@ -8,6 +8,19 @@ to 1.0.0 are beta releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.11.0] - 2024-11-03
|
||||
### Added
|
||||
- `age_core::format`:
|
||||
- `FileKey::new`
|
||||
- `FileKey::init_with_mut`
|
||||
- `FileKey::try_init_with_mut`
|
||||
- `is_arbitrary_string`
|
||||
|
||||
### Changed
|
||||
- Migrated to `secrecy 0.10`.
|
||||
- `age::plugin::Connection::unidir_receive` now takes an additional argument to
|
||||
enable handling an optional fourth command.
|
||||
|
||||
## [0.10.0] - 2024-02-04
|
||||
### Added
|
||||
- `impl Eq for age_core::format::Stanza`
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "age-core"
|
||||
description = "[BETA] Common functions used across the age crates"
|
||||
version = "0.10.0"
|
||||
version = "0.11.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
|
|
@ -5,7 +5,7 @@ use rand::{
|
|||
distributions::{Distribution, Uniform},
|
||||
thread_rng, RngCore,
|
||||
};
|
||||
use secrecy::{ExposeSecret, Secret};
|
||||
use secrecy::{ExposeSecret, ExposeSecretMut, SecretBox};
|
||||
|
||||
/// The prefix identifying an age stanza.
|
||||
const STANZA_TAG: &str = "-> ";
|
||||
|
@ -14,11 +14,26 @@ const STANZA_TAG: &str = "-> ";
|
|||
pub const FILE_KEY_BYTES: usize = 16;
|
||||
|
||||
/// A file key for encrypting or decrypting an age file.
|
||||
pub struct FileKey(Secret<[u8; FILE_KEY_BYTES]>);
|
||||
pub struct FileKey(SecretBox<[u8; FILE_KEY_BYTES]>);
|
||||
|
||||
impl From<[u8; FILE_KEY_BYTES]> for FileKey {
|
||||
fn from(file_key: [u8; FILE_KEY_BYTES]) -> Self {
|
||||
FileKey(Secret::new(file_key))
|
||||
impl FileKey {
|
||||
/// Creates a file key using a pre-boxed key.
|
||||
pub fn new(file_key: Box<[u8; FILE_KEY_BYTES]>) -> Self {
|
||||
Self(SecretBox::new(file_key))
|
||||
}
|
||||
|
||||
/// Creates a file key using a function that can initialize the key in-place.
|
||||
pub fn init_with_mut(ctr: impl FnOnce(&mut [u8; FILE_KEY_BYTES])) -> Self {
|
||||
Self(SecretBox::init_with_mut(ctr))
|
||||
}
|
||||
|
||||
/// Same as [`Self::init_with_mut`], but the constructor can be fallible.
|
||||
pub fn try_init_with_mut<E>(
|
||||
ctr: impl FnOnce(&mut [u8; FILE_KEY_BYTES]) -> Result<(), E>,
|
||||
) -> Result<Self, E> {
|
||||
let mut file_key = SecretBox::new(Box::new([0; FILE_KEY_BYTES]));
|
||||
ctr(file_key.expose_secret_mut())?;
|
||||
Ok(Self(file_key))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,6 +105,16 @@ impl From<AgeStanza<'_>> for Stanza {
|
|||
}
|
||||
}
|
||||
|
||||
/// Checks whether the string is a valid age "arbitrary string" (`1*VCHAR` in ABNF).
|
||||
pub fn is_arbitrary_string<S: AsRef<str>>(s: &S) -> bool {
|
||||
let s = s.as_ref();
|
||||
!s.is_empty()
|
||||
&& s.chars().all(|c| match u8::try_from(c) {
|
||||
Ok(u) => (33..=126).contains(&u),
|
||||
Err(_) => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a random recipient stanza that exercises the joint in the age v1 format.
|
||||
///
|
||||
/// This function is guaranteed to return a valid stanza, but makes no other guarantees
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
//! implementations built around the `age-plugin` crate.
|
||||
|
||||
use rand::{thread_rng, Rng};
|
||||
use secrecy::Zeroize;
|
||||
use secrecy::zeroize::Zeroize;
|
||||
use std::env;
|
||||
use std::fmt;
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
|
@ -51,10 +51,11 @@ impl std::error::Error for Error {}
|
|||
/// should explicitly handle.
|
||||
pub type Result<T> = io::Result<std::result::Result<T, Error>>;
|
||||
|
||||
type UnidirResult<A, B, C, E> = io::Result<(
|
||||
type UnidirResult<A, B, C, D, E> = io::Result<(
|
||||
std::result::Result<Vec<A>, Vec<E>>,
|
||||
std::result::Result<Vec<B>, Vec<E>>,
|
||||
Option<std::result::Result<Vec<C>, Vec<E>>>,
|
||||
Option<std::result::Result<Vec<D>, Vec<E>>>,
|
||||
)>;
|
||||
|
||||
/// A connection to a plugin binary.
|
||||
|
@ -205,23 +206,26 @@ impl<R: Read, W: Write> Connection<R, W> {
|
|||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// `command_a`, `command_b`, and (optionally) `command_c` are the known commands that
|
||||
/// are expected to be received. All other received commands (including grease) will
|
||||
/// be ignored.
|
||||
pub fn unidir_receive<A, B, C, E, F, G, H>(
|
||||
/// `command_a`, `command_b`, and (optionally) `command_c` and `command_d` are the
|
||||
/// known commands that are expected to be received. All other received commands
|
||||
/// (including grease) will be ignored.
|
||||
pub fn unidir_receive<A, B, C, D, E, F, G, H, I>(
|
||||
&mut self,
|
||||
command_a: (&str, F),
|
||||
command_b: (&str, G),
|
||||
command_c: (Option<&str>, H),
|
||||
) -> UnidirResult<A, B, C, E>
|
||||
command_d: (Option<&str>, I),
|
||||
) -> UnidirResult<A, B, C, D, E>
|
||||
where
|
||||
F: Fn(Stanza) -> std::result::Result<A, E>,
|
||||
G: Fn(Stanza) -> std::result::Result<B, E>,
|
||||
H: Fn(Stanza) -> std::result::Result<C, E>,
|
||||
I: Fn(Stanza) -> std::result::Result<D, E>,
|
||||
{
|
||||
let mut res_a = Ok(vec![]);
|
||||
let mut res_b = Ok(vec![]);
|
||||
let mut res_c = Ok(vec![]);
|
||||
let mut res_d = Ok(vec![]);
|
||||
|
||||
for stanza in iter::repeat_with(|| self.receive()).take_while(|res| match res {
|
||||
Ok(stanza) => stanza.tag != COMMAND_DONE,
|
||||
|
@ -251,14 +255,28 @@ impl<R: Read, W: Write> Connection<R, W> {
|
|||
validate(command_a.1(stanza), &mut res_a)
|
||||
} else if stanza.tag.as_str() == command_b.0 {
|
||||
validate(command_b.1(stanza), &mut res_b)
|
||||
} else if let Some(tag) = command_c.0 {
|
||||
if stanza.tag.as_str() == tag {
|
||||
validate(command_c.1(stanza), &mut res_c)
|
||||
} else {
|
||||
if let Some(tag) = command_c.0 {
|
||||
if stanza.tag.as_str() == tag {
|
||||
validate(command_c.1(stanza), &mut res_c);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(tag) = command_d.0 {
|
||||
if stanza.tag.as_str() == tag {
|
||||
validate(command_d.1(stanza), &mut res_d);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((res_a, res_b, command_c.0.map(|_| res_c)))
|
||||
Ok((
|
||||
res_a,
|
||||
res_b,
|
||||
command_c.0.map(|_| res_c),
|
||||
command_d.0.map(|_| res_d),
|
||||
))
|
||||
}
|
||||
|
||||
/// Runs a bidirectional phase as the controller.
|
||||
|
@ -481,10 +499,11 @@ mod tests {
|
|||
.unidir_send(|mut phase| phase.send("test", &["foo"], b"bar"))
|
||||
.unwrap();
|
||||
let stanza = plugin_conn
|
||||
.unidir_receive::<_, (), (), _, _, _, _>(
|
||||
.unidir_receive::<_, (), (), (), _, _, _, _, _>(
|
||||
("test", Ok),
|
||||
("other", |_| Err(())),
|
||||
(None, |_| Ok(())),
|
||||
(None, |_| Ok(())),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -496,7 +515,8 @@ mod tests {
|
|||
body: b"bar"[..].to_owned()
|
||||
}]),
|
||||
Ok(vec![]),
|
||||
None
|
||||
None,
|
||||
None,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,26 @@ to 1.0.0 are beta releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.0] - 2024-11-03
|
||||
### Added
|
||||
- `age_plugin::PluginHandler`
|
||||
- `impl age_plugin::identity::IdentityPluginV1 for std::convert::Infallible`
|
||||
- `impl age_plugin::recipient::RecipientPluginV1 for std::convert::Infallible`
|
||||
|
||||
### Changed
|
||||
- Migrated to `age-core 0.11`.
|
||||
- `age_plugin::recipient::RecipientPluginV1` has a new `labels` method. Existing
|
||||
implementations of the trait should either return `HashSet::new()` to maintain
|
||||
existing compatibility, or return labels that apply the desired constraints.
|
||||
- `age_plugin::run_state_machine` now supports the `recipient-v1` labels
|
||||
extension.
|
||||
|
||||
### Fixed
|
||||
- `age_plugin::run_state_machine` now takes an `impl age_plugin::PluginHandler`
|
||||
argument, instead of its previous arguments.
|
||||
- This fixes the change from the previous release, because the type parameters
|
||||
were basically impossible to set correctly when attempting to pass `None`.
|
||||
|
||||
## [0.5.0] - 2024-02-04
|
||||
### Changed
|
||||
- MSRV is now 1.65.0.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "age-plugin"
|
||||
description = "[BETA] API for writing age plugins."
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
|
|
@ -6,11 +6,12 @@ use age_plugin::{
|
|||
identity::{self, IdentityPluginV1},
|
||||
print_new_identity,
|
||||
recipient::{self, RecipientPluginV1},
|
||||
run_state_machine, Callbacks,
|
||||
run_state_machine, Callbacks, PluginHandler,
|
||||
};
|
||||
use clap::Parser;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::convert::Infallible;
|
||||
use std::env;
|
||||
use std::io;
|
||||
|
||||
|
@ -25,6 +26,43 @@ fn explode(location: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
struct FullHandler;
|
||||
|
||||
impl PluginHandler for FullHandler {
|
||||
type RecipientV1 = RecipientPlugin;
|
||||
type IdentityV1 = IdentityPlugin;
|
||||
|
||||
fn recipient_v1(self) -> io::Result<Self::RecipientV1> {
|
||||
Ok(RecipientPlugin)
|
||||
}
|
||||
|
||||
fn identity_v1(self) -> io::Result<Self::IdentityV1> {
|
||||
Ok(IdentityPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipientHandler;
|
||||
|
||||
impl PluginHandler for RecipientHandler {
|
||||
type RecipientV1 = RecipientPlugin;
|
||||
type IdentityV1 = Infallible;
|
||||
|
||||
fn recipient_v1(self) -> io::Result<Self::RecipientV1> {
|
||||
Ok(RecipientPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
struct IdentityHandler;
|
||||
|
||||
impl PluginHandler for IdentityHandler {
|
||||
type RecipientV1 = Infallible;
|
||||
type IdentityV1 = IdentityPlugin;
|
||||
|
||||
fn identity_v1(self) -> io::Result<Self::IdentityV1> {
|
||||
Ok(IdentityPlugin)
|
||||
}
|
||||
}
|
||||
|
||||
struct RecipientPlugin;
|
||||
|
||||
impl RecipientPluginV1 for RecipientPlugin {
|
||||
|
@ -66,6 +104,16 @@ impl RecipientPluginV1 for RecipientPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
fn labels(&mut self) -> HashSet<String> {
|
||||
let mut labels = HashSet::new();
|
||||
if let Ok(s) = env::var("AGE_PLUGIN_LABELS") {
|
||||
for label in s.split(',') {
|
||||
labels.insert(label.into());
|
||||
}
|
||||
}
|
||||
labels
|
||||
}
|
||||
|
||||
fn wrap_file_keys(
|
||||
&mut self,
|
||||
file_keys: Vec<FileKey>,
|
||||
|
@ -127,9 +175,14 @@ impl IdentityPluginV1 for IdentityPlugin {
|
|||
// identities.
|
||||
let _ = callbacks.message("This identity does nothing!")?;
|
||||
file_keys.entry(file_index).or_insert_with(|| {
|
||||
Ok(FileKey::from(
|
||||
TryInto::<[u8; 16]>::try_into(&stanza.body[..]).unwrap(),
|
||||
))
|
||||
FileKey::try_init_with_mut(|file_key| {
|
||||
if stanza.body.len() == file_key.len() {
|
||||
file_key.copy_from_slice(&stanza.body);
|
||||
Ok(())
|
||||
} else {
|
||||
panic!("File key is wrong length")
|
||||
}
|
||||
})
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
@ -149,11 +202,15 @@ fn main() -> io::Result<()> {
|
|||
let opts = PluginOptions::parse();
|
||||
|
||||
if let Some(state_machine) = opts.age_plugin {
|
||||
run_state_machine(
|
||||
&state_machine,
|
||||
Some(|| RecipientPlugin),
|
||||
Some(|| IdentityPlugin),
|
||||
)
|
||||
if let Ok(s) = env::var("AGE_HALF_PLUGIN") {
|
||||
match s.as_str() {
|
||||
"recipient" => run_state_machine(&state_machine, RecipientHandler),
|
||||
"identity" => run_state_machine(&state_machine, IdentityHandler),
|
||||
_ => panic!("Env variable AGE_HALF_PLUGIN={s} has unknown value. Boom! 💥"),
|
||||
}
|
||||
} else {
|
||||
run_state_machine(&state_machine, FullHandler)
|
||||
}
|
||||
} else {
|
||||
// A real plugin would generate a new identity here.
|
||||
print_new_identity(PLUGIN_NAME, &[], &[]);
|
||||
|
|
|
@ -7,7 +7,9 @@ use age_core::{
|
|||
};
|
||||
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||
use bech32::FromBase32;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
use std::io;
|
||||
|
||||
use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX};
|
||||
|
@ -16,6 +18,10 @@ const ADD_IDENTITY: &str = "add-identity";
|
|||
const RECIPIENT_STANZA: &str = "recipient-stanza";
|
||||
|
||||
/// The interface that age implementations will use to interact with an age plugin.
|
||||
///
|
||||
/// Implementations of this trait will be used within the [`identity-v1`] state machine.
|
||||
///
|
||||
/// [`identity-v1`]: https://c2sp.org/age-plugin#unwrapping-with-identity-v1
|
||||
pub trait IdentityPluginV1 {
|
||||
/// Stores an identity that the user would like to use for decrypting age files.
|
||||
///
|
||||
|
@ -49,6 +55,22 @@ pub trait IdentityPluginV1 {
|
|||
) -> io::Result<HashMap<usize, Result<FileKey, Vec<Error>>>>;
|
||||
}
|
||||
|
||||
impl IdentityPluginV1 for Infallible {
|
||||
fn add_identity(&mut self, _: usize, _: &str, _: &[u8]) -> Result<(), Error> {
|
||||
// This is never executed.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unwrap_file_keys(
|
||||
&mut self,
|
||||
_: Vec<Vec<Stanza>>,
|
||||
_: impl Callbacks<Error>,
|
||||
) -> io::Result<HashMap<usize, Result<FileKey, Vec<Error>>>> {
|
||||
// This is never executed.
|
||||
Ok(HashMap::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// The interface that age plugins can use to interact with an age implementation.
|
||||
struct BidirCallbacks<'a, 'b, R: io::Read, W: io::Write>(&'b mut BidirSend<'a, R, W>);
|
||||
|
||||
|
@ -113,7 +135,7 @@ impl<'a, 'b, R: io::Read, W: io::Write> Callbacks<Error> for BidirCallbacks<'a,
|
|||
.and_then(|res| match res {
|
||||
Ok(s) => String::from_utf8(s.body)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "secret is not UTF-8"))
|
||||
.map(|s| Ok(SecretString::new(s))),
|
||||
.map(|s| Ok(SecretString::from(s))),
|
||||
Err(e) => Ok(Err(e)),
|
||||
})
|
||||
}
|
||||
|
@ -200,7 +222,7 @@ pub(crate) fn run_v1<P: IdentityPluginV1>(mut plugin: P) -> io::Result<()> {
|
|||
|
||||
// Phase 1: receive identities and stanzas
|
||||
let (identities, recipient_stanzas) = {
|
||||
let (identities, stanzas, _) = conn.unidir_receive(
|
||||
let (identities, stanzas, _, _) = conn.unidir_receive(
|
||||
(ADD_IDENTITY, |s| match (&s.args[..], &s.body[..]) {
|
||||
([identity], []) => Ok(identity.clone()),
|
||||
_ => Err(Error::Internal {
|
||||
|
@ -233,6 +255,7 @@ pub(crate) fn run_v1<P: IdentityPluginV1>(mut plugin: P) -> io::Result<()> {
|
|||
}
|
||||
}),
|
||||
(None, |_| Ok(())),
|
||||
(None, |_| Ok(())),
|
||||
)?;
|
||||
|
||||
// Now that we have the full list of identities, parse them as Bech32 and add them
|
||||
|
|
|
@ -74,13 +74,28 @@
|
|||
//! identity::{self, IdentityPluginV1},
|
||||
//! print_new_identity,
|
||||
//! recipient::{self, RecipientPluginV1},
|
||||
//! Callbacks, run_state_machine,
|
||||
//! Callbacks, PluginHandler, run_state_machine,
|
||||
//! };
|
||||
//! use clap::Parser;
|
||||
//!
|
||||
//! use std::collections::HashMap;
|
||||
//! use std::collections::{HashMap, HashSet};
|
||||
//! use std::io;
|
||||
//!
|
||||
//! struct Handler;
|
||||
//!
|
||||
//! impl PluginHandler for Handler {
|
||||
//! type RecipientV1 = RecipientPlugin;
|
||||
//! type IdentityV1 = IdentityPlugin;
|
||||
//!
|
||||
//! fn recipient_v1(self) -> io::Result<Self::RecipientV1> {
|
||||
//! Ok(RecipientPlugin)
|
||||
//! }
|
||||
//!
|
||||
//! fn identity_v1(self) -> io::Result<Self::IdentityV1> {
|
||||
//! Ok(IdentityPlugin)
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! struct RecipientPlugin;
|
||||
//!
|
||||
//! impl RecipientPluginV1 for RecipientPlugin {
|
||||
|
@ -102,6 +117,10 @@
|
|||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! fn labels(&mut self) -> HashSet<String> {
|
||||
//! todo!()
|
||||
//! }
|
||||
//!
|
||||
//! fn wrap_file_keys(
|
||||
//! &mut self,
|
||||
//! file_keys: Vec<FileKey>,
|
||||
|
@ -143,11 +162,7 @@
|
|||
//!
|
||||
//! if let Some(state_machine) = opts.age_plugin {
|
||||
//! // The plugin was started by an age client; run the state machine.
|
||||
//! run_state_machine(
|
||||
//! &state_machine,
|
||||
//! Some(|| RecipientPlugin),
|
||||
//! Some(|| IdentityPlugin),
|
||||
//! )?;
|
||||
//! run_state_machine(&state_machine, Handler)?;
|
||||
//! return Ok(());
|
||||
//! }
|
||||
//!
|
||||
|
@ -209,34 +224,12 @@ pub fn print_new_identity(plugin_name: &str, identity: &[u8], recipient: &[u8])
|
|||
///
|
||||
/// This should be triggered if the `--age-plugin=state_machine` flag is provided as an
|
||||
/// argument when starting the plugin.
|
||||
pub fn run_state_machine<R: recipient::RecipientPluginV1, I: identity::IdentityPluginV1>(
|
||||
state_machine: &str,
|
||||
recipient_v1: Option<impl FnOnce() -> R>,
|
||||
identity_v1: Option<impl FnOnce() -> I>,
|
||||
) -> io::Result<()> {
|
||||
pub fn run_state_machine(state_machine: &str, handler: impl PluginHandler) -> io::Result<()> {
|
||||
use age_core::plugin::{IDENTITY_V1, RECIPIENT_V1};
|
||||
|
||||
match state_machine {
|
||||
RECIPIENT_V1 => {
|
||||
if let Some(plugin) = recipient_v1 {
|
||||
recipient::run_v1(plugin())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"plugin doesn't support recipient-v1 state machine",
|
||||
))
|
||||
}
|
||||
}
|
||||
IDENTITY_V1 => {
|
||||
if let Some(plugin) = identity_v1 {
|
||||
identity::run_v1(plugin())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"plugin doesn't support identity-v1 state machine",
|
||||
))
|
||||
}
|
||||
}
|
||||
RECIPIENT_V1 => recipient::run_v1(handler.recipient_v1()?),
|
||||
IDENTITY_V1 => identity::run_v1(handler.identity_v1()?),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"unknown plugin state machine",
|
||||
|
@ -244,6 +237,63 @@ pub fn run_state_machine<R: recipient::RecipientPluginV1, I: identity::IdentityP
|
|||
}
|
||||
}
|
||||
|
||||
/// The interfaces that age implementations will use to interact with an age plugin.
|
||||
///
|
||||
/// This trait exists to encapsulate the set of arguments to [`run_state_machine`] that
|
||||
/// different plugins may want to provide.
|
||||
///
|
||||
/// # How to implement this trait
|
||||
///
|
||||
/// ## Full plugins
|
||||
///
|
||||
/// - Set all associated types to your plugin's implementations.
|
||||
/// - Override all default methods of the trait.
|
||||
///
|
||||
/// ## Recipient-only plugins
|
||||
///
|
||||
/// - Set [`PluginHandler::RecipientV1`] to your plugin's implementation.
|
||||
/// - Override [`PluginHandler::recipient_v1`] to return an instance of your type.
|
||||
/// - Set [`PluginHandler::IdentityV1`] to [`std::convert::Infallible`].
|
||||
/// - Don't override [`PluginHandler::identity_v1`].
|
||||
///
|
||||
/// ## Identity-only plugins
|
||||
///
|
||||
/// - Set [`PluginHandler::RecipientV1`] to [`std::convert::Infallible`].
|
||||
/// - Don't override [`PluginHandler::recipient_v1`].
|
||||
/// - Set [`PluginHandler::IdentityV1`] to your plugin's implementation.
|
||||
/// - Override [`PluginHandler::identity_v1`] to return an instance of your type.
|
||||
pub trait PluginHandler: Sized {
|
||||
/// The plugin's [`recipient-v1`] implementation.
|
||||
///
|
||||
/// [`recipient-v1`]: https://c2sp.org/age-plugin#wrapping-with-recipient-v1
|
||||
type RecipientV1: recipient::RecipientPluginV1;
|
||||
|
||||
/// The plugin's [`identity-v1`] implementation.
|
||||
///
|
||||
/// [`identity-v1`]: https://c2sp.org/age-plugin#unwrapping-with-identity-v1
|
||||
type IdentityV1: identity::IdentityPluginV1;
|
||||
|
||||
/// Returns an instance of the plugin's [`recipient-v1`] implementation.
|
||||
///
|
||||
/// [`recipient-v1`]: https://c2sp.org/age-plugin#wrapping-with-recipient-v1
|
||||
fn recipient_v1(self) -> io::Result<Self::RecipientV1> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"plugin doesn't support recipient-v1 state machine",
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns an instance of the plugin's [`identity-v1`] implementation.
|
||||
///
|
||||
/// [`identity-v1`]: https://c2sp.org/age-plugin#unwrapping-with-identity-v1
|
||||
fn identity_v1(self) -> io::Result<Self::IdentityV1> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"plugin doesn't support identity-v1 state machine",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// The interface that age plugins can use to interact with an age implementation.
|
||||
pub trait Callbacks<E> {
|
||||
/// Shows a message to the user.
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
//! Recipient plugin helpers.
|
||||
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza, FILE_KEY_BYTES},
|
||||
format::{is_arbitrary_string, FileKey, Stanza},
|
||||
plugin::{self, BidirSend, Connection},
|
||||
secrecy::SecretString,
|
||||
};
|
||||
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||
use bech32::FromBase32;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::convert::Infallible;
|
||||
use std::io;
|
||||
|
||||
use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX, PLUGIN_RECIPIENT_PREFIX};
|
||||
|
@ -14,9 +17,21 @@ use crate::{Callbacks, PLUGIN_IDENTITY_PREFIX, PLUGIN_RECIPIENT_PREFIX};
|
|||
const ADD_RECIPIENT: &str = "add-recipient";
|
||||
const ADD_IDENTITY: &str = "add-identity";
|
||||
const WRAP_FILE_KEY: &str = "wrap-file-key";
|
||||
const EXTENSION_LABELS: &str = "extension-labels";
|
||||
const RECIPIENT_STANZA: &str = "recipient-stanza";
|
||||
const LABELS: &str = "labels";
|
||||
|
||||
/// The interface that age implementations will use to interact with an age plugin.
|
||||
///
|
||||
/// Implementations of this trait will be used within the [`recipient-v1`] state machine.
|
||||
///
|
||||
/// The trait methods are always called in this order:
|
||||
/// - [`Self::add_recipient`] / [`Self::add_identity`] (in any order, including
|
||||
/// potentially interleaved).
|
||||
/// - [`Self::labels`] (once all recipients and identities have been added).
|
||||
/// - [`Self::wrap_file_keys`]
|
||||
///
|
||||
/// [`recipient-v1`]: https://c2sp.org/age-plugin#wrapping-with-recipient-v1
|
||||
pub trait RecipientPluginV1 {
|
||||
/// Stores a recipient that the user would like to encrypt age files to.
|
||||
///
|
||||
|
@ -33,11 +48,45 @@ pub trait RecipientPluginV1 {
|
|||
/// Returns an error if the identity is unknown or invalid.
|
||||
fn add_identity(&mut self, index: usize, plugin_name: &str, bytes: &[u8]) -> Result<(), Error>;
|
||||
|
||||
/// Returns labels that constrain how the stanzas produced by [`Self::wrap_file_keys`]
|
||||
/// may be combined with those from other recipients.
|
||||
///
|
||||
/// Encryption will succeed only if every recipient returns the same set of labels.
|
||||
/// Subsets or partial overlapping sets are not allowed; all sets must be identical.
|
||||
/// Labels are compared exactly, and are case-sensitive.
|
||||
///
|
||||
/// Label sets can be used to ensure a recipient is only encrypted to alongside other
|
||||
/// recipients with equivalent properties, or to ensure a recipient is always used
|
||||
/// alone. A recipient with no particular properties to enforce should return an empty
|
||||
/// label set.
|
||||
///
|
||||
/// Labels can have any value that is a valid arbitrary string (`1*VCHAR` in ABNF),
|
||||
/// but usually take one of several forms:
|
||||
/// - *Common public label* - used by multiple recipients to permit their stanzas to
|
||||
/// be used only together. Examples include:
|
||||
/// - `postquantum` - indicates that the recipient stanzas being generated are
|
||||
/// postquantum-secure, and that they can only be combined with other stanzas
|
||||
/// that are also postquantum-secure.
|
||||
/// - *Common private label* - used by recipients created by the same private entity
|
||||
/// to permit their recipient stanzas to be used only together. For example,
|
||||
/// private recipients used in a corporate environment could all send the same
|
||||
/// private label in order to prevent compliant age clients from simultaneously
|
||||
/// wrapping file keys with other recipients.
|
||||
/// - *Random label* - used by recipients that want to ensure their stanzas are not
|
||||
/// used with any other recipient stanzas. This can be used to produce a file key
|
||||
/// that is only encrypted to a single recipient stanza, for example to preserve
|
||||
/// its authentication properties.
|
||||
fn labels(&mut self) -> HashSet<String>;
|
||||
|
||||
/// Wraps each `file_key` to all recipients and identities previously added via
|
||||
/// `add_recipient` and `add_identity`.
|
||||
///
|
||||
/// Returns either one stanza per recipient and identity for each file key, or any
|
||||
/// errors if one or more recipients or identities could not be wrapped to.
|
||||
/// Returns a set of stanzas per file key that wrap it to each recipient and identity.
|
||||
/// Plugins may return more than one stanza per "actual recipient", e.g. to support
|
||||
/// multiple formats, to build group aliases, or to act as a proxy.
|
||||
///
|
||||
/// If one or more recipients or identities could not be wrapped to, no stanzas are
|
||||
/// returned for any of the file keys.
|
||||
///
|
||||
/// `callbacks` can be used to interact with the user, to have them take some physical
|
||||
/// action or request a secret value.
|
||||
|
@ -48,6 +97,32 @@ pub trait RecipientPluginV1 {
|
|||
) -> io::Result<Result<Vec<Vec<Stanza>>, Vec<Error>>>;
|
||||
}
|
||||
|
||||
impl RecipientPluginV1 for Infallible {
|
||||
fn add_recipient(&mut self, _: usize, _: &str, _: &[u8]) -> Result<(), Error> {
|
||||
// This is never executed.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_identity(&mut self, _: usize, _: &str, _: &[u8]) -> Result<(), Error> {
|
||||
// This is never executed.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn labels(&mut self) -> HashSet<String> {
|
||||
// This is never executed.
|
||||
HashSet::new()
|
||||
}
|
||||
|
||||
fn wrap_file_keys(
|
||||
&mut self,
|
||||
_: Vec<FileKey>,
|
||||
_: impl Callbacks<Error>,
|
||||
) -> io::Result<Result<Vec<Vec<Stanza>>, Vec<Error>>> {
|
||||
// This is never executed.
|
||||
Ok(Ok(vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
/// The interface that age plugins can use to interact with an age implementation.
|
||||
struct BidirCallbacks<'a, 'b, R: io::Read, W: io::Write>(&'b mut BidirSend<'a, R, W>);
|
||||
|
||||
|
@ -112,7 +187,7 @@ impl<'a, 'b, R: io::Read, W: io::Write> Callbacks<Error> for BidirCallbacks<'a,
|
|||
.and_then(|res| match res {
|
||||
Ok(s) => String::from_utf8(s.body)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "secret is not UTF-8"))
|
||||
.map(|s| Ok(SecretString::new(s))),
|
||||
.map(|s| Ok(SecretString::from(s))),
|
||||
Err(e) => Ok(Err(e)),
|
||||
})
|
||||
}
|
||||
|
@ -188,8 +263,8 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
|
|||
let mut conn = Connection::accept();
|
||||
|
||||
// Phase 1: collect recipients, and file keys to be wrapped
|
||||
let ((recipients, identities), file_keys) = {
|
||||
let (recipients, identities, file_keys) = conn.unidir_receive(
|
||||
let ((recipients, identities), file_keys, labels_supported) = {
|
||||
let (recipients, identities, file_keys, labels_supported) = conn.unidir_receive(
|
||||
(ADD_RECIPIENT, |s| match (&s.args[..], &s.body[..]) {
|
||||
([recipient], []) => Ok(recipient.clone()),
|
||||
_ => Err(Error::Internal {
|
||||
|
@ -210,12 +285,18 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
|
|||
}),
|
||||
(Some(WRAP_FILE_KEY), |s| {
|
||||
// TODO: Should we ignore file key commands with unexpected metadata args?
|
||||
TryInto::<[u8; FILE_KEY_BYTES]>::try_into(&s.body[..])
|
||||
.map_err(|_| Error::Internal {
|
||||
message: "invalid file key length".to_owned(),
|
||||
})
|
||||
.map(FileKey::from)
|
||||
FileKey::try_init_with_mut(|file_key| {
|
||||
if s.body.len() == file_key.len() {
|
||||
file_key.copy_from_slice(&s.body);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Internal {
|
||||
message: "invalid file key length".to_owned(),
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
(Some(EXTENSION_LABELS), |_| Ok(())),
|
||||
)?;
|
||||
(
|
||||
match (recipients, identities) {
|
||||
|
@ -236,6 +317,13 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
|
|||
}]),
|
||||
r => r,
|
||||
},
|
||||
match &labels_supported.unwrap() {
|
||||
Ok(v) if v.is_empty() => Ok(false),
|
||||
Ok(v) if v.len() == 1 => Ok(true),
|
||||
_ => Err(vec![Error::Internal {
|
||||
message: format!("Received more than one {} command", EXTENSION_LABELS),
|
||||
}]),
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -300,23 +388,61 @@ pub(crate) fn run_v1<P: RecipientPluginV1>(mut plugin: P) -> io::Result<()> {
|
|||
|index, plugin_name, bytes| plugin.add_identity(index, plugin_name, &bytes),
|
||||
);
|
||||
|
||||
let required_labels = plugin.labels();
|
||||
|
||||
let labels = match (labels_supported, required_labels.is_empty()) {
|
||||
(Ok(true), _) | (Ok(false), true) => {
|
||||
if required_labels.contains("") {
|
||||
Err(vec![Error::Internal {
|
||||
message: "Plugin tried to use the empty string as a label".into(),
|
||||
}])
|
||||
} else if required_labels.iter().all(is_arbitrary_string) {
|
||||
Ok(required_labels)
|
||||
} else {
|
||||
Err(vec![Error::Internal {
|
||||
message: "Plugin tried to use a label containing an invalid character".into(),
|
||||
}])
|
||||
}
|
||||
}
|
||||
(Ok(false), false) => Err(vec![Error::Internal {
|
||||
message: "Plugin requires labels but client does not support them".into(),
|
||||
}]),
|
||||
(Err(errors), true) => Err(errors),
|
||||
(Err(mut errors), false) => {
|
||||
errors.push(Error::Internal {
|
||||
message: "Plugin requires labels but client does not support them".into(),
|
||||
});
|
||||
Err(errors)
|
||||
}
|
||||
};
|
||||
|
||||
// Phase 2: wrap the file keys or return errors
|
||||
conn.bidir_send(|mut phase| {
|
||||
let (expected_stanzas, file_keys) = match (recipients, identities, file_keys) {
|
||||
(Ok(recipients), Ok(identities), Ok(file_keys)) => (recipients + identities, file_keys),
|
||||
(recipients, identities, file_keys) => {
|
||||
for error in recipients
|
||||
.err()
|
||||
.into_iter()
|
||||
.chain(identities.err())
|
||||
.chain(file_keys.err())
|
||||
.flatten()
|
||||
{
|
||||
error.send(&mut phase)?;
|
||||
let (expected_stanzas, file_keys, labels) =
|
||||
match (recipients, identities, file_keys, labels) {
|
||||
(Ok(recipients), Ok(identities), Ok(file_keys), Ok(labels)) => {
|
||||
(recipients + identities, file_keys, labels)
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
(recipients, identities, file_keys, labels) => {
|
||||
for error in recipients
|
||||
.err()
|
||||
.into_iter()
|
||||
.chain(identities.err())
|
||||
.chain(file_keys.err())
|
||||
.chain(labels.err())
|
||||
.flatten()
|
||||
{
|
||||
error.send(&mut phase)?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let labels = labels.iter().map(|s| s.as_str()).collect::<Vec<_>>();
|
||||
// We confirmed above that if `labels` is non-empty, the client supports labels.
|
||||
// So we can unconditionally send this, and will only get an `unsupported`
|
||||
// response if `labels` is empty (where it does not matter).
|
||||
let _ = phase.send(LABELS, &labels, &[])?;
|
||||
|
||||
match plugin.wrap_file_keys(file_keys, BidirCallbacks(&mut phase))? {
|
||||
Ok(files) => {
|
||||
|
|
|
@ -10,13 +10,68 @@ to 1.0.0 are beta releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1] - 2024-11-18
|
||||
## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1, 0.11.1] - 2024-11-18
|
||||
### Security
|
||||
- The age plugin protocol previously allowed plugin names that could be
|
||||
interpreted as file paths. Under certain conditions, this could lead to a
|
||||
different binary being executed as an age plugin than intended. Plugin names
|
||||
are now required to only contain alphanumeric characters or the four special
|
||||
characters `+-._`.
|
||||
- Fixed a security vulnerability that could allow an attacker to execute an
|
||||
arbitrary binary under certain conditions. See GHSA-4fg7-vxc8-qx5w. Plugin
|
||||
names are now required to only contain alphanumeric characters or the four
|
||||
special characters `+-._`. Thanks to ⬡-49016 for reporting this issue.
|
||||
|
||||
## [0.11.0] - 2024-11-03
|
||||
### Added
|
||||
- New streamlined APIs for use with a single recipient or identity and a small
|
||||
amount of data (that can fit entirely in memory):
|
||||
- `age::encrypt`
|
||||
- `age::encrypt_and_armor`
|
||||
- `age::decrypt`
|
||||
- `age::Decryptor::{decrypt, decrypt_async, is_scrypt}`
|
||||
- `age::IdentityFile::to_recipients`
|
||||
- `age::IdentityFile::with_callbacks`
|
||||
- `age::IdentityFile::write_recipients_file`
|
||||
- `age::IdentityFileConvertError`
|
||||
- `age::NoCallbacks`
|
||||
- `age::scrypt`, providing recipient and identity types for passphrase-based
|
||||
encryption.
|
||||
- Partial French translation!
|
||||
|
||||
### Changed
|
||||
- Migrated to `i18n-embed 0.15`, `secrecy 0.10`.
|
||||
- `age::Encryptor::with_recipients` now takes recipients by reference instead of
|
||||
by value. This aligns it with `age::Decryptor` (which takes identities by
|
||||
reference), and also means that errors with recipients are reported earlier.
|
||||
This causes the following changes to the API:
|
||||
- `Encryptor::with_recipients` takes `impl Iterator<Item = &'a dyn Recipient>`
|
||||
instead of `Vec<Box<dyn Recipient + Send>>`.
|
||||
- Verification of recipients and generation of stanzas now happens in
|
||||
`Encryptor::with_recipients` instead of `Encryptor::wrap_output` and
|
||||
`Encryptor::wrap_async_output`.
|
||||
- `Encryptor::with_recipients` returns `Result<Self, EncryptError>` instead of
|
||||
`Option<Self>`, and `Encryptor::{wrap_output, wrap_async_output}` return
|
||||
`io::Result<StreamWriter<W>>` instead of `Result<StreamWriter<W>, EncryptError>`.
|
||||
- `age::EncryptError` has a new variant `MissingRecipients`, taking the place
|
||||
of the `None` that `Encryptor::with_recipients` could previously return.
|
||||
- `age::Decryptor` is now an opaque struct instead of an enum with `Recipients`
|
||||
and `Passphrase` variants.
|
||||
- `age::IdentityFile` now has a `C: Callbacks` generic parameter, which defaults
|
||||
to `NoCallbacks`.
|
||||
- `age::IdentityFile::into_identities` now returns
|
||||
`Result<Vec<Box<dyn crate::Identity>>, DecryptError>` instead of
|
||||
`Vec<IdentityFileEntry>`.
|
||||
- `age::Recipient::wrap_file_key` now returns `(Vec<Stanza>, HashSet<String>)`:
|
||||
a tuple of the stanzas to be placed in an age file header, and labels that
|
||||
constrain how the stanzas may be combined with those from other recipients.
|
||||
- `age::plugin::RecipientPluginV1` now supports the labels extension.
|
||||
|
||||
### Fixed
|
||||
- `age::cli_common::read_identities` once again correctly parses identity files
|
||||
that are a single line without a trailing newline. This broke in 0.10.0 due to
|
||||
an unrelated refactor.
|
||||
|
||||
### Removed
|
||||
- `age::decryptor::PassphraseDecryptor` (use `age::Decryptor` with
|
||||
`age::scrypt::Identity` instead).
|
||||
- `age::decryptor::RecipientsDecryptor` (use `age::Decryptor` instead).
|
||||
- `age::IdentityFileEntry`
|
||||
|
||||
## [0.10.0] - 2024-02-04
|
||||
### Added
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "age"
|
||||
description = "[BETA] A simple, secure, and modern encryption library."
|
||||
version = "0.10.1"
|
||||
version = "0.11.1"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
|
@ -37,7 +37,7 @@ futures = { version = "0.3", optional = true }
|
|||
pin-project = "1"
|
||||
|
||||
# Common CLI dependencies
|
||||
pinentry = { version = "0.5", optional = true }
|
||||
pinentry = { workspace = true, optional = true }
|
||||
|
||||
# Dependencies used internally:
|
||||
# (Breaking upgrades to these are usually backwards-compatible, but check MSRVs.)
|
||||
|
|
|
@ -12,8 +12,8 @@ encryption and decryption of files or streams (e.g. in shell scripts), as well
|
|||
as additional features such as mounting an encrypted archive.
|
||||
|
||||
The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1).
|
||||
The age format was designed by [@Benjojo12](https://twitter.com/Benjojo12) and
|
||||
[@FiloSottile](https://twitter.com/FiloSottile).
|
||||
The age format was designed by [@Benjojo](https://benjojo.co.uk/) and
|
||||
[@FiloSottile](https://bsky.app/profile/did:plc:x2nsupeeo52oznrmplwapppl).
|
||||
|
||||
The reference interoperable Go implementation is available at
|
||||
[filippo.io/age](https://filippo.io/age).
|
||||
|
@ -23,7 +23,7 @@ The reference interoperable Go implementation is available at
|
|||
Add this line to your `Cargo.toml`:
|
||||
|
||||
```
|
||||
age = "0.10"
|
||||
age = "0.11"
|
||||
```
|
||||
|
||||
See the [documentation](https://docs.rs/age) for examples.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use age::{x25519, Decryptor, Encryptor, Recipient};
|
||||
use age::{x25519, Decryptor, Encryptor};
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
|
||||
|
||||
#[cfg(unix)]
|
||||
|
@ -8,7 +8,7 @@ use std::io::Write;
|
|||
|
||||
fn bench(c: &mut Criterion) {
|
||||
let recipients: Vec<_> = (0..10)
|
||||
.map(|_| Box::new(x25519::Identity::generate().to_public()))
|
||||
.map(|_| x25519::Identity::generate().to_public())
|
||||
.collect();
|
||||
let mut group = c.benchmark_group("header");
|
||||
|
||||
|
@ -16,17 +16,11 @@ fn bench(c: &mut Criterion) {
|
|||
group.throughput(Throughput::Elements(count as u64));
|
||||
group.bench_function(BenchmarkId::new("parse", count), |b| {
|
||||
let mut encrypted = vec![];
|
||||
let mut output = Encryptor::with_recipients(
|
||||
recipients
|
||||
.iter()
|
||||
.take(count)
|
||||
.cloned()
|
||||
.map(|r| r as Box<dyn Recipient + Send>)
|
||||
.collect(),
|
||||
)
|
||||
.unwrap()
|
||||
.wrap_output(&mut encrypted)
|
||||
.unwrap();
|
||||
let mut output =
|
||||
Encryptor::with_recipients(recipients.iter().take(count).map(|r| r as _))
|
||||
.unwrap()
|
||||
.wrap_output(&mut encrypted)
|
||||
.unwrap();
|
||||
output.write_all(&[]).unwrap();
|
||||
output.finish().unwrap();
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ fn bench(c: &mut Criterion_) {
|
|||
|
||||
group.bench_function(BenchmarkId::new("encrypt", size), |b| {
|
||||
b.iter(|| {
|
||||
let mut output = Encryptor::with_recipients(vec![Box::new(recipient.clone())])
|
||||
let mut output = Encryptor::with_recipients(iter::once(&recipient as _))
|
||||
.unwrap()
|
||||
.wrap_output(io::sink())
|
||||
.unwrap();
|
||||
|
@ -62,7 +62,7 @@ fn bench(c: &mut Criterion_) {
|
|||
});
|
||||
|
||||
group.bench_function(BenchmarkId::new("decrypt", size), |b| {
|
||||
let mut output = Encryptor::with_recipients(vec![Box::new(recipient.clone())])
|
||||
let mut output = Encryptor::with_recipients(iter::once(&recipient as _))
|
||||
.unwrap()
|
||||
.wrap_output(&mut ct_buf)
|
||||
.unwrap();
|
||||
|
@ -70,10 +70,7 @@ fn bench(c: &mut Criterion_) {
|
|||
output.finish().unwrap();
|
||||
|
||||
b.iter(|| {
|
||||
let decryptor = match Decryptor::new_buffered(&ct_buf[..]).unwrap() {
|
||||
Decryptor::Recipients(decryptor) => decryptor,
|
||||
_ => panic!(),
|
||||
};
|
||||
let decryptor = Decryptor::new_buffered(&ct_buf[..]).unwrap();
|
||||
let mut input = decryptor
|
||||
.decrypt(iter::once(&identity as &dyn age::Identity))
|
||||
.unwrap();
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
|
@ -44,6 +46,20 @@ rec-deny-binary-output = Did you mean to use {-flag-armor}? {rec-detected-binary
|
|||
|
||||
err-deny-overwrite-file = refusing to overwrite existing file '{$filename}'.
|
||||
|
||||
err-invalid-filename = invalid filename '{$filename}'.
|
||||
|
||||
err-missing-directory = directory '{$path}' does not exist.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Failed to write to output: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Identity file '{$filename}' contains identities for '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Try using '{-age-plugin-}{$plugin_name}' to convert this identity to a recipient.
|
||||
|
||||
err-no-identities-in-file = No identities found in file '{$filename}'.
|
||||
err-no-identities-in-stdin = No identities found in standard input.
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Decryption failed
|
||||
|
@ -55,8 +71,17 @@ err-header-invalid = Header is invalid
|
|||
|
||||
err-header-mac-invalid = Header MAC is invalid
|
||||
|
||||
err-incompatible-recipients-oneway = Cannot encrypt to a recipient with labels '{$labels}' alongside a recipient with no labels
|
||||
err-incompatible-recipients-twoway = Cannot encrypt to a recipient with labels '{$left}' alongside a recipient with labels '{$right}'
|
||||
|
||||
err-invalid-recipient-labels = The first recipient requires one or more invalid labels: '{$labels}'
|
||||
|
||||
err-key-decryption = Failed to decrypt an encrypted key
|
||||
|
||||
err-missing-recipients = Missing recipients.
|
||||
|
||||
err-mixed-recipient-passphrase = {-scrypt-recipient} can't be used with other recipients.
|
||||
|
||||
err-no-matching-keys = No matching keys found
|
||||
|
||||
err-unknown-format = Unknown {-age} format.
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
|
@ -55,6 +57,8 @@ err-header-mac-invalid = MAC de encabezado inválido.
|
|||
|
||||
err-key-decryption = No se pudo desencriptar una clave encriptada.
|
||||
|
||||
err-missing-recipients = No se encontraron destinatarios.
|
||||
|
||||
err-no-matching-keys = No se encontraron claves coincidentes.
|
||||
|
||||
err-unknown-format = Formato {-age} desconocido.
|
||||
|
|
185
age/i18n/fr/age.ftl
Normal file
185
age/i18n/fr/age.ftl
Normal file
|
@ -0,0 +1,185 @@
|
|||
# Copyright 2020 Jack Grigg
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
# option. This file may not be copied, modified, or distributed
|
||||
# except according to those terms.
|
||||
|
||||
### Localization for strings in the age library crate
|
||||
|
||||
## Terms (not to be localized)
|
||||
|
||||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
-ssh-ed25519 = ssh-ed25519
|
||||
-fido-u2f = FIDO/U2F
|
||||
-yubikeys = YubiKeys
|
||||
-piv = PIV
|
||||
|
||||
## CLI helpers
|
||||
|
||||
cli-secret-input-required = Entrée requise
|
||||
cli-secret-input-mismatch = Les entrées ne correspondent pas
|
||||
|
||||
cli-passphrase-desc = Tapez votre phrase secrète (laissez vide pour en générer une très sure automatiquement)
|
||||
cli-passphrase-prompt = Phrase secrète
|
||||
cli-passphrase-confirm = Confirmez votre phrase secrète
|
||||
|
||||
-flag-armor = -a/--armor
|
||||
-flag-output = -o/--output
|
||||
-output-stdout = -o -
|
||||
|
||||
cli-truncated-tty = tronqué; utilisez un pipe, une redirection ou {-flag-output} pour déchiffrer l'entièreté du fichier
|
||||
|
||||
err-detected-binary = données non impressibles détectées; par précaution, pas d'impression dans le terminal.
|
||||
rec-detected-binary = Forcez l'impression avec '{-output-stdout}'.
|
||||
|
||||
err-deny-binary-output = refus d'impression de valeurs binaires dans le terminal.
|
||||
rec-deny-binary-output = Est-ce que vous vouliez utiliser {-flag-armor}? {rec-detected-binary}
|
||||
|
||||
err-deny-overwrite-file = refus d'écraser le fichier existant '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Echec d'écriture vers la sortie: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Le ficher d'identité '{$filename}' contient des identités pour '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Essayez d'utiliser {-age-plugin-}{$plugin_name}' pour convertir cette identité en un destinataire.
|
||||
|
||||
err-no-identities-in-file = Aucune identité trouvée dans le fichier '{$filename}'.
|
||||
err-no-identities-in-stdin = Aucune identité trouvée dans l'entrée standard (stdin).
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Echec du déchiffrement
|
||||
|
||||
err-excessive-work = Facteur d'effort trop grand pour la phrase secrète.
|
||||
rec-excessive-work = Le déchiffrement prendrait environ {$duration} seconds.
|
||||
|
||||
err-header-invalid = En-tête non valable
|
||||
|
||||
err-header-mac-invalid = Le MAC de l'en-tête est invalide
|
||||
|
||||
err-key-decryption = Echec du déchiffrement d'une clef chiffrée
|
||||
|
||||
err-missing-recipients = Destinataires manquants.
|
||||
|
||||
err-no-matching-keys = Aucune clef correspondante n'a été trouvée
|
||||
|
||||
err-unknown-format = Format {-age} inconnu.
|
||||
rec-unknown-format = Avez-vous tenté de mettre jour vers la dernière version ?
|
||||
|
||||
err-missing-plugin = Impossible de trouver '{$plugin_name}' dans le PATH.
|
||||
rec-missing-plugin = Avez-vous installé le plugin ?
|
||||
|
||||
err-plugin-identity = '{$plugin_name}' n'a pas pu utiliser une identité: {$message}
|
||||
err-plugin-recipient = '{$plugin_name}' n'a pas pu utiliser le destinataire {$recipient}: {$message}
|
||||
|
||||
err-plugin-died = '{$plugin_name}' est mort de manière inopinée.
|
||||
rec-plugin-died-1 = Si vous développez un plugin, utilisez {$env_var} pour plus d'informations.
|
||||
rec-plugin-died-2 = Attention: ceci imprime des informations de clef privées sur la sortie d'erreur standard.
|
||||
|
||||
err-plugin-multiple = Le plugin a retourné de multiples erreurs:
|
||||
|
||||
err-read-identity-encrypted-without-passphrase =
|
||||
Le fichier d'identité '{$filename}' est chiffré avec {-age} mais pas avec une phrase secrète.
|
||||
err-read-identity-not-found = Fichier d'identité introuvable: {$filename}
|
||||
|
||||
err-read-invalid-recipient = Destinataire invalide: '{$recipient}'.
|
||||
|
||||
err-read-invalid-recipients-file =
|
||||
Le fichier de destinataires '{$filename}' contient des données autres que des destinataires à la ligne {$line_number}.
|
||||
|
||||
err-read-missing-recipients-file = Fichier de destinataires introuvable: {$filename}
|
||||
|
||||
err-read-multiple-stdin = L'entrée standard (stdin) ne peut pas être utilisée pour plus d'une chose.
|
||||
|
||||
err-read-rsa-modulus-too-large =
|
||||
Module RSA Trop Grand
|
||||
---------------------
|
||||
{-openssh} supporte de nombreuses tailles de modules RSA, mais {-rage} ne supporte que des clefs
|
||||
publiques d'au plus {$max_size} bits, pour éviter les risques de déni de service (DoS) lors du
|
||||
chiffrement vers des clefs publiques inconnues.
|
||||
|
||||
err-read-rsa-modulus-too-small = Taille de clef RSA trop petite.
|
||||
|
||||
err-stream-last-chunk-empty = Le dernier morceau du STREAM est vide. chunk is empty. S'il vous plait, faites un bug report, et/ou essayez avec une version plus ancienne de {-rage}.
|
||||
|
||||
## Encrypted identities
|
||||
|
||||
encrypted-passphrase-prompt = Type passphrase for encrypted identity '{$filename}'
|
||||
|
||||
encrypted-warn-no-match = Warning: encrypted identity file '{$filename}' didn't match file's recipients
|
||||
|
||||
## Plugin identities
|
||||
|
||||
plugin-waiting-on-binary = Waiting for {$binary_name}...
|
||||
|
||||
## SSH identities
|
||||
|
||||
ssh-passphrase-prompt = Type passphrase for {-openssh} key '{$filename}'
|
||||
|
||||
ssh-unsupported-key = Unsupported SSH key: {$name}
|
||||
|
||||
ssh-insecure-key-format =
|
||||
Insecure Encrypted Key Format
|
||||
-----------------------------
|
||||
Prior to {-openssh} version 7.8, if a password was set when generating a new
|
||||
DSA, ECDSA, or RSA key, {-ssh-keygen} would encrypt the key using the encrypted
|
||||
PEM format. This encryption format is insecure and should no longer be used.
|
||||
|
||||
You can migrate your key to the encrypted SSH private key format (which has
|
||||
been supported by {-openssh} since version 6.5, released in January 2014) by
|
||||
changing its passphrase with the following command:
|
||||
|
||||
{" "}{$change_passphrase}
|
||||
|
||||
If you are using an {-openssh} version between 6.5 and 7.7 (such as the default
|
||||
{-openssh} provided on Ubuntu 18.04 LTS), you can use the following command to
|
||||
force keys to be generated using the new format:
|
||||
|
||||
{" "}{$gen_new}
|
||||
|
||||
ssh-unsupported-cipher =
|
||||
Unsupported Cipher for Encrypted SSH Key
|
||||
----------------------------------------
|
||||
{-openssh} internally supports several different ciphers for encrypted keys,
|
||||
but it has only ever directly generated a few of them. {-rage} supports all
|
||||
ciphers that {-ssh-keygen} might generate, and is being updated on a
|
||||
case-by-case basis with support for non-standard ciphers. Your key uses a
|
||||
currently-unsupported cipher ({$cipher}).
|
||||
|
||||
If you would like support for this key type, please open an issue here:
|
||||
|
||||
{$new_issue}
|
||||
|
||||
ssh-unsupported-key-type =
|
||||
Unsupported SSH Key Type
|
||||
------------------------
|
||||
{-openssh} supports various different key types, but {-rage} only supports a
|
||||
subset of these for backwards compatibility, specifically the '{-ssh-rsa}'
|
||||
and '{-ssh-ed25519}' key types. This SSH key uses the unsupported key type
|
||||
'{$key_type}'.
|
||||
|
||||
ssh-unsupported-security-key =
|
||||
Authenficateur physique SSH non supporté
|
||||
--------------------------------------
|
||||
{-openssh} version 8.2p1 a ajouté le support pour les authentificateurs physique {-fido-u2f}
|
||||
y compris les clefs de sécurité physiques telles que {-yubikeys}. {-rage} ne fonctionne pas
|
||||
avec ce type de clef SSH, parcque leur protocole ne supporte pas le chiffrement.
|
||||
Cette clef SSH est du type '{$key_type}' qui n'est pas compatible.
|
||||
|
||||
Si vous avez une clef de sécurité physique, vous devriez utiliser ce plugin:
|
||||
|
||||
{$age_plugin_yubikey_url}
|
||||
|
||||
Une clef de sécurité utilisée avec à la fois {-openssh} et ce plugin aura
|
||||
une clef SSH publique différente de sa clef destinataire {-age}, car ce plugin
|
||||
implémente le protocol {-piv}.
|
|
@ -13,6 +13,8 @@
|
|||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
|
@ -44,6 +46,16 @@ rec-deny-binary-output = Intendevi usare {-flag-armor}? {rec-detected-binary}
|
|||
|
||||
err-deny-overwrite-file = rifiuto di sovrascrivere il file esistente '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Impossibile scrivere sull'output: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Il file '{$filename}' contiene identità per '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Prova a usare '{-age-plugin-}{$plugin_name}' per convertire questa identità in destinatario.
|
||||
|
||||
err-no-identities-in-file = Nessuna identità trovata nel file '{$filename}'.
|
||||
err-no-identities-in-stdin = Nessuna identità trovata tramite standard input.
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Decifrazione fallita
|
||||
|
@ -57,6 +69,8 @@ err-header-mac-invalid = Il MAC dell'header è invalido
|
|||
|
||||
err-key-decryption = La decifrazione di una chiave crittografata è fallita
|
||||
|
||||
err-missing-recipients = Destinatari mancanti.
|
||||
|
||||
err-no-matching-keys = Nessuna chiave corrispondente trovata
|
||||
|
||||
err-unknown-format = Formato {-age} sconosciuto.
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
|
@ -44,6 +46,16 @@ rec-deny-binary-output = Возможно, вы хотели использов
|
|||
|
||||
err-deny-overwrite-file = отказ от перезаписи существующего файла '{$filename}'.
|
||||
|
||||
## Identity file errors
|
||||
|
||||
err-failed-to-write-output = Не удалось записать в выходной файл: {$err}
|
||||
|
||||
err-identity-file-contains-plugin = Файл идентификации '{$filename}' содержит идентификаторы для '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Попробуйте использовать '{-age-plugin-}{$plugin_name}' для преобразования этого идентификатора в получателя.
|
||||
|
||||
err-no-identities-in-file = Идентификаторы в файле '{$filename}' не найдены.
|
||||
err-no-identities-in-stdin = Идентификаторы в стандартном вводе не найдены.
|
||||
|
||||
## Errors
|
||||
|
||||
err-decryption-failed = Ошибка дешифрования
|
||||
|
@ -57,6 +69,8 @@ err-header-mac-invalid = Недействительный MAC заголовка
|
|||
|
||||
err-key-decryption = Не удалось расшифровать зашифрованный ключ
|
||||
|
||||
err-missing-recipients = Отсутствуют получатели.
|
||||
|
||||
err-no-matching-keys = Не найдены подходящие ключи
|
||||
|
||||
err-unknown-format = Неизвестный формат {-age}.
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
|
@ -55,6 +57,8 @@ err-header-mac-invalid = 标头消息认证码 (MAC) 无效
|
|||
|
||||
err-key-decryption = 未能解密加密密钥
|
||||
|
||||
err-missing-recipients = 缺少接收方。
|
||||
|
||||
err-no-matching-keys = 未搜索到匹配的密钥
|
||||
|
||||
err-unknown-format = 未知的 {-age} 格式。
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
-age = age
|
||||
-rage = rage
|
||||
|
||||
-scrypt-recipient = scrypt::Recipient
|
||||
|
||||
-openssh = OpenSSH
|
||||
-ssh-keygen = ssh-keygen
|
||||
-ssh-rsa = ssh-rsa
|
||||
|
@ -55,6 +57,8 @@ err-header-mac-invalid = 標頭消息認證碼 (MAC) 無效
|
|||
|
||||
err-key-decryption = 未能解密加密密鑰
|
||||
|
||||
err-missing-recipients = 缺少接收方。
|
||||
|
||||
err-no-matching-keys = 未搜索到匹配的密鑰
|
||||
|
||||
err-unknown-format = 未知的 {-age} 格式。
|
||||
|
|
|
@ -125,10 +125,10 @@ pub fn read_secret(
|
|||
input.interact()
|
||||
} else {
|
||||
// Fall back to CLI interface.
|
||||
let passphrase = prompt_password(format!("{}: ", description)).map(SecretString::new)?;
|
||||
let passphrase = prompt_password(format!("{}: ", description)).map(SecretString::from)?;
|
||||
if let Some(confirm_prompt) = confirm {
|
||||
let confirm_passphrase =
|
||||
prompt_password(format!("{}: ", confirm_prompt)).map(SecretString::new)?;
|
||||
prompt_password(format!("{}: ", confirm_prompt)).map(SecretString::from)?;
|
||||
|
||||
if !bool::from(
|
||||
passphrase
|
||||
|
@ -199,7 +199,7 @@ impl Passphrase {
|
|||
acc + "-" + s
|
||||
}
|
||||
});
|
||||
Passphrase::Generated(SecretString::new(new_passphrase))
|
||||
Passphrase::Generated(SecretString::from(new_passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use crate::{wfl, wlnfl, DecryptError};
|
||||
use crate::{wfl, DecryptError};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::wlnfl;
|
||||
|
||||
/// Errors that can occur while reading recipients or identities.
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -16,39 +16,37 @@ use crate::{fl, util::LINE_ENDING, wfl, wlnfl};
|
|||
const SHORT_OUTPUT_LENGTH: usize = 20 * 80;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DenyBinaryOutputError;
|
||||
enum FileError {
|
||||
DenyBinaryOutput,
|
||||
DenyOverwriteFile(String),
|
||||
DetectedBinaryOutput,
|
||||
InvalidFilename(String),
|
||||
MissingDirectory(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for DenyBinaryOutputError {
|
||||
impl fmt::Display for FileError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
wlnfl!(f, "err-deny-binary-output")?;
|
||||
wfl!(f, "rec-deny-binary-output")
|
||||
match self {
|
||||
Self::DenyBinaryOutput => {
|
||||
wlnfl!(f, "err-deny-binary-output")?;
|
||||
wfl!(f, "rec-deny-binary-output")
|
||||
}
|
||||
Self::DenyOverwriteFile(filename) => {
|
||||
wfl!(f, "err-deny-overwrite-file", filename = filename.as_str())
|
||||
}
|
||||
Self::DetectedBinaryOutput => {
|
||||
wlnfl!(f, "err-detected-binary")?;
|
||||
wfl!(f, "rec-detected-binary")
|
||||
}
|
||||
Self::InvalidFilename(filename) => {
|
||||
wfl!(f, "err-invalid-filename", filename = filename.as_str())
|
||||
}
|
||||
Self::MissingDirectory(path) => wfl!(f, "err-missing-directory", path = path.as_str()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DenyBinaryOutputError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DetectedBinaryOutputError;
|
||||
|
||||
impl fmt::Display for DetectedBinaryOutputError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
wlnfl!(f, "err-detected-binary")?;
|
||||
wfl!(f, "rec-detected-binary")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DetectedBinaryOutputError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DenyOverwriteFileError(String);
|
||||
|
||||
impl fmt::Display for DenyOverwriteFileError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
wfl!(f, "err-deny-overwrite-file", filename = self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for DenyOverwriteFileError {}
|
||||
impl std::error::Error for FileError {}
|
||||
|
||||
/// Wrapper around a [`File`].
|
||||
pub struct FileReader {
|
||||
|
@ -211,7 +209,7 @@ impl Write for StdoutWriter {
|
|||
if std::str::from_utf8(data).is_err() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
DetectedBinaryOutputError,
|
||||
FileError::DetectedBinaryOutput,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -353,13 +351,32 @@ impl OutputWriter {
|
|||
// Respect the Unix convention that "-" as an output filename
|
||||
// parameter is an explicit request to use standard output.
|
||||
if filename != "-" {
|
||||
let file_path = Path::new(&filename);
|
||||
|
||||
// Provide a better error if the filename is invalid, or the directory
|
||||
// containing the file does not exist (we don't automatically create
|
||||
// directories).
|
||||
if let Some(dir_path) = file_path.parent() {
|
||||
if !(dir_path == Path::new("") || dir_path.exists()) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
FileError::MissingDirectory(dir_path.display().to_string()),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
FileError::InvalidFilename(filename),
|
||||
));
|
||||
}
|
||||
|
||||
// We open the file lazily, but as we don't want the caller to assume
|
||||
// this, we eagerly confirm that the file does not exist if we can't
|
||||
// overwrite it.
|
||||
if !allow_overwrite && Path::new(&filename).exists() {
|
||||
if !allow_overwrite && file_path.exists() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::AlreadyExists,
|
||||
DenyOverwriteFileError(filename),
|
||||
FileError::DenyOverwriteFile(filename),
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -378,7 +395,10 @@ impl OutputWriter {
|
|||
} else if is_tty {
|
||||
if let OutputFormat::Binary = format {
|
||||
// If output == Some("-") then this error is skipped.
|
||||
return Err(io::Error::new(io::ErrorKind::Other, DenyBinaryOutputError));
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
FileError::DenyBinaryOutput,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
use std::io::{self, BufReader};
|
||||
|
||||
use super::{file_io::InputReader, ReadError, StdinGuard, UiCallbacks};
|
||||
use super::{ReadError, StdinGuard, UiCallbacks};
|
||||
use crate::{identity::IdentityFile, Identity};
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
use crate::armor::ArmoredReader;
|
||||
use crate::{armor::ArmoredReader, cli_common::file_io::InputReader};
|
||||
|
||||
/// Reads identities from the provided files.
|
||||
///
|
||||
|
@ -23,19 +23,21 @@ pub fn read_identities(
|
|||
max_work_factor,
|
||||
stdin_guard,
|
||||
&mut identities,
|
||||
#[cfg(feature = "armor")]
|
||||
|identities, identity| {
|
||||
identities.push(Box::new(identity));
|
||||
Ok(())
|
||||
},
|
||||
#[cfg(feature = "ssh")]
|
||||
|identities, _, identity| {
|
||||
identities.push(Box::new(identity.with_callbacks(UiCallbacks)));
|
||||
Ok(())
|
||||
},
|
||||
|identities, entry| {
|
||||
let entry = entry.into_identity(UiCallbacks);
|
||||
|identities, identity_file| {
|
||||
let new_identities = identity_file.into_identities();
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
let entry = entry.map_err(|e| match e {
|
||||
let new_identities = new_identities.map_err(|e| match e {
|
||||
#[cfg(feature = "plugin")]
|
||||
crate::DecryptError::MissingPlugin { binary_name } => {
|
||||
ReadError::MissingPlugin { binary_name }
|
||||
|
@ -48,9 +50,9 @@ pub fn read_identities(
|
|||
// IdentityFileEntry::into_identity will never return a MissingPlugin error
|
||||
// when plugin feature is not enabled.
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
let entry = entry.unwrap();
|
||||
let new_identities = new_identities.unwrap();
|
||||
|
||||
identities.push(entry);
|
||||
identities.extend(new_identities);
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|
@ -62,7 +64,7 @@ pub fn read_identities(
|
|||
/// Parses the provided identity files.
|
||||
pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
||||
filenames: Vec<String>,
|
||||
max_work_factor: Option<u8>,
|
||||
_max_work_factor: Option<u8>,
|
||||
stdin_guard: &mut StdinGuard,
|
||||
ctx: &mut Ctx,
|
||||
#[cfg(feature = "armor")] encrypted_identity: impl Fn(
|
||||
|
@ -70,17 +72,21 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
|||
crate::encrypted::Identity<ArmoredReader<BufReader<InputReader>>, UiCallbacks>,
|
||||
) -> Result<(), E>,
|
||||
#[cfg(feature = "ssh")] ssh_identity: impl Fn(&mut Ctx, &str, crate::ssh::Identity) -> Result<(), E>,
|
||||
identity_file_entry: impl Fn(&mut Ctx, crate::IdentityFileEntry) -> Result<(), E>,
|
||||
identity_file: impl Fn(&mut Ctx, crate::IdentityFile<UiCallbacks>) -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
for filename in filenames {
|
||||
let mut reader = PeekableReader::new(BufReader::new(
|
||||
stdin_guard.open(filename.clone()).map_err(|e| match e {
|
||||
#[cfg_attr(not(any(feature = "armor", feature = "ssh")), allow(unused_mut))]
|
||||
let mut reader =
|
||||
PeekableReader::new(stdin_guard.open(filename.clone()).map_err(|e| match e {
|
||||
ReadError::Io(e) if matches!(e.kind(), io::ErrorKind::NotFound) => {
|
||||
ReadError::IdentityNotFound(filename.clone())
|
||||
}
|
||||
_ => e,
|
||||
})?,
|
||||
));
|
||||
})?);
|
||||
|
||||
// Note to future self: the order in which we try parsing formats here is critical
|
||||
// to the correct behaviour of `PeekableReader::fill_buf`. See the comments in
|
||||
// that method.
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
// Try parsing as an encrypted age identity.
|
||||
|
@ -88,7 +94,7 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
|||
ArmoredReader::new_buffered(&mut reader),
|
||||
Some(filename.clone()),
|
||||
UiCallbacks,
|
||||
max_work_factor,
|
||||
_max_work_factor,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
|
@ -101,7 +107,7 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
|||
ArmoredReader::new_buffered(reader.inner),
|
||||
Some(filename.clone()),
|
||||
UiCallbacks,
|
||||
max_work_factor,
|
||||
_max_work_factor,
|
||||
)
|
||||
.expect("already parsed the age ciphertext header");
|
||||
|
||||
|
@ -132,34 +138,42 @@ pub(super) fn parse_identity_files<Ctx, E: From<ReadError> + From<io::Error>>(
|
|||
reader.reset()?;
|
||||
|
||||
// Try parsing as multiple single-line age identities.
|
||||
let identity_file = IdentityFile::from_buffer(reader)?;
|
||||
|
||||
for entry in identity_file.into_identities() {
|
||||
identity_file_entry(ctx, entry)?;
|
||||
}
|
||||
identity_file(
|
||||
ctx,
|
||||
IdentityFile::from_buffer(reader)?.with_callbacks(UiCallbacks),
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Same as default buffer size for `BufReader`, but hard-coded so we know exactly what
|
||||
/// the buffer size is, and therefore can detect if the entire file fits into a single
|
||||
/// buffer.
|
||||
///
|
||||
/// This must be at least 71 bytes to ensure the correct behaviour of
|
||||
/// `PeekableReader::fill_buf`. See the comments in that method.
|
||||
const PEEKABLE_SIZE: usize = 8 * 1024;
|
||||
|
||||
enum PeekState {
|
||||
Peeking { consumed: usize },
|
||||
Reading,
|
||||
}
|
||||
|
||||
struct PeekableReader<R: io::BufRead> {
|
||||
inner: R,
|
||||
struct PeekableReader<R: io::Read> {
|
||||
inner: BufReader<R>,
|
||||
state: PeekState,
|
||||
}
|
||||
|
||||
impl<R: io::BufRead> PeekableReader<R> {
|
||||
impl<R: io::Read> PeekableReader<R> {
|
||||
fn new(inner: R) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
inner: BufReader::with_capacity(PEEKABLE_SIZE, inner),
|
||||
state: PeekState::Peeking { consumed: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "armor", feature = "ssh"))]
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
match &mut self.state {
|
||||
PeekState::Peeking { consumed } => {
|
||||
|
@ -174,7 +188,7 @@ impl<R: io::BufRead> PeekableReader<R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<R: io::BufRead> io::Read for PeekableReader<R> {
|
||||
impl<R: io::Read> io::Read for PeekableReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.state {
|
||||
PeekState::Peeking { .. } => {
|
||||
|
@ -192,7 +206,7 @@ impl<R: io::BufRead> io::Read for PeekableReader<R> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<R: io::BufRead> io::BufRead for PeekableReader<R> {
|
||||
impl<R: io::Read> io::BufRead for PeekableReader<R> {
|
||||
fn fill_buf(&mut self) -> io::Result<&[u8]> {
|
||||
match self.state {
|
||||
PeekState::Peeking { consumed } => {
|
||||
|
@ -208,6 +222,28 @@ impl<R: io::BufRead> io::BufRead for PeekableReader<R> {
|
|||
// on `self.inner` to outside the conditional, which would prevent us
|
||||
// from performing other mutable operations on the other side.
|
||||
Ok(&self.inner.fill_buf()?[consumed..])
|
||||
} else if inner_len < PEEKABLE_SIZE {
|
||||
// We have read the entire file into a single buffer and consumed all
|
||||
// of it. Don't fall through to change the state to `Reading`, because
|
||||
// we can always reset a single-buffer stream.
|
||||
//
|
||||
// Note that we cannot distinguish between the file being the exact
|
||||
// same size as our buffer, and the file being larger than it. But
|
||||
// this only becomes relevant if we cannot distinguish between the
|
||||
// kinds of identity files we support parsing, within a single buffer.
|
||||
// We should always be able to distinguish before then, because we
|
||||
// parse in the following order:
|
||||
//
|
||||
// - Encrypted identities, which are parsed incrementally as age
|
||||
// ciphertexts with optional armor, and can be detected in at most
|
||||
// 70 bytes.
|
||||
// - SSH identities, which are parsed as a PEM encoding and can be
|
||||
// detected in at most 36 bytes.
|
||||
// - Identity files, which have one identity per line and therefore
|
||||
// can have arbitrarily long lines. We intentionally try this format
|
||||
// last.
|
||||
assert_eq!(consumed, inner_len);
|
||||
Ok(&[])
|
||||
} else {
|
||||
// We're done peeking.
|
||||
self.inner.consume(consumed);
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
use std::io::{self, BufReader};
|
||||
|
||||
use super::StdinGuard;
|
||||
use super::{identities::parse_identity_files, ReadError, UiCallbacks};
|
||||
use crate::{x25519, EncryptError, IdentityFileEntry, Recipient};
|
||||
use super::{identities::parse_identity_files, ReadError};
|
||||
use crate::identity::RecipientsAccumulator;
|
||||
use crate::{x25519, Recipient};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::plugin;
|
||||
use crate::{cli_common::UiCallbacks, plugin};
|
||||
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
use crate::ssh;
|
||||
|
||||
#[cfg(any(feature = "armor", feature = "plugin"))]
|
||||
use crate::EncryptError;
|
||||
|
||||
/// Handles error mapping for the given SSH recipient parser.
|
||||
///
|
||||
/// Returns `Ok(None)` if the parser finds a parseable value that should be ignored. This
|
||||
|
@ -44,25 +51,34 @@ where
|
|||
|
||||
/// Parses a recipient from a string.
|
||||
fn parse_recipient(
|
||||
filename: &str,
|
||||
_filename: &str,
|
||||
s: String,
|
||||
recipients: &mut Vec<Box<dyn Recipient + Send>>,
|
||||
plugin_recipients: &mut Vec<plugin::Recipient>,
|
||||
recipients: &mut RecipientsAccumulator,
|
||||
) -> Result<(), ReadError> {
|
||||
if let Ok(pk) = s.parse::<x25519::Recipient>() {
|
||||
recipients.push(Box::new(pk));
|
||||
} else if let Some(pk) = {
|
||||
#[cfg(feature = "ssh")]
|
||||
{
|
||||
parse_ssh_recipient(|| s.parse::<ssh::Recipient>(), || Ok(None), filename)?
|
||||
parse_ssh_recipient(|| s.parse::<ssh::Recipient>(), || Ok(None), _filename)?
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "ssh"))]
|
||||
None
|
||||
} {
|
||||
recipients.push(pk);
|
||||
} else if let Ok(recipient) = s.parse::<plugin::Recipient>() {
|
||||
plugin_recipients.push(recipient);
|
||||
} else if let Some(_recipient) = {
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
// TODO Do something with the error?
|
||||
s.parse::<plugin::Recipient>().ok()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
None::<Infallible>
|
||||
} {
|
||||
#[cfg(feature = "plugin")]
|
||||
recipients.push_plugin(_recipient);
|
||||
} else {
|
||||
return Err(ReadError::InvalidRecipient(s));
|
||||
}
|
||||
|
@ -74,8 +90,7 @@ fn parse_recipient(
|
|||
fn read_recipients_list<R: io::BufRead>(
|
||||
filename: &str,
|
||||
buf: R,
|
||||
recipients: &mut Vec<Box<dyn Recipient + Send>>,
|
||||
plugin_recipients: &mut Vec<plugin::Recipient>,
|
||||
recipients: &mut RecipientsAccumulator,
|
||||
) -> Result<(), ReadError> {
|
||||
for (line_number, line) in buf.lines().enumerate() {
|
||||
let line = line?;
|
||||
|
@ -83,13 +98,13 @@ fn read_recipients_list<R: io::BufRead>(
|
|||
// Skip empty lines and comments
|
||||
if line.is_empty() || line.find('#') == Some(0) {
|
||||
continue;
|
||||
} else if let Err(e) = parse_recipient(filename, line, recipients, plugin_recipients) {
|
||||
} else if let Err(_e) = parse_recipient(filename, line, recipients) {
|
||||
#[cfg(feature = "ssh")]
|
||||
match e {
|
||||
match _e {
|
||||
ReadError::RsaModulusTooLarge
|
||||
| ReadError::RsaModulusTooSmall
|
||||
| ReadError::UnsupportedKey(_, _) => {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, e.to_string()).into());
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, _e.to_string()).into());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -118,12 +133,10 @@ pub fn read_recipients(
|
|||
max_work_factor: Option<u8>,
|
||||
stdin_guard: &mut StdinGuard,
|
||||
) -> Result<Vec<Box<dyn Recipient + Send>>, ReadError> {
|
||||
let mut recipients: Vec<Box<dyn Recipient + Send>> = vec![];
|
||||
let mut plugin_recipients: Vec<plugin::Recipient> = vec![];
|
||||
let mut plugin_identities: Vec<plugin::Identity> = vec![];
|
||||
let mut recipients = RecipientsAccumulator::new();
|
||||
|
||||
for arg in recipient_strings {
|
||||
parse_recipient("", arg, &mut recipients, &mut plugin_recipients)?;
|
||||
parse_recipient("", arg, &mut recipients)?;
|
||||
}
|
||||
|
||||
for arg in recipients_file_strings {
|
||||
|
@ -134,15 +147,16 @@ pub fn read_recipients(
|
|||
_ => e,
|
||||
})?;
|
||||
let buf = BufReader::new(f);
|
||||
read_recipients_list(&arg, buf, &mut recipients, &mut plugin_recipients)?;
|
||||
read_recipients_list(&arg, buf, &mut recipients)?;
|
||||
}
|
||||
|
||||
parse_identity_files::<_, ReadError>(
|
||||
identity_strings,
|
||||
max_work_factor,
|
||||
stdin_guard,
|
||||
&mut (&mut recipients, &mut plugin_identities),
|
||||
|(recipients, _), identity| {
|
||||
&mut recipients,
|
||||
#[cfg(feature = "armor")]
|
||||
|recipients, identity| {
|
||||
recipients.extend(identity.recipients().map_err(|e| {
|
||||
// Only one error can occur here.
|
||||
if let EncryptError::EncryptedIdentities(e) = e {
|
||||
|
@ -153,7 +167,8 @@ pub fn read_recipients(
|
|||
})?);
|
||||
Ok(())
|
||||
},
|
||||
|(recipients, _), filename, identity| {
|
||||
#[cfg(feature = "ssh")]
|
||||
|recipients, filename, identity| {
|
||||
let recipient = parse_ssh_recipient(
|
||||
|| ssh::Recipient::try_from(identity),
|
||||
|| Err(ReadError::InvalidRecipient(filename.to_owned())),
|
||||
|
@ -163,43 +178,29 @@ pub fn read_recipients(
|
|||
recipients.push(recipient);
|
||||
Ok(())
|
||||
},
|
||||
|(recipients, plugin_identities), entry| {
|
||||
match entry {
|
||||
IdentityFileEntry::Native(i) => recipients.push(Box::new(i.to_public())),
|
||||
IdentityFileEntry::Plugin(i) => plugin_identities.push(i),
|
||||
}
|
||||
|recipients, identity_file| {
|
||||
recipients.with_identities(identity_file);
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
|
||||
// Collect the names of the required plugins.
|
||||
let mut plugin_names = plugin_recipients
|
||||
.iter()
|
||||
.map(|r| r.plugin())
|
||||
.chain(plugin_identities.iter().map(|i| i.plugin()))
|
||||
.collect::<Vec<_>>();
|
||||
plugin_names.sort_unstable();
|
||||
plugin_names.dedup();
|
||||
|
||||
// Find the required plugins.
|
||||
for plugin_name in plugin_names {
|
||||
recipients.push(Box::new(
|
||||
plugin::RecipientPluginV1::new(
|
||||
plugin_name,
|
||||
&plugin_recipients,
|
||||
&plugin_identities,
|
||||
UiCallbacks,
|
||||
)
|
||||
.map_err(|e| {
|
||||
// Only one error can occur here.
|
||||
if let EncryptError::MissingPlugin { binary_name } = e {
|
||||
recipients
|
||||
.build(
|
||||
#[cfg(feature = "plugin")]
|
||||
UiCallbacks,
|
||||
)
|
||||
.map_err(|_e| {
|
||||
// Only one error can occur here.
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
if let EncryptError::MissingPlugin { binary_name } = _e {
|
||||
ReadError::MissingPlugin { binary_name }
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
})?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(recipients)
|
||||
#[cfg(not(feature = "plugin"))]
|
||||
unreachable!()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,18 +2,16 @@
|
|||
|
||||
use std::{cell::Cell, io};
|
||||
|
||||
use crate::{
|
||||
decryptor::PassphraseDecryptor, fl, Callbacks, DecryptError, Decryptor, EncryptError,
|
||||
IdentityFile, IdentityFileEntry,
|
||||
};
|
||||
use crate::{fl, scrypt, Callbacks, DecryptError, Decryptor, EncryptError, IdentityFile};
|
||||
|
||||
/// The state of the encrypted age identity.
|
||||
enum IdentityState<R: io::Read> {
|
||||
enum IdentityState<R: io::Read, C: Callbacks> {
|
||||
Encrypted {
|
||||
decryptor: PassphraseDecryptor<R>,
|
||||
decryptor: Decryptor<R>,
|
||||
max_work_factor: Option<u8>,
|
||||
callbacks: C,
|
||||
},
|
||||
Decrypted(Vec<IdentityFileEntry>),
|
||||
Decrypted(IdentityFile<C>),
|
||||
|
||||
/// The file was not correctly encrypted, or did not contain age identities. We cache
|
||||
/// this error in case the caller tries to use this identity again. The `Option` is to
|
||||
|
@ -22,26 +20,23 @@ enum IdentityState<R: io::Read> {
|
|||
Poisoned(Option<DecryptError>),
|
||||
}
|
||||
|
||||
impl<R: io::Read> Default for IdentityState<R> {
|
||||
impl<R: io::Read, C: Callbacks> Default for IdentityState<R, C> {
|
||||
fn default() -> Self {
|
||||
Self::Poisoned(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: io::Read> IdentityState<R> {
|
||||
impl<R: io::Read, C: Callbacks> IdentityState<R, C> {
|
||||
/// Decrypts this encrypted identity if necessary.
|
||||
///
|
||||
/// Returns the (possibly cached) identities, and a boolean marking if the identities
|
||||
/// were not cached (and we just asked the user for a passphrase).
|
||||
fn decrypt<C: Callbacks>(
|
||||
self,
|
||||
filename: Option<&str>,
|
||||
callbacks: C,
|
||||
) -> Result<(Vec<IdentityFileEntry>, bool), DecryptError> {
|
||||
fn decrypt(self, filename: Option<&str>) -> Result<(IdentityFile<C>, bool), DecryptError> {
|
||||
match self {
|
||||
Self::Encrypted {
|
||||
decryptor,
|
||||
max_work_factor,
|
||||
callbacks,
|
||||
} => {
|
||||
let passphrase = match callbacks.request_passphrase(&fl!(
|
||||
"encrypted-passphrase-prompt",
|
||||
|
@ -51,8 +46,13 @@ impl<R: io::Read> IdentityState<R> {
|
|||
None => todo!(),
|
||||
};
|
||||
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
if let Some(max_work_factor) = max_work_factor {
|
||||
identity.set_max_work_factor(max_work_factor);
|
||||
}
|
||||
|
||||
decryptor
|
||||
.decrypt(&passphrase, max_work_factor)
|
||||
.decrypt(Some(&identity as _).into_iter())
|
||||
.map_err(|e| {
|
||||
if matches!(e, DecryptError::DecryptionFailed) {
|
||||
DecryptError::KeyDecryptionFailed
|
||||
|
@ -61,11 +61,12 @@ impl<R: io::Read> IdentityState<R> {
|
|||
}
|
||||
})
|
||||
.and_then(|stream| {
|
||||
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?;
|
||||
Ok((file.into_identities(), true))
|
||||
let file = IdentityFile::from_buffer(io::BufReader::new(stream))?
|
||||
.with_callbacks(callbacks);
|
||||
Ok((file, true))
|
||||
})
|
||||
}
|
||||
Self::Decrypted(identities) => Ok((identities, false)),
|
||||
Self::Decrypted(identity_file) => Ok((identity_file, false)),
|
||||
// `IdentityState::decrypt` is only ever called with `Some`.
|
||||
Self::Poisoned(e) => Err(e.unwrap()),
|
||||
}
|
||||
|
@ -74,9 +75,8 @@ impl<R: io::Read> IdentityState<R> {
|
|||
|
||||
/// An encrypted age identity file.
|
||||
pub struct Identity<R: io::Read, C: Callbacks> {
|
||||
state: Cell<IdentityState<R>>,
|
||||
state: Cell<IdentityState<R, C>>,
|
||||
filename: Option<String>,
|
||||
callbacks: C,
|
||||
}
|
||||
|
||||
impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
||||
|
@ -92,17 +92,15 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
|||
callbacks: C,
|
||||
max_work_factor: Option<u8>,
|
||||
) -> Result<Option<Self>, DecryptError> {
|
||||
match Decryptor::new(data)? {
|
||||
Decryptor::Recipients(_) => Ok(None),
|
||||
Decryptor::Passphrase(decryptor) => Ok(Some(Identity {
|
||||
state: Cell::new(IdentityState::Encrypted {
|
||||
decryptor,
|
||||
max_work_factor,
|
||||
}),
|
||||
filename,
|
||||
let decryptor = Decryptor::new(data)?;
|
||||
Ok(decryptor.is_scrypt().then_some(Identity {
|
||||
state: Cell::new(IdentityState::Encrypted {
|
||||
decryptor,
|
||||
max_work_factor,
|
||||
callbacks,
|
||||
})),
|
||||
}
|
||||
}),
|
||||
filename,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns the recipients contained within this encrypted identity.
|
||||
|
@ -110,18 +108,10 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
|||
/// If this encrypted identity has not been decrypted yet, calling this method will
|
||||
/// trigger a passphrase request.
|
||||
pub fn recipients(&self) -> Result<Vec<Box<dyn crate::Recipient + Send>>, EncryptError> {
|
||||
match self
|
||||
.state
|
||||
.take()
|
||||
.decrypt(self.filename.as_deref(), self.callbacks.clone())
|
||||
{
|
||||
Ok((identities, _)) => {
|
||||
let recipients = identities
|
||||
.iter()
|
||||
.map(|entry| entry.to_recipient(self.callbacks.clone()))
|
||||
.collect::<Result<Vec<_>, _>>();
|
||||
|
||||
self.state.set(IdentityState::Decrypted(identities));
|
||||
match self.state.take().decrypt(self.filename.as_deref()) {
|
||||
Ok((identity_file, _)) => {
|
||||
let recipients = identity_file.to_recipients();
|
||||
self.state.set(IdentityState::Decrypted(identity_file));
|
||||
recipients
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -151,27 +141,20 @@ impl<R: io::Read, C: Callbacks> Identity<R, C> {
|
|||
Result<Box<dyn crate::Identity>, DecryptError>,
|
||||
) -> Option<Result<age_core::format::FileKey, DecryptError>>,
|
||||
{
|
||||
match self
|
||||
.state
|
||||
.take()
|
||||
.decrypt(self.filename.as_deref(), self.callbacks.clone())
|
||||
{
|
||||
Ok((identities, requested_passphrase)) => {
|
||||
let result = identities
|
||||
.iter()
|
||||
.map(|entry| entry.clone().into_identity(self.callbacks.clone()))
|
||||
.find_map(filter);
|
||||
match self.state.take().decrypt(self.filename.as_deref()) {
|
||||
Ok((identity_file, requested_passphrase)) => {
|
||||
let result = identity_file.to_identities().find_map(filter);
|
||||
|
||||
// If we requested a passphrase to decrypt, and none of the identities
|
||||
// matched, warn the user.
|
||||
if requested_passphrase && result.is_none() {
|
||||
self.callbacks.display_message(&fl!(
|
||||
identity_file.callbacks.display_message(&fl!(
|
||||
"encrypted-warn-no-match",
|
||||
filename = self.filename.as_deref().unwrap_or_default()
|
||||
));
|
||||
}
|
||||
|
||||
self.state.set(IdentityState::Decrypted(identities));
|
||||
self.state.set(IdentityState::Decrypted(identity_file));
|
||||
result
|
||||
}
|
||||
Err(e) => {
|
||||
|
@ -256,7 +239,7 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=
|
|||
|
||||
/// This intentionally panics if called twice.
|
||||
fn request_passphrase(&self, _: &str) -> Option<SecretString> {
|
||||
Some(SecretString::new(
|
||||
Some(SecretString::from(
|
||||
self.0.lock().unwrap().take().unwrap().to_owned(),
|
||||
))
|
||||
}
|
||||
|
@ -265,9 +248,12 @@ fOrxrKTj7xCdNS3+OrCdnBC8Z9cKDxjCGWW3fkjLsYha0Jo=
|
|||
#[test]
|
||||
#[cfg(feature = "armor")]
|
||||
fn round_trip() {
|
||||
use age_core::format::FileKey;
|
||||
|
||||
let pk: x25519::Recipient = TEST_RECIPIENT.parse().unwrap();
|
||||
let file_key = [12; 16].into();
|
||||
let wrapped = pk.wrap_file_key(&file_key).unwrap();
|
||||
let file_key = FileKey::new(Box::new([12; 16]));
|
||||
let (wrapped, labels) = pk.wrap_file_key(&file_key).unwrap();
|
||||
assert!(labels.is_empty());
|
||||
|
||||
// Unwrapping with the wrong passphrase fails.
|
||||
{
|
||||
|
|
138
age/src/error.rs
138
age/src/error.rs
|
@ -1,5 +1,6 @@
|
|||
//! Error type.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
|
@ -8,6 +9,72 @@ use crate::{wfl, wlnfl};
|
|||
#[cfg(feature = "plugin")]
|
||||
use age_core::format::Stanza;
|
||||
|
||||
/// Errors returned when converting an identity file to a recipients file.
|
||||
#[derive(Debug)]
|
||||
pub enum IdentityFileConvertError {
|
||||
/// An I/O error occurred while writing out a recipient corresponding to an identity
|
||||
/// in this file.
|
||||
FailedToWriteOutput(io::Error),
|
||||
/// The identity file contains a plugin identity, which can be converted to a
|
||||
/// recipient for encryption purposes, but not for writing a recipients file.
|
||||
#[cfg(feature = "plugin")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
|
||||
IdentityFileContainsPlugin {
|
||||
/// The given identity file.
|
||||
filename: Option<String>,
|
||||
/// The name of the plugin.
|
||||
plugin_name: String,
|
||||
},
|
||||
/// The identity file contains no identities, and thus cannot be used to produce a
|
||||
/// recipients file.
|
||||
NoIdentities {
|
||||
/// The given identity file.
|
||||
filename: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for IdentityFileConvertError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
IdentityFileConvertError::FailedToWriteOutput(e) => {
|
||||
wfl!(f, "err-failed-to-write-output", err = e.to_string())
|
||||
}
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileConvertError::IdentityFileContainsPlugin {
|
||||
filename,
|
||||
plugin_name,
|
||||
} => {
|
||||
wlnfl!(
|
||||
f,
|
||||
"err-identity-file-contains-plugin",
|
||||
filename = filename.as_deref().unwrap_or_default(),
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)?;
|
||||
wfl!(
|
||||
f,
|
||||
"rec-identity-file-contains-plugin",
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)
|
||||
}
|
||||
IdentityFileConvertError::NoIdentities { filename } => match filename {
|
||||
Some(filename) => {
|
||||
wfl!(f, "err-no-identities-in-file", filename = filename.as_str())
|
||||
}
|
||||
None => wfl!(f, "err-no-identities-in-stdin"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IdentityFileConvertError {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
IdentityFileConvertError::FailedToWriteOutput(e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors returned by a plugin.
|
||||
#[cfg(feature = "plugin")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
|
||||
|
@ -101,6 +168,18 @@ impl fmt::Display for PluginError {
|
|||
pub enum EncryptError {
|
||||
/// An error occured while decrypting passphrase-encrypted identities.
|
||||
EncryptedIdentities(DecryptError),
|
||||
/// The encryptor was given recipients that declare themselves incompatible.
|
||||
IncompatibleRecipients {
|
||||
/// The set of labels from the first recipient provided to the encryptor.
|
||||
l_labels: HashSet<String>,
|
||||
/// The set of labels from the first non-matching recipient.
|
||||
r_labels: HashSet<String>,
|
||||
},
|
||||
/// One or more of the labels from the first recipient provided to the encryptor are
|
||||
/// invalid.
|
||||
///
|
||||
/// Labels must be valid age "arbitrary string"s (`1*VCHAR` in ABNF).
|
||||
InvalidRecipientLabels(HashSet<String>),
|
||||
/// An I/O error occurred during encryption.
|
||||
Io(io::Error),
|
||||
/// A required plugin could not be found.
|
||||
|
@ -110,6 +189,12 @@ pub enum EncryptError {
|
|||
/// The plugin's binary name.
|
||||
binary_name: String,
|
||||
},
|
||||
/// The encryptor was not given any recipients.
|
||||
MissingRecipients,
|
||||
/// [`scrypt::Recipient`] was mixed with other recipient types.
|
||||
///
|
||||
/// [`scrypt::Recipient`]: crate::scrypt::Recipient
|
||||
MixedRecipientAndPassphrase,
|
||||
/// Errors from a plugin.
|
||||
#[cfg(feature = "plugin")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "plugin")))]
|
||||
|
@ -126,27 +211,79 @@ impl Clone for EncryptError {
|
|||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::EncryptedIdentities(e) => Self::EncryptedIdentities(e.clone()),
|
||||
Self::IncompatibleRecipients { l_labels, r_labels } => Self::IncompatibleRecipients {
|
||||
l_labels: l_labels.clone(),
|
||||
r_labels: r_labels.clone(),
|
||||
},
|
||||
Self::InvalidRecipientLabels(labels) => Self::InvalidRecipientLabels(labels.clone()),
|
||||
Self::Io(e) => Self::Io(io::Error::new(e.kind(), e.to_string())),
|
||||
#[cfg(feature = "plugin")]
|
||||
Self::MissingPlugin { binary_name } => Self::MissingPlugin {
|
||||
binary_name: binary_name.clone(),
|
||||
},
|
||||
Self::MissingRecipients => Self::MissingRecipients,
|
||||
Self::MixedRecipientAndPassphrase => Self::MixedRecipientAndPassphrase,
|
||||
#[cfg(feature = "plugin")]
|
||||
Self::Plugin(e) => Self::Plugin(e.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_labels(labels: &HashSet<String>) -> String {
|
||||
let mut s = String::new();
|
||||
for (i, label) in labels.iter().enumerate() {
|
||||
s.push_str(label);
|
||||
if i != 0 {
|
||||
s.push_str(", ");
|
||||
}
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
EncryptError::EncryptedIdentities(e) => e.fmt(f),
|
||||
EncryptError::IncompatibleRecipients { l_labels, r_labels } => {
|
||||
match (l_labels.is_empty(), r_labels.is_empty()) {
|
||||
(true, true) => unreachable!("labels are compatible"),
|
||||
(false, true) => {
|
||||
wfl!(
|
||||
f,
|
||||
"err-incompatible-recipients-oneway",
|
||||
labels = print_labels(l_labels),
|
||||
)
|
||||
}
|
||||
(true, false) => {
|
||||
wfl!(
|
||||
f,
|
||||
"err-incompatible-recipients-oneway",
|
||||
labels = print_labels(r_labels),
|
||||
)
|
||||
}
|
||||
(false, false) => wfl!(
|
||||
f,
|
||||
"err-incompatible-recipients-twoway",
|
||||
left = print_labels(l_labels),
|
||||
right = print_labels(r_labels),
|
||||
),
|
||||
}
|
||||
}
|
||||
EncryptError::InvalidRecipientLabels(labels) => wfl!(
|
||||
f,
|
||||
"err-invalid-recipient-labels",
|
||||
labels = print_labels(labels),
|
||||
),
|
||||
EncryptError::Io(e) => e.fmt(f),
|
||||
#[cfg(feature = "plugin")]
|
||||
EncryptError::MissingPlugin { binary_name } => {
|
||||
wlnfl!(f, "err-missing-plugin", plugin_name = binary_name.as_str())?;
|
||||
wfl!(f, "rec-missing-plugin")
|
||||
}
|
||||
EncryptError::MissingRecipients => wfl!(f, "err-missing-recipients"),
|
||||
EncryptError::MixedRecipientAndPassphrase => {
|
||||
wfl!(f, "err-mixed-recipient-passphrase")
|
||||
}
|
||||
#[cfg(feature = "plugin")]
|
||||
EncryptError::Plugin(errors) => match &errors[..] {
|
||||
[] => unreachable!(),
|
||||
|
@ -168,7 +305,6 @@ impl std::error::Error for EncryptError {
|
|||
match self {
|
||||
EncryptError::EncryptedIdentities(inner) => Some(inner),
|
||||
EncryptError::Io(inner) => Some(inner),
|
||||
#[cfg(feature = "plugin")]
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
//! The age file format.
|
||||
|
||||
use age_core::format::Stanza;
|
||||
use std::io::{self, BufRead, Read, Write};
|
||||
|
||||
use age_core::format::{grease_the_joint, Stanza};
|
||||
|
||||
use crate::{
|
||||
error::DecryptError,
|
||||
primitives::{HmacKey, HmacWriter},
|
||||
scrypt, EncryptError,
|
||||
};
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
|
@ -32,13 +34,22 @@ pub(crate) struct HeaderV1 {
|
|||
}
|
||||
|
||||
impl HeaderV1 {
|
||||
pub(crate) fn new(recipients: Vec<Stanza>, mac_key: HmacKey) -> Self {
|
||||
pub(crate) fn new(recipients: Vec<Stanza>, mac_key: HmacKey) -> Result<Self, EncryptError> {
|
||||
let mut header = HeaderV1 {
|
||||
recipients,
|
||||
mac: [0; 32],
|
||||
encoded_bytes: None,
|
||||
};
|
||||
|
||||
if header.no_scrypt() {
|
||||
// Keep the joint well oiled!
|
||||
header.recipients.push(grease_the_joint());
|
||||
}
|
||||
|
||||
if !header.is_valid() {
|
||||
return Err(EncryptError::MixedRecipientAndPassphrase);
|
||||
}
|
||||
|
||||
let mut mac = HmacWriter::new(mac_key);
|
||||
cookie_factory::gen(write::header_v1_minus_mac(&header), &mut mac)
|
||||
.expect("can serialize Header into HmacWriter");
|
||||
|
@ -46,7 +57,7 @@ impl HeaderV1 {
|
|||
.mac
|
||||
.copy_from_slice(mac.finalize().into_bytes().as_slice());
|
||||
|
||||
header
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_mac(&self, mac_key: HmacKey) -> Result<(), hmac::digest::MacError> {
|
||||
|
@ -61,6 +72,33 @@ impl HeaderV1 {
|
|||
}
|
||||
mac.verify(&self.mac)
|
||||
}
|
||||
|
||||
fn any_scrypt(&self) -> bool {
|
||||
self.recipients
|
||||
.iter()
|
||||
.any(|r| r.tag == scrypt::SCRYPT_RECIPIENT_TAG)
|
||||
}
|
||||
|
||||
/// Checks whether the header contains a single recipient of type `scrypt`.
|
||||
///
|
||||
/// This can be used along with [`Self::no_scrypt`] to enforce the structural
|
||||
/// requirements on the v1 header.
|
||||
pub(crate) fn valid_scrypt(&self) -> bool {
|
||||
self.any_scrypt() && self.recipients.len() == 1
|
||||
}
|
||||
|
||||
/// Checks whether the header contains no `scrypt` recipients.
|
||||
///
|
||||
/// This can be used along with [`Self::valid_scrypt`] to enforce the structural
|
||||
/// requirements on the v1 header.
|
||||
pub(crate) fn no_scrypt(&self) -> bool {
|
||||
!self.any_scrypt()
|
||||
}
|
||||
|
||||
/// Enforces structural requirements on the v1 header.
|
||||
pub(crate) fn is_valid(&self) -> bool {
|
||||
self.valid_scrypt() || self.no_scrypt()
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
|
|
|
@ -16,7 +16,7 @@ lazy_static! {
|
|||
// Ensure that the fallback language is always loaded, even if the library user
|
||||
// doesn't call `localizer().select(languages)`.
|
||||
let fallback: LanguageIdentifier = "en-US".parse().unwrap();
|
||||
language_loader.load_languages(&Localizations, &[&fallback]).unwrap();
|
||||
language_loader.load_languages(&Localizations, &[fallback]).unwrap();
|
||||
language_loader
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::fs::File;
|
||||
use std::io;
|
||||
|
||||
use crate::{x25519, Callbacks, DecryptError, EncryptError};
|
||||
use crate::{x25519, Callbacks, DecryptError, EncryptError, IdentityFileConvertError, NoCallbacks};
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
use crate::cli_common::file_io::InputReader;
|
||||
|
@ -11,7 +11,7 @@ use crate::plugin;
|
|||
|
||||
/// The supported kinds of identities within an [`IdentityFile`].
|
||||
#[derive(Clone)]
|
||||
pub enum IdentityFileEntry {
|
||||
enum IdentityFileEntry {
|
||||
/// The standard age identity type.
|
||||
Native(x25519::Identity),
|
||||
/// A plugin-compatible identity.
|
||||
|
@ -29,38 +29,25 @@ impl IdentityFileEntry {
|
|||
match self {
|
||||
IdentityFileEntry::Native(i) => Ok(Box::new(i)),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => Ok(Box::new(crate::plugin::IdentityPluginV1::new(
|
||||
i.plugin(),
|
||||
&[i.clone()],
|
||||
callbacks,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub(crate) fn to_recipient(
|
||||
&self,
|
||||
callbacks: impl Callbacks,
|
||||
) -> Result<Box<dyn crate::Recipient + Send>, EncryptError> {
|
||||
match self {
|
||||
IdentityFileEntry::Native(i) => Ok(Box::new(i.to_public())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => Ok(Box::new(crate::plugin::RecipientPluginV1::new(
|
||||
i.plugin(),
|
||||
&[],
|
||||
&[i.clone()],
|
||||
callbacks,
|
||||
)?)),
|
||||
IdentityFileEntry::Plugin(i) => Ok(Box::new(
|
||||
crate::plugin::Plugin::new(i.plugin())
|
||||
.map_err(|binary_name| DecryptError::MissingPlugin { binary_name })
|
||||
.map(|plugin| {
|
||||
crate::plugin::IdentityPluginV1::from_parts(plugin, vec![i], callbacks)
|
||||
})?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of identities that has been parsed from some input file.
|
||||
pub struct IdentityFile {
|
||||
pub struct IdentityFile<C: Callbacks> {
|
||||
filename: Option<String>,
|
||||
identities: Vec<IdentityFileEntry>,
|
||||
pub(crate) callbacks: C,
|
||||
}
|
||||
|
||||
impl IdentityFile {
|
||||
impl IdentityFile<NoCallbacks> {
|
||||
/// Parses one or more identities from a file containing valid UTF-8.
|
||||
pub fn from_file(filename: String) -> io::Result<Self> {
|
||||
File::open(&filename)
|
||||
|
@ -129,12 +116,177 @@ impl IdentityFile {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(IdentityFile { identities })
|
||||
Ok(IdentityFile {
|
||||
filename,
|
||||
identities,
|
||||
callbacks: NoCallbacks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: Callbacks> IdentityFile<C> {
|
||||
/// Sets the provided callbacks on this identity file, so that if this is an encrypted
|
||||
/// identity, it can potentially be decrypted.
|
||||
pub fn with_callbacks<D: Callbacks>(self, callbacks: D) -> IdentityFile<D> {
|
||||
IdentityFile {
|
||||
filename: self.filename,
|
||||
identities: self.identities,
|
||||
callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a recipients file containing the recipients corresponding to the identities
|
||||
/// in this file.
|
||||
///
|
||||
/// Returns an error if this file is empty, or if it contains plugin identities (which
|
||||
/// can only be converted by the plugin binary itself).
|
||||
pub fn write_recipients_file<W: io::Write>(
|
||||
&self,
|
||||
mut output: W,
|
||||
) -> Result<(), IdentityFileConvertError> {
|
||||
if self.identities.is_empty() {
|
||||
return Err(IdentityFileConvertError::NoIdentities {
|
||||
filename: self.filename.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
for identity in &self.identities {
|
||||
match identity {
|
||||
IdentityFileEntry::Native(sk) => writeln!(output, "{}", sk.to_public())
|
||||
.map_err(IdentityFileConvertError::FailedToWriteOutput)?,
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(id) => {
|
||||
return Err(IdentityFileConvertError::IdentityFileContainsPlugin {
|
||||
filename: self.filename.clone(),
|
||||
plugin_name: id.plugin().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns recipients for the identities in this file.
|
||||
///
|
||||
/// Plugin identities will be merged into one [`Recipient`] per unique plugin.
|
||||
///
|
||||
/// [`Recipient`]: crate::Recipient
|
||||
pub fn to_recipients(&self) -> Result<Vec<Box<dyn crate::Recipient + Send>>, EncryptError> {
|
||||
let mut recipients = RecipientsAccumulator::new();
|
||||
recipients.with_identities_ref(self);
|
||||
recipients.build(
|
||||
#[cfg(feature = "plugin")]
|
||||
self.callbacks.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the identities in this file.
|
||||
pub fn into_identities(self) -> Vec<IdentityFileEntry> {
|
||||
pub(crate) fn to_identities(
|
||||
&self,
|
||||
) -> impl Iterator<Item = Result<Box<dyn crate::Identity>, DecryptError>> + '_ {
|
||||
self.identities
|
||||
.iter()
|
||||
.map(|entry| entry.clone().into_identity(self.callbacks.clone()))
|
||||
}
|
||||
|
||||
/// Returns the identities in this file.
|
||||
pub fn into_identities(self) -> Result<Vec<Box<dyn crate::Identity>>, DecryptError> {
|
||||
self.identities
|
||||
.into_iter()
|
||||
.map(|entry| entry.into_identity(self.callbacks.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RecipientsAccumulator {
|
||||
recipients: Vec<Box<dyn crate::Recipient + Send>>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_recipients: Vec<plugin::Recipient>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_identities: Vec<plugin::Identity>,
|
||||
}
|
||||
|
||||
impl RecipientsAccumulator {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
recipients: vec![],
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_recipients: vec![],
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_identities: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
pub(crate) fn push(&mut self, recipient: Box<dyn crate::Recipient + Send>) {
|
||||
self.recipients.push(recipient);
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(crate) fn push_plugin(&mut self, recipient: plugin::Recipient) {
|
||||
self.plugin_recipients.push(recipient);
|
||||
}
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
pub(crate) fn extend(
|
||||
&mut self,
|
||||
iter: impl IntoIterator<Item = Box<dyn crate::Recipient + Send>>,
|
||||
) {
|
||||
self.recipients.extend(iter);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
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())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => self.plugin_identities.push(i),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())),
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(i) => self.plugin_identities.push(i.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "plugin"), allow(unused_mut))]
|
||||
pub(crate) fn build(
|
||||
mut self,
|
||||
#[cfg(feature = "plugin")] callbacks: impl Callbacks,
|
||||
) -> Result<Vec<Box<dyn crate::Recipient + Send>>, EncryptError> {
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
// Collect the names of the required plugins.
|
||||
let mut plugin_names = self
|
||||
.plugin_recipients
|
||||
.iter()
|
||||
.map(|r| r.plugin())
|
||||
.chain(self.plugin_identities.iter().map(|i| i.plugin()))
|
||||
.collect::<Vec<_>>();
|
||||
plugin_names.sort_unstable();
|
||||
plugin_names.dedup();
|
||||
|
||||
// Find the required plugins.
|
||||
for plugin_name in plugin_names {
|
||||
self.recipients
|
||||
.push(Box::new(plugin::RecipientPluginV1::new(
|
||||
plugin_name,
|
||||
&self.plugin_recipients,
|
||||
&self.plugin_identities,
|
||||
callbacks.clone(),
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.recipients)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
use age_core::{
|
||||
format::FileKey,
|
||||
primitives::hkdf,
|
||||
secrecy::{ExposeSecret, Secret},
|
||||
secrecy::{ExposeSecret, SecretBox},
|
||||
};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
|
||||
|
@ -18,17 +18,15 @@ const HEADER_KEY_LABEL: &[u8] = b"header";
|
|||
const PAYLOAD_KEY_LABEL: &[u8] = b"payload";
|
||||
|
||||
pub(crate) fn new_file_key() -> FileKey {
|
||||
let mut file_key = [0; 16];
|
||||
OsRng.fill_bytes(&mut file_key);
|
||||
file_key.into()
|
||||
FileKey::init_with_mut(|file_key| OsRng.fill_bytes(file_key))
|
||||
}
|
||||
|
||||
pub(crate) fn mac_key(file_key: &FileKey) -> HmacKey {
|
||||
HmacKey(Secret::new(hkdf(
|
||||
HmacKey(SecretBox::new(Box::new(hkdf(
|
||||
&[],
|
||||
HEADER_KEY_LABEL,
|
||||
file_key.expose_secret(),
|
||||
)))
|
||||
))))
|
||||
}
|
||||
|
||||
pub(crate) fn v1_payload_key(
|
||||
|
|
229
age/src/lib.rs
229
age/src/lib.rs
|
@ -8,8 +8,10 @@
|
|||
//! There are several ways to use these:
|
||||
//! - For most cases (including programmatic usage), use [`Encryptor::with_recipients`]
|
||||
//! with [`x25519::Recipient`], and [`Decryptor`] with [`x25519::Identity`].
|
||||
//! - APIs are available for passphrase-based encryption and decryption. These should
|
||||
//! only be used with passphrases that were provided by (or generated for) a human.
|
||||
//! - 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)
|
||||
//! a human.
|
||||
//! - For compatibility with existing SSH keys, enable the `ssh` feature flag, and use
|
||||
//! [`ssh::Recipient`] and [`ssh::Identity`].
|
||||
//!
|
||||
|
@ -25,7 +27,76 @@
|
|||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ## Recipient-based encryption
|
||||
//! ## Streamlined APIs
|
||||
//!
|
||||
//! These are useful when you only need to encrypt to a single recipient, and the data is
|
||||
//! small enough to fit in memory.
|
||||
//!
|
||||
//! ### Recipient-based encryption
|
||||
//!
|
||||
//! ```
|
||||
//! # fn run_main() -> Result<(), ()> {
|
||||
//! let key = age::x25519::Identity::generate();
|
||||
//! let pubkey = key.to_public();
|
||||
//!
|
||||
//! let plaintext = b"Hello world!";
|
||||
//!
|
||||
//! # fn encrypt(pubkey: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
//! let encrypted = age::encrypt(&pubkey, plaintext)?;
|
||||
//! # Ok(encrypted)
|
||||
//! # }
|
||||
//! # fn decrypt(key: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
//! let decrypted = age::decrypt(&key, &encrypted)?;
|
||||
//! # Ok(decrypted)
|
||||
//! # }
|
||||
//! # let decrypted = decrypt(
|
||||
//! # key,
|
||||
//! # encrypt(pubkey, &plaintext[..]).map_err(|_| ())?
|
||||
//! # ).map_err(|_| ())?;
|
||||
//!
|
||||
//! assert_eq!(decrypted, plaintext);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # run_main().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Passphrase-based encryption
|
||||
//!
|
||||
//! ```
|
||||
//! use age::secrecy::SecretString;
|
||||
//!
|
||||
//! # fn run_main() -> Result<(), ()> {
|
||||
//! let passphrase = SecretString::from("this is not a good passphrase".to_owned());
|
||||
//! let recipient = age::scrypt::Recipient::new(passphrase.clone());
|
||||
//! let identity = age::scrypt::Identity::new(passphrase);
|
||||
//!
|
||||
//! let plaintext = b"Hello world!";
|
||||
//!
|
||||
//! # fn encrypt(recipient: age::scrypt::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
//! let encrypted = age::encrypt(&recipient, plaintext)?;
|
||||
//! # Ok(encrypted)
|
||||
//! # }
|
||||
//! # fn decrypt(identity: age::scrypt::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
//! let decrypted = age::decrypt(&identity, &encrypted)?;
|
||||
//! # Ok(decrypted)
|
||||
//! # }
|
||||
//! # let decrypted = decrypt(
|
||||
//! # identity,
|
||||
//! # encrypt(recipient, &plaintext[..]).map_err(|_| ())?
|
||||
//! # ).map_err(|_| ())?;
|
||||
//!
|
||||
//! assert_eq!(decrypted, plaintext);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # run_main().unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! ## Full APIs
|
||||
//!
|
||||
//! The full APIs support encrypting to multiple recipients, streaming the data, and have
|
||||
//! async I/O options.
|
||||
//!
|
||||
//! ### Recipient-based encryption
|
||||
//!
|
||||
//! ```
|
||||
//! use std::io::{Read, Write};
|
||||
|
@ -40,7 +111,7 @@
|
|||
//! // Encrypt the plaintext to a ciphertext...
|
||||
//! # fn encrypt(pubkey: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
//! let encrypted = {
|
||||
//! let encryptor = age::Encryptor::with_recipients(vec![Box::new(pubkey)])
|
||||
//! let encryptor = age::Encryptor::with_recipients(iter::once(&pubkey as _))
|
||||
//! .expect("we provided a recipient");
|
||||
//!
|
||||
//! let mut encrypted = vec![];
|
||||
|
@ -56,10 +127,7 @@
|
|||
//! // ... and decrypt the obtained ciphertext to the plaintext again.
|
||||
//! # fn decrypt(key: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
//! let decrypted = {
|
||||
//! let decryptor = match age::Decryptor::new(&encrypted[..])? {
|
||||
//! age::Decryptor::Recipients(d) => d,
|
||||
//! _ => unreachable!(),
|
||||
//! };
|
||||
//! let decryptor = age::Decryptor::new(&encrypted[..])?;
|
||||
//!
|
||||
//! let mut decrypted = vec![];
|
||||
//! let mut reader = decryptor.decrypt(iter::once(&key as &dyn age::Identity))?;
|
||||
|
@ -84,17 +152,18 @@
|
|||
//! ## Passphrase-based encryption
|
||||
//!
|
||||
//! ```
|
||||
//! use age::secrecy::Secret;
|
||||
//! use age::secrecy::SecretString;
|
||||
//! use std::io::{Read, Write};
|
||||
//! use std::iter;
|
||||
//!
|
||||
//! # fn run_main() -> Result<(), ()> {
|
||||
//! let plaintext = b"Hello world!";
|
||||
//! let passphrase = "this is not a good passphrase";
|
||||
//! let passphrase = SecretString::from("this is not a good passphrase".to_owned());
|
||||
//!
|
||||
//! // Encrypt the plaintext to a ciphertext using the passphrase...
|
||||
//! # fn encrypt(passphrase: &str, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
//! # fn encrypt(passphrase: SecretString, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
//! let encrypted = {
|
||||
//! let encryptor = age::Encryptor::with_user_passphrase(Secret::new(passphrase.to_owned()));
|
||||
//! let encryptor = age::Encryptor::with_user_passphrase(passphrase.clone());
|
||||
//!
|
||||
//! let mut encrypted = vec![];
|
||||
//! let mut writer = encryptor.wrap_output(&mut encrypted)?;
|
||||
|
@ -107,15 +176,12 @@
|
|||
//! # }
|
||||
//!
|
||||
//! // ... and decrypt the ciphertext to the plaintext again using the same passphrase.
|
||||
//! # fn decrypt(passphrase: &str, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
//! # fn decrypt(passphrase: SecretString, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
//! let decrypted = {
|
||||
//! let decryptor = match age::Decryptor::new(&encrypted[..])? {
|
||||
//! age::Decryptor::Passphrase(d) => d,
|
||||
//! _ => unreachable!(),
|
||||
//! };
|
||||
//! let decryptor = age::Decryptor::new(&encrypted[..])?;
|
||||
//!
|
||||
//! let mut decrypted = vec![];
|
||||
//! let mut reader = decryptor.decrypt(&Secret::new(passphrase.to_owned()), None)?;
|
||||
//! let mut reader = decryptor.decrypt(iter::once(&age::scrypt::Identity::new(passphrase) as _))?;
|
||||
//! reader.read_to_end(&mut decrypted);
|
||||
//!
|
||||
//! decrypted
|
||||
|
@ -123,7 +189,7 @@
|
|||
//! # Ok(decrypted)
|
||||
//! # }
|
||||
//! # let decrypted = decrypt(
|
||||
//! # passphrase,
|
||||
//! # passphrase.clone(),
|
||||
//! # encrypt(passphrase, &plaintext[..]).map_err(|_| ())?
|
||||
//! # ).map_err(|_| ())?;
|
||||
//!
|
||||
|
@ -139,6 +205,8 @@
|
|||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Re-export crates that are used in our public API.
|
||||
pub use age_core::secrecy;
|
||||
|
||||
|
@ -150,12 +218,13 @@ mod primitives;
|
|||
mod protocol;
|
||||
mod util;
|
||||
|
||||
pub use error::{DecryptError, EncryptError};
|
||||
pub use identity::{IdentityFile, IdentityFileEntry};
|
||||
pub use error::{DecryptError, EncryptError, IdentityFileConvertError};
|
||||
pub use identity::IdentityFile;
|
||||
pub use primitives::stream;
|
||||
pub use protocol::{decryptor, Decryptor, Encryptor};
|
||||
pub use protocol::{Decryptor, Encryptor};
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "armor")))]
|
||||
pub use primitives::armor;
|
||||
|
||||
#[cfg(feature = "cli-common")]
|
||||
|
@ -165,12 +234,23 @@ pub mod cli_common;
|
|||
mod i18n;
|
||||
pub use i18n::localizer;
|
||||
|
||||
//
|
||||
// Simple interface
|
||||
//
|
||||
|
||||
mod simple;
|
||||
pub use simple::{decrypt, encrypt};
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "armor")))]
|
||||
pub use simple::encrypt_and_armor;
|
||||
|
||||
//
|
||||
// Identity types
|
||||
//
|
||||
|
||||
pub mod encrypted;
|
||||
mod scrypt;
|
||||
pub mod scrypt;
|
||||
pub mod x25519;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
|
@ -181,6 +261,10 @@ pub mod plugin;
|
|||
#[cfg_attr(docsrs, doc(cfg(feature = "ssh")))]
|
||||
pub mod ssh;
|
||||
|
||||
//
|
||||
// Core traits
|
||||
//
|
||||
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza},
|
||||
secrecy::SecretString,
|
||||
|
@ -188,12 +272,25 @@ use age_core::{
|
|||
|
||||
/// A private key or other value that can unwrap an opaque file key from a recipient
|
||||
/// stanza.
|
||||
///
|
||||
/// # Implementation notes
|
||||
///
|
||||
/// The canonical entry point for this trait is [`Identity::unwrap_stanzas`]. The default
|
||||
/// implementation of that method is:
|
||||
/// ```ignore
|
||||
/// stanzas.iter().find_map(|stanza| self.unwrap_stanza(stanza))
|
||||
/// ```
|
||||
///
|
||||
/// The `age` crate otherwise does not call [`Identity::unwrap_stanza`] directly. As such,
|
||||
/// if you want to add file-level stanza checks, override [`Identity::unwrap_stanzas`].
|
||||
pub trait Identity {
|
||||
/// Attempts to unwrap the given stanza with this identity.
|
||||
///
|
||||
/// This method is part of the `Identity` trait to expose age's [one joint] for
|
||||
/// external implementations. You should not need to call this directly; instead, pass
|
||||
/// identities to [`RecipientsDecryptor::decrypt`].
|
||||
/// identities to [`Decryptor::decrypt`].
|
||||
///
|
||||
/// The `age` crate only calls this method via [`Identity::unwrap_stanzas`].
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Some(Ok(file_key))` on success.
|
||||
|
@ -201,7 +298,6 @@ pub trait Identity {
|
|||
/// - `None` if the recipient stanza does not match this key.
|
||||
///
|
||||
/// [one joint]: https://www.imperialviolet.org/2016/05/16/agility.html
|
||||
/// [`RecipientsDecryptor::decrypt`]: protocol::decryptor::RecipientsDecryptor::decrypt
|
||||
fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>>;
|
||||
|
||||
/// Attempts to unwrap any of the given stanzas, which are assumed to come from the
|
||||
|
@ -209,7 +305,7 @@ pub trait Identity {
|
|||
///
|
||||
/// This method is part of the `Identity` trait to expose age's [one joint] for
|
||||
/// external implementations. You should not need to call this directly; instead, pass
|
||||
/// identities to [`RecipientsDecryptor::decrypt`].
|
||||
/// identities to [`Decryptor::decrypt`].
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Some(Ok(file_key))` on success.
|
||||
|
@ -217,7 +313,6 @@ pub trait Identity {
|
|||
/// - `None` if none of the recipient stanzas match this identity.
|
||||
///
|
||||
/// [one joint]: https://www.imperialviolet.org/2016/05/16/agility.html
|
||||
/// [`RecipientsDecryptor::decrypt`]: protocol::decryptor::RecipientsDecryptor::decrypt
|
||||
fn unwrap_stanzas(&self, stanzas: &[Stanza]) -> Option<Result<FileKey, DecryptError>> {
|
||||
stanzas.iter().find_map(|stanza| self.unwrap_stanza(stanza))
|
||||
}
|
||||
|
@ -227,16 +322,50 @@ pub trait Identity {
|
|||
///
|
||||
/// Implementations of this trait might represent more than one recipient.
|
||||
pub trait Recipient {
|
||||
/// Wraps the given file key, returning stanzas to be placed in an age file header.
|
||||
/// Wraps the given file key, returning stanzas to be placed in an age file header,
|
||||
/// and labels that constrain how the stanzas may be combined with those from other
|
||||
/// recipients.
|
||||
///
|
||||
/// Implementations MUST NOT return more than one stanza per "actual recipient".
|
||||
/// Implementations may return more than one stanza per "actual recipient", e.g. to
|
||||
/// support multiple formats, to build group aliases, or to act as a proxy.
|
||||
///
|
||||
/// This method is part of the `Recipient` trait to expose age's [one joint] for
|
||||
/// external implementations. You should not need to call this directly; instead, pass
|
||||
/// recipients to [`Encryptor::with_recipients`].
|
||||
///
|
||||
/// [one joint]: https://www.imperialviolet.org/2016/05/16/agility.html
|
||||
fn wrap_file_key(&self, file_key: &FileKey) -> Result<Vec<Stanza>, EncryptError>;
|
||||
///
|
||||
/// # Labels
|
||||
///
|
||||
/// [`Encryptor`] will succeed at encrypting only if every recipient returns the same
|
||||
/// set of labels. Subsets or partial overlapping sets are not allowed; all sets must
|
||||
/// be identical. Labels are compared exactly, and are case-sensitive.
|
||||
///
|
||||
/// Label sets can be used to ensure a recipient is only encrypted to alongside other
|
||||
/// recipients with equivalent properties, or to ensure a recipient is always used
|
||||
/// alone. A recipient with no particular properties to enforce should return an empty
|
||||
/// label set.
|
||||
///
|
||||
/// Labels can have any value that is a valid arbitrary string (`1*VCHAR` in ABNF),
|
||||
/// but usually take one of several forms:
|
||||
/// - *Common public label* - used by multiple recipients to permit their stanzas to
|
||||
/// be used only together. Examples include:
|
||||
/// - `postquantum` - indicates that the recipient stanzas being generated are
|
||||
/// postquantum-secure, and that they can only be combined with other stanzas
|
||||
/// that are also postquantum-secure.
|
||||
/// - *Common private label* - used by recipients created by the same private entity
|
||||
/// to permit their recipient stanzas to be used only together. For example,
|
||||
/// private recipients used in a corporate environment could all send the same
|
||||
/// private label in order to prevent compliant age clients from simultaneously
|
||||
/// wrapping file keys with other recipients.
|
||||
/// - *Random label* - used by recipients that want to ensure their stanzas are not
|
||||
/// used with any other recipient stanzas. This can be used to produce a file key
|
||||
/// that is only encrypted to a single recipient stanza, for example to preserve
|
||||
/// its authentication properties.
|
||||
fn wrap_file_key(
|
||||
&self,
|
||||
file_key: &FileKey,
|
||||
) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError>;
|
||||
}
|
||||
|
||||
/// Callbacks that might be triggered during encryption or decryption.
|
||||
|
@ -248,6 +377,9 @@ pub trait Callbacks: Clone + Send + Sync + 'static {
|
|||
///
|
||||
/// This can be used to prompt the user to take some physical action, such as
|
||||
/// inserting a hardware key.
|
||||
///
|
||||
/// No guarantee is provided that the user sees this message (for example, if there is
|
||||
/// no UI for displaying messages).
|
||||
fn display_message(&self, message: &str);
|
||||
|
||||
/// Requests that the user provides confirmation for some action.
|
||||
|
@ -270,12 +402,49 @@ pub trait Callbacks: Clone + Send + Sync + 'static {
|
|||
/// Requests non-private input from the user.
|
||||
///
|
||||
/// To request private inputs, use [`Callbacks::request_passphrase`].
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Some(input)` with the user-provided input.
|
||||
/// - `None` if no input could be requested from the user (for example, if there is no
|
||||
/// UI for displaying messages or typing inputs).
|
||||
fn request_public_string(&self, description: &str) -> Option<String>;
|
||||
|
||||
/// Requests a passphrase to decrypt a key.
|
||||
///
|
||||
/// Returns:
|
||||
/// - `Some(passphrase)` with the user-provided passphrase.
|
||||
/// - `None` if no passphrase could be requested from the user (for example, if there
|
||||
/// is no UI for displaying messages or typing inputs).
|
||||
fn request_passphrase(&self, description: &str) -> Option<SecretString>;
|
||||
}
|
||||
|
||||
/// An implementation of [`Callbacks`] that does not allow callbacks.
|
||||
///
|
||||
/// No user interaction will occur; [`Recipient`] or [`Identity`] implementations will
|
||||
/// receive `None` from the callbacks that return responses, and will act accordingly.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct NoCallbacks;
|
||||
|
||||
impl Callbacks for NoCallbacks {
|
||||
fn display_message(&self, _: &str) {}
|
||||
|
||||
fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_public_string(&self, _: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_passphrase(&self, _: &str) -> Option<SecretString> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Fuzzing APIs
|
||||
//
|
||||
|
||||
/// Helper for fuzzing the Header parser and serializer.
|
||||
#[cfg(fuzzing)]
|
||||
pub fn fuzz_header(data: &[u8]) {
|
||||
|
|
|
@ -10,6 +10,7 @@ use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
|||
use bech32::Variant;
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::iter;
|
||||
|
@ -32,6 +33,7 @@ const PLUGIN_IDENTITY_PREFIX: &str = "age-plugin-";
|
|||
|
||||
const CMD_ERROR: &str = "error";
|
||||
const CMD_RECIPIENT_STANZA: &str = "recipient-stanza";
|
||||
const CMD_LABELS: &str = "labels";
|
||||
const CMD_MSG: &str = "msg";
|
||||
const CMD_CONFIRM: &str = "confirm";
|
||||
const CMD_REQUEST_PUBLIC: &str = "request-public";
|
||||
|
@ -215,7 +217,7 @@ impl Identity {
|
|||
}
|
||||
|
||||
/// An age plugin.
|
||||
struct Plugin {
|
||||
pub(crate) struct Plugin {
|
||||
binary_name: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
@ -224,7 +226,7 @@ impl Plugin {
|
|||
/// Finds the age plugin with the given name in `$PATH`.
|
||||
///
|
||||
/// On error, returns the binary name that could not be located.
|
||||
fn new(name: &str) -> Result<Self, String> {
|
||||
pub(crate) fn new(name: &str) -> Result<Self, String> {
|
||||
let binary_name = binary_name(name);
|
||||
match which::which(&binary_name).or_else(|e| {
|
||||
// If we are running in WSL, try appending `.exe`; plugins installed in
|
||||
|
@ -410,7 +412,10 @@ impl<C: Callbacks> RecipientPluginV1<C> {
|
|||
}
|
||||
|
||||
impl<C: Callbacks> crate::Recipient for RecipientPluginV1<C> {
|
||||
fn wrap_file_key(&self, file_key: &FileKey) -> Result<Vec<Stanza>, EncryptError> {
|
||||
fn wrap_file_key(
|
||||
&self,
|
||||
file_key: &FileKey,
|
||||
) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
|
||||
// Open connection
|
||||
let mut conn = self.plugin.connect(RECIPIENT_V1)?;
|
||||
|
||||
|
@ -424,11 +429,13 @@ impl<C: Callbacks> crate::Recipient for RecipientPluginV1<C> {
|
|||
for identity in &self.identities {
|
||||
phase.send("add-identity", &[&identity.identity], &[])?;
|
||||
}
|
||||
phase.send("extension-labels", &[], &[])?;
|
||||
phase.send("wrap-file-key", &[], file_key.expose_secret())
|
||||
})?;
|
||||
|
||||
// Phase 2: collect either stanzas or errors
|
||||
let mut stanzas = vec![];
|
||||
let mut labels = None;
|
||||
let mut errors = vec![];
|
||||
if let Err(e) = conn.bidir_receive(
|
||||
&[
|
||||
|
@ -437,6 +444,7 @@ impl<C: Callbacks> crate::Recipient for RecipientPluginV1<C> {
|
|||
CMD_REQUEST_PUBLIC,
|
||||
CMD_REQUEST_SECRET,
|
||||
CMD_RECIPIENT_STANZA,
|
||||
CMD_LABELS,
|
||||
CMD_ERROR,
|
||||
],
|
||||
|mut command, reply| match command.tag.as_str() {
|
||||
|
@ -492,6 +500,34 @@ impl<C: Callbacks> crate::Recipient for RecipientPluginV1<C> {
|
|||
}
|
||||
reply.ok(None)
|
||||
}
|
||||
CMD_LABELS => {
|
||||
if labels.is_none() {
|
||||
let labels_count = command.args.len();
|
||||
let label_set = command.args.into_iter().collect::<HashSet<_>>();
|
||||
if label_set.len() == labels_count {
|
||||
labels = Some(label_set);
|
||||
} else {
|
||||
errors.push(PluginError::Other {
|
||||
kind: "internal".to_owned(),
|
||||
metadata: vec![],
|
||||
message: format!(
|
||||
"{} command must not contain duplicate labels",
|
||||
CMD_LABELS
|
||||
),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
errors.push(PluginError::Other {
|
||||
kind: "internal".to_owned(),
|
||||
metadata: vec![],
|
||||
message: format!(
|
||||
"{} command must not be sent more than once",
|
||||
CMD_LABELS
|
||||
),
|
||||
});
|
||||
}
|
||||
reply.ok(None)
|
||||
}
|
||||
CMD_ERROR => {
|
||||
if command.args.len() == 2 && command.args[0] == "recipient" {
|
||||
let index: usize = command.args[1].parse().unwrap();
|
||||
|
@ -517,7 +553,7 @@ impl<C: Callbacks> crate::Recipient for RecipientPluginV1<C> {
|
|||
return Err(e.into());
|
||||
};
|
||||
match (stanzas.is_empty(), errors.is_empty()) {
|
||||
(false, true) => Ok(stanzas),
|
||||
(false, true) => Ok((stanzas, labels.unwrap_or_default())),
|
||||
(a, b) => {
|
||||
if a & b {
|
||||
errors.push(PluginError::Other {
|
||||
|
@ -563,14 +599,13 @@ impl<C: Callbacks> IdentityPluginV1<C> {
|
|||
if valid_plugin_name(plugin_name) {
|
||||
Plugin::new(plugin_name)
|
||||
.map_err(|binary_name| DecryptError::MissingPlugin { binary_name })
|
||||
.map(|plugin| IdentityPluginV1 {
|
||||
plugin,
|
||||
identities: identities
|
||||
.map(|plugin| {
|
||||
let identities = identities
|
||||
.iter()
|
||||
.filter(|r| r.name == plugin_name)
|
||||
.cloned()
|
||||
.collect(),
|
||||
callbacks,
|
||||
.collect();
|
||||
Self::from_parts(plugin, identities, callbacks)
|
||||
})
|
||||
} else {
|
||||
Err(DecryptError::MissingPlugin {
|
||||
|
@ -579,6 +614,14 @@ impl<C: Callbacks> IdentityPluginV1<C> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_parts(plugin: Plugin, identities: Vec<Identity>, callbacks: C) -> Self {
|
||||
IdentityPluginV1 {
|
||||
plugin,
|
||||
identities,
|
||||
callbacks,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_stanzas<'a>(
|
||||
&self,
|
||||
stanzas: impl Iterator<Item = &'a Stanza>,
|
||||
|
@ -645,11 +688,14 @@ impl<C: Callbacks> IdentityPluginV1<C> {
|
|||
// We only support a single file.
|
||||
assert!(command.args[0] == "0");
|
||||
assert!(file_key.is_none());
|
||||
file_key = Some(
|
||||
TryInto::<[u8; 16]>::try_into(&command.body[..])
|
||||
.map_err(|_| DecryptError::DecryptionFailed)
|
||||
.map(FileKey::from),
|
||||
);
|
||||
file_key = Some(FileKey::try_init_with_mut(|file_key| {
|
||||
if command.body.len() == file_key.len() {
|
||||
file_key.copy_from_slice(&command.body);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DecryptError::DecryptionFailed)
|
||||
}
|
||||
}));
|
||||
reply.ok(None)
|
||||
}
|
||||
CMD_ERROR => {
|
||||
|
@ -690,7 +736,7 @@ impl<C: Callbacks> crate::Identity for IdentityPluginV1<C> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{Callbacks, DecryptError, EncryptError};
|
||||
use crate::{DecryptError, EncryptError, NoCallbacks};
|
||||
|
||||
use super::{
|
||||
Identity, IdentityPluginV1, Recipient, RecipientPluginV1, PLUGIN_IDENTITY_PREFIX,
|
||||
|
@ -699,21 +745,6 @@ mod tests {
|
|||
|
||||
const INVALID_PLUGIN_NAME: &str = "foobar/../../../../../../../usr/bin/echo";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct NoCallbacks;
|
||||
impl Callbacks for NoCallbacks {
|
||||
fn display_message(&self, _: &str) {}
|
||||
fn confirm(&self, _: &str, _: &str, _: Option<&str>) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
fn request_public_string(&self, _: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
fn request_passphrase(&self, _: &str) -> Option<crate::secrecy::SecretString> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_for_plugin() {
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Primitive cryptographic operations used by `age`.
|
||||
|
||||
use age_core::secrecy::{ExposeSecret, Secret};
|
||||
use age_core::secrecy::{ExposeSecret, SecretBox};
|
||||
use hmac::{
|
||||
digest::{CtOutput, MacError},
|
||||
Hmac, Mac,
|
||||
|
@ -15,7 +15,7 @@ pub mod armor;
|
|||
|
||||
pub mod stream;
|
||||
|
||||
pub(crate) struct HmacKey(pub(crate) Secret<[u8; 32]>);
|
||||
pub(crate) struct HmacKey(pub(crate) SecretBox<[u8; 32]>);
|
||||
|
||||
/// `HMAC[key](message)`
|
||||
///
|
||||
|
|
|
@ -291,7 +291,7 @@ enum ArmorIs<W> {
|
|||
/// ```
|
||||
/// # use std::io::Read;
|
||||
/// use std::io::Write;
|
||||
/// # use std::iter;
|
||||
/// use std::iter;
|
||||
///
|
||||
/// # fn run_main() -> Result<(), ()> {
|
||||
/// # let identity = age::x25519::Identity::generate();
|
||||
|
@ -301,7 +301,7 @@ enum ArmorIs<W> {
|
|||
///
|
||||
/// # fn encrypt(recipient: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
/// let encrypted = {
|
||||
/// let encryptor = age::Encryptor::with_recipients(vec![Box::new(recipient)])
|
||||
/// let encryptor = age::Encryptor::with_recipients(iter::once(&recipient as _))
|
||||
/// .expect("we provided a recipient");
|
||||
///
|
||||
/// let mut encrypted = vec![];
|
||||
|
@ -321,12 +321,7 @@ enum ArmorIs<W> {
|
|||
/// # }
|
||||
/// # fn decrypt(identity: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
/// # let decrypted = {
|
||||
/// # let decryptor = match age::Decryptor::new(
|
||||
/// # age::armor::ArmoredReader::new(&encrypted[..])
|
||||
/// # )? {
|
||||
/// # age::Decryptor::Recipients(d) => d,
|
||||
/// # _ => unreachable!(),
|
||||
/// # };
|
||||
/// # let decryptor = age::Decryptor::new(age::armor::ArmoredReader::new(&encrypted[..]))?;
|
||||
/// # let mut decrypted = vec![];
|
||||
/// # let mut reader = decryptor.decrypt(iter::once(&identity as &dyn age::Identity))?;
|
||||
/// # reader.read_to_end(&mut decrypted);
|
||||
|
@ -669,7 +664,7 @@ enum StartPos {
|
|||
/// # fn run_main() -> Result<(), ()> {
|
||||
/// # fn encrypt(recipient: age::x25519::Recipient, plaintext: &[u8]) -> Result<Vec<u8>, age::EncryptError> {
|
||||
/// # let encrypted = {
|
||||
/// # let encryptor = age::Encryptor::with_recipients(vec![Box::new(recipient)])
|
||||
/// # let encryptor = age::Encryptor::with_recipients(iter::once(&recipient as _))
|
||||
/// # .expect("we provided a recipient");
|
||||
/// # let mut encrypted = vec![];
|
||||
/// # let mut writer = encryptor.wrap_output(
|
||||
|
@ -693,12 +688,7 @@ enum StartPos {
|
|||
///
|
||||
/// # fn decrypt(identity: age::x25519::Identity, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
|
||||
/// let decrypted = {
|
||||
/// let decryptor = match age::Decryptor::new(
|
||||
/// age::armor::ArmoredReader::new(&encrypted[..])
|
||||
/// )? {
|
||||
/// age::Decryptor::Recipients(d) => d,
|
||||
/// _ => unreachable!(),
|
||||
/// };
|
||||
/// let decryptor = age::Decryptor::new(age::armor::ArmoredReader::new(&encrypted[..]))?;
|
||||
///
|
||||
/// let mut decrypted = vec![];
|
||||
/// let mut reader = decryptor.decrypt(iter::once(&identity as &dyn age::Identity))?;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! I/O helper structs for age file encryption and decryption.
|
||||
|
||||
use age_core::secrecy::{ExposeSecret, SecretVec};
|
||||
use age_core::secrecy::{ExposeSecret, SecretSlice};
|
||||
use chacha20poly1305::{
|
||||
aead::{generic_array::GenericArray, Aead, KeyInit, KeySizeUser},
|
||||
ChaCha20Poly1305,
|
||||
|
@ -194,7 +194,7 @@ impl Stream {
|
|||
Ok(encrypted)
|
||||
}
|
||||
|
||||
fn decrypt_chunk(&mut self, chunk: &[u8], last: bool) -> io::Result<SecretVec<u8>> {
|
||||
fn decrypt_chunk(&mut self, chunk: &[u8], last: bool) -> io::Result<SecretSlice<u8>> {
|
||||
assert!(chunk.len() <= ENCRYPTED_CHUNK_SIZE);
|
||||
|
||||
self.nonce.set_last(last).map_err(|_| {
|
||||
|
@ -204,7 +204,7 @@ impl Stream {
|
|||
let decrypted = self
|
||||
.aead
|
||||
.decrypt(&self.nonce.to_bytes().into(), chunk)
|
||||
.map(SecretVec::new)
|
||||
.map(SecretSlice::from)
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "decryption error"))?;
|
||||
self.nonce.increment_counter();
|
||||
|
||||
|
@ -407,7 +407,7 @@ pub struct StreamReader<R> {
|
|||
start: StartPos,
|
||||
plaintext_len: Option<u64>,
|
||||
cur_plaintext_pos: u64,
|
||||
chunk: Option<SecretVec<u8>>,
|
||||
chunk: Option<SecretSlice<u8>>,
|
||||
}
|
||||
|
||||
impl<R> StreamReader<R> {
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
//! Encryption and decryption routines for age.
|
||||
|
||||
use age_core::{format::grease_the_joint, secrecy::SecretString};
|
||||
use age_core::{format::is_arbitrary_string, secrecy::SecretString};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
|
||||
use std::io::{self, BufRead, Read, Write};
|
||||
use std::iter;
|
||||
|
||||
use crate::{
|
||||
error::{DecryptError, EncryptError},
|
||||
format::{Header, HeaderV1},
|
||||
keys::{mac_key, new_file_key, v1_payload_key},
|
||||
primitives::stream::{PayloadKey, Stream, StreamWriter},
|
||||
scrypt, Recipient,
|
||||
primitives::stream::{PayloadKey, Stream, StreamReader, StreamWriter},
|
||||
scrypt, Identity, Recipient,
|
||||
};
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
use futures::io::{AsyncBufRead, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
|
||||
pub mod decryptor;
|
||||
|
||||
pub(crate) struct Nonce([u8; 16]);
|
||||
|
||||
impl AsRef<[u8]> for Nonce {
|
||||
|
@ -47,26 +47,14 @@ impl Nonce {
|
|||
}
|
||||
}
|
||||
|
||||
/// Handles the various types of age encryption.
|
||||
enum EncryptorType {
|
||||
/// Encryption to a list of recipients identified by keys.
|
||||
Keys(Vec<Box<dyn Recipient + Send>>),
|
||||
/// Encryption to a passphrase.
|
||||
Passphrase(SecretString),
|
||||
/// Encryptor for creating an age file.
|
||||
pub struct Encryptor {
|
||||
header: Header,
|
||||
nonce: Nonce,
|
||||
payload_key: PayloadKey,
|
||||
}
|
||||
|
||||
/// Encryptor for creating an age file.
|
||||
pub struct Encryptor(EncryptorType);
|
||||
|
||||
impl Encryptor {
|
||||
/// Constructs an `Encryptor` that will create an age file encrypted to a list of
|
||||
/// recipients.
|
||||
///
|
||||
/// Returns `None` if no recipients were provided.
|
||||
pub fn with_recipients(recipients: Vec<Box<dyn Recipient + Send>>) -> Option<Self> {
|
||||
(!recipients.is_empty()).then_some(Encryptor(EncryptorType::Keys(recipients)))
|
||||
}
|
||||
|
||||
/// Returns an `Encryptor` that will create an age file encrypted with a passphrase.
|
||||
/// Anyone with the passphrase can decrypt the file.
|
||||
///
|
||||
|
@ -76,33 +64,64 @@ impl Encryptor {
|
|||
///
|
||||
/// [`x25519::Identity`]: crate::x25519::Identity
|
||||
pub fn with_user_passphrase(passphrase: SecretString) -> Self {
|
||||
Encryptor(EncryptorType::Passphrase(passphrase))
|
||||
Self::with_recipients(iter::once(&scrypt::Recipient::new(passphrase) as _))
|
||||
.expect("no errors can occur with this recipient set")
|
||||
}
|
||||
|
||||
/// Creates the header for this age file.
|
||||
fn prepare_header(self) -> Result<(Header, Nonce, PayloadKey), EncryptError> {
|
||||
/// Constructs an `Encryptor` that will create an age file encrypted to a list of
|
||||
/// recipients.
|
||||
pub fn with_recipients<'a>(
|
||||
recipients: impl Iterator<Item = &'a dyn Recipient>,
|
||||
) -> Result<Self, EncryptError> {
|
||||
let file_key = new_file_key();
|
||||
|
||||
let recipients = match self.0 {
|
||||
EncryptorType::Keys(recipients) => {
|
||||
let mut stanzas = Vec::with_capacity(recipients.len() + 1);
|
||||
for recipient in recipients {
|
||||
stanzas.append(&mut recipient.wrap_file_key(&file_key)?);
|
||||
let recipients = {
|
||||
let mut control = None;
|
||||
|
||||
let mut stanzas = vec![];
|
||||
let mut have_recipients = false;
|
||||
for recipient in recipients {
|
||||
have_recipients = true;
|
||||
let (mut r_stanzas, r_labels) = recipient.wrap_file_key(&file_key)?;
|
||||
|
||||
if let Some(l_labels) = control.take() {
|
||||
if l_labels != r_labels {
|
||||
// Improve error message.
|
||||
let err = if stanzas
|
||||
.iter()
|
||||
.chain(&r_stanzas)
|
||||
.any(|stanza| stanza.tag == crate::scrypt::SCRYPT_RECIPIENT_TAG)
|
||||
{
|
||||
EncryptError::MixedRecipientAndPassphrase
|
||||
} else {
|
||||
EncryptError::IncompatibleRecipients { l_labels, r_labels }
|
||||
};
|
||||
return Err(err);
|
||||
}
|
||||
control = Some(l_labels);
|
||||
} else if r_labels.iter().all(is_arbitrary_string) {
|
||||
control = Some(r_labels);
|
||||
} else {
|
||||
return Err(EncryptError::InvalidRecipientLabels(r_labels));
|
||||
}
|
||||
// Keep the joint well oiled!
|
||||
stanzas.push(grease_the_joint());
|
||||
stanzas
|
||||
|
||||
stanzas.append(&mut r_stanzas);
|
||||
}
|
||||
EncryptorType::Passphrase(passphrase) => {
|
||||
scrypt::Recipient { passphrase }.wrap_file_key(&file_key)?
|
||||
if !have_recipients {
|
||||
return Err(EncryptError::MissingRecipients);
|
||||
}
|
||||
stanzas
|
||||
};
|
||||
|
||||
let header = HeaderV1::new(recipients, mac_key(&file_key));
|
||||
let header = HeaderV1::new(recipients, mac_key(&file_key))?;
|
||||
let nonce = Nonce::random();
|
||||
let payload_key = v1_payload_key(&file_key, &header, &nonce).expect("MAC is correct");
|
||||
|
||||
Ok((Header::V1(header), nonce, payload_key))
|
||||
Ok(Self {
|
||||
header: Header::V1(header),
|
||||
nonce,
|
||||
payload_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a wrapper around a writer that will encrypt its input.
|
||||
|
@ -112,8 +131,12 @@ impl Encryptor {
|
|||
/// You **MUST** call [`StreamWriter::finish`] when you are done writing, in order to
|
||||
/// finish the encryption process. Failing to call [`StreamWriter::finish`] will
|
||||
/// result in a truncated file that will fail to decrypt.
|
||||
pub fn wrap_output<W: Write>(self, mut output: W) -> Result<StreamWriter<W>, EncryptError> {
|
||||
let (header, nonce, payload_key) = self.prepare_header()?;
|
||||
pub fn wrap_output<W: Write>(self, mut output: W) -> io::Result<StreamWriter<W>> {
|
||||
let Self {
|
||||
header,
|
||||
nonce,
|
||||
payload_key,
|
||||
} = self;
|
||||
header.write(&mut output)?;
|
||||
output.write_all(nonce.as_ref())?;
|
||||
Ok(Stream::encrypt(payload_key, output))
|
||||
|
@ -131,8 +154,12 @@ impl Encryptor {
|
|||
pub async fn wrap_async_output<W: AsyncWrite + Unpin>(
|
||||
self,
|
||||
mut output: W,
|
||||
) -> Result<StreamWriter<W>, EncryptError> {
|
||||
let (header, nonce, payload_key) = self.prepare_header()?;
|
||||
) -> io::Result<StreamWriter<W>> {
|
||||
let Self {
|
||||
header,
|
||||
nonce,
|
||||
payload_key,
|
||||
} = self;
|
||||
header.write_async(&mut output).await?;
|
||||
output.write_all(nonce.as_ref()).await?;
|
||||
Ok(Stream::encrypt_async(payload_key, output))
|
||||
|
@ -140,41 +167,49 @@ impl Encryptor {
|
|||
}
|
||||
|
||||
/// Decryptor for an age file.
|
||||
pub enum Decryptor<R> {
|
||||
/// Decryption with a list of identities.
|
||||
Recipients(decryptor::RecipientsDecryptor<R>),
|
||||
/// Decryption with a passphrase.
|
||||
Passphrase(decryptor::PassphraseDecryptor<R>),
|
||||
}
|
||||
|
||||
impl<R> From<decryptor::RecipientsDecryptor<R>> for Decryptor<R> {
|
||||
fn from(decryptor: decryptor::RecipientsDecryptor<R>) -> Self {
|
||||
Decryptor::Recipients(decryptor)
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> From<decryptor::PassphraseDecryptor<R>> for Decryptor<R> {
|
||||
fn from(decryptor: decryptor::PassphraseDecryptor<R>) -> Self {
|
||||
Decryptor::Passphrase(decryptor)
|
||||
}
|
||||
pub struct Decryptor<R> {
|
||||
/// The age file.
|
||||
input: R,
|
||||
/// The age file's header.
|
||||
header: Header,
|
||||
/// The age file's AEAD nonce
|
||||
nonce: Nonce,
|
||||
}
|
||||
|
||||
impl<R> Decryptor<R> {
|
||||
fn from_v1_header(input: R, header: HeaderV1, nonce: Nonce) -> Result<Self, DecryptError> {
|
||||
// Enforce structural requirements on the v1 header.
|
||||
let any_scrypt = header
|
||||
.recipients
|
||||
.iter()
|
||||
.any(|r| r.tag == scrypt::SCRYPT_RECIPIENT_TAG);
|
||||
|
||||
if any_scrypt && header.recipients.len() == 1 {
|
||||
Ok(decryptor::PassphraseDecryptor::new(input, Header::V1(header), nonce).into())
|
||||
} else if !any_scrypt {
|
||||
Ok(decryptor::RecipientsDecryptor::new(input, Header::V1(header), nonce).into())
|
||||
if header.is_valid() {
|
||||
Ok(Self {
|
||||
input,
|
||||
header: Header::V1(header),
|
||||
nonce,
|
||||
})
|
||||
} else {
|
||||
Err(DecryptError::InvalidHeader)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the age file is encrypted to a passphrase.
|
||||
pub fn is_scrypt(&self) -> bool {
|
||||
match &self.header {
|
||||
Header::V1(header) => header.valid_scrypt(),
|
||||
Header::Unknown(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn obtain_payload_key<'a>(
|
||||
&self,
|
||||
mut identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) -> Result<PayloadKey, DecryptError> {
|
||||
match &self.header {
|
||||
Header::V1(header) => identities
|
||||
.find_map(|key| key.unwrap_stanzas(&header.recipients))
|
||||
.unwrap_or(Err(DecryptError::NoMatchingKeys))
|
||||
.and_then(|file_key| v1_payload_key(&file_key, header, &self.nonce)),
|
||||
Header::Unknown(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Decryptor<R> {
|
||||
|
@ -199,6 +234,17 @@ impl<R: Read> Decryptor<R> {
|
|||
Header::Unknown(_) => Err(DecryptError::UnknownFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to decrypt the age file.
|
||||
///
|
||||
/// If successful, returns a reader that will provide the plaintext.
|
||||
pub fn decrypt<'a>(
|
||||
self,
|
||||
identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) -> Result<StreamReader<R>, DecryptError> {
|
||||
self.obtain_payload_key(identities)
|
||||
.map(|payload_key| Stream::decrypt(payload_key, self.input))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: BufRead> Decryptor<R> {
|
||||
|
@ -247,6 +293,17 @@ impl<R: AsyncRead + Unpin> Decryptor<R> {
|
|||
Header::Unknown(_) => Err(DecryptError::UnknownFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to decrypt the age file.
|
||||
///
|
||||
/// If successful, returns a reader that will provide the plaintext.
|
||||
pub fn decrypt_async<'a>(
|
||||
self,
|
||||
identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) -> Result<StreamReader<R>, DecryptError> {
|
||||
self.obtain_payload_key(identities)
|
||||
.map(|payload_key| Stream::decrypt_async(payload_key, self.input))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
|
@ -275,17 +332,16 @@ impl<R: AsyncBufRead + Unpin> Decryptor<R> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use age_core::secrecy::SecretString;
|
||||
use std::collections::HashSet;
|
||||
use std::io::{BufReader, Read, Write};
|
||||
|
||||
use age_core::secrecy::SecretString;
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
use std::iter;
|
||||
|
||||
use super::{Decryptor, Encryptor};
|
||||
use crate::{
|
||||
identity::{IdentityFile, IdentityFileEntry},
|
||||
x25519, Identity, Recipient,
|
||||
};
|
||||
use crate::{identity::IdentityFile, scrypt, x25519, EncryptError, Identity, Recipient};
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
use futures::{
|
||||
|
@ -298,7 +354,7 @@ mod tests {
|
|||
use futures_test::task::noop_context;
|
||||
|
||||
fn recipient_round_trip<'a>(
|
||||
recipients: Vec<Box<dyn Recipient + Send>>,
|
||||
recipients: impl Iterator<Item = &'a dyn Recipient>,
|
||||
identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) {
|
||||
let test_msg = b"This is a test message. For testing.";
|
||||
|
@ -311,10 +367,7 @@ mod tests {
|
|||
w.finish().unwrap();
|
||||
}
|
||||
|
||||
let d = match Decryptor::new(&encrypted[..]) {
|
||||
Ok(Decryptor::Recipients(d)) => d,
|
||||
_ => panic!(),
|
||||
};
|
||||
let d = Decryptor::new(&encrypted[..]).unwrap();
|
||||
let mut r = d.decrypt(identities).unwrap();
|
||||
let mut decrypted = vec![];
|
||||
r.read_to_end(&mut decrypted).unwrap();
|
||||
|
@ -324,7 +377,7 @@ mod tests {
|
|||
|
||||
#[cfg(feature = "async")]
|
||||
fn recipient_async_round_trip<'a>(
|
||||
recipients: Vec<Box<dyn Recipient + Send>>,
|
||||
recipients: impl Iterator<Item = &'a dyn Recipient>,
|
||||
identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) {
|
||||
let test_msg = b"This is a test message. For testing.";
|
||||
|
@ -365,7 +418,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
let d = match {
|
||||
let d = {
|
||||
let f = Decryptor::new_async(&encrypted[..]);
|
||||
pin_mut!(f);
|
||||
|
||||
|
@ -376,9 +429,6 @@ mod tests {
|
|||
Poll::Pending => panic!("Unexpected Pending"),
|
||||
}
|
||||
}
|
||||
} {
|
||||
Decryptor::Recipients(d) => d,
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
let decrypted = {
|
||||
|
@ -406,12 +456,8 @@ mod tests {
|
|||
let f = IdentityFile::from_buffer(buf).unwrap();
|
||||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
|
||||
recipient_round_trip(
|
||||
vec![Box::new(pk)],
|
||||
f.into_identities().iter().map(|sk| match sk {
|
||||
IdentityFileEntry::Native(sk) => sk as &dyn Identity,
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(_) => unreachable!(),
|
||||
}),
|
||||
iter::once(&pk as _),
|
||||
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -422,12 +468,8 @@ mod tests {
|
|||
let f = IdentityFile::from_buffer(buf).unwrap();
|
||||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
|
||||
recipient_async_round_trip(
|
||||
vec![Box::new(pk)],
|
||||
f.into_identities().iter().map(|sk| match sk {
|
||||
IdentityFileEntry::Native(sk) => sk as &dyn Identity,
|
||||
#[cfg(feature = "plugin")]
|
||||
IdentityFileEntry::Plugin(_) => unreachable!(),
|
||||
}),
|
||||
iter::once(&pk as _),
|
||||
f.into_identities().unwrap().iter().map(|i| i.as_ref()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -435,20 +477,24 @@ mod tests {
|
|||
fn scrypt_round_trip() {
|
||||
let test_msg = b"This is a test message. For testing.";
|
||||
|
||||
let mut recipient = scrypt::Recipient::new(SecretString::from("passphrase".to_string()));
|
||||
// Override to something very fast for testing.
|
||||
recipient.set_work_factor(2);
|
||||
|
||||
let mut encrypted = vec![];
|
||||
let e = Encryptor::with_user_passphrase(SecretString::new("passphrase".to_string()));
|
||||
let e = Encryptor::with_recipients(iter::once(&recipient as _)).unwrap();
|
||||
{
|
||||
let mut w = e.wrap_output(&mut encrypted).unwrap();
|
||||
w.write_all(test_msg).unwrap();
|
||||
w.finish().unwrap();
|
||||
}
|
||||
|
||||
let d = match Decryptor::new(&encrypted[..]) {
|
||||
Ok(Decryptor::Passphrase(d)) => d,
|
||||
_ => panic!(),
|
||||
};
|
||||
let d = Decryptor::new(&encrypted[..]).unwrap();
|
||||
let mut r = d
|
||||
.decrypt(&SecretString::new("passphrase".to_string()), None)
|
||||
.decrypt(
|
||||
Some(&scrypt::Identity::new(SecretString::from("passphrase".to_string())) as _)
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut decrypted = vec![];
|
||||
r.read_to_end(&mut decrypted).unwrap();
|
||||
|
@ -464,7 +510,7 @@ mod tests {
|
|||
let pk: crate::ssh::Recipient = crate::ssh::recipient::tests::TEST_SSH_RSA_PK
|
||||
.parse()
|
||||
.unwrap();
|
||||
recipient_round_trip(vec![Box::new(pk)], iter::once(&sk as &dyn Identity));
|
||||
recipient_round_trip(iter::once(&pk as _), iter::once(&sk as &dyn Identity));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssh", feature = "async"))]
|
||||
|
@ -475,7 +521,7 @@ mod tests {
|
|||
let pk: crate::ssh::Recipient = crate::ssh::recipient::tests::TEST_SSH_RSA_PK
|
||||
.parse()
|
||||
.unwrap();
|
||||
recipient_async_round_trip(vec![Box::new(pk)], iter::once(&sk as &dyn Identity));
|
||||
recipient_async_round_trip(iter::once(&pk as _), iter::once(&sk as &dyn Identity));
|
||||
}
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
|
@ -486,7 +532,7 @@ mod tests {
|
|||
let pk: crate::ssh::Recipient = crate::ssh::recipient::tests::TEST_SSH_ED25519_PK
|
||||
.parse()
|
||||
.unwrap();
|
||||
recipient_round_trip(vec![Box::new(pk)], iter::once(&sk as &dyn Identity));
|
||||
recipient_round_trip(iter::once(&pk as _), iter::once(&sk as &dyn Identity));
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "ssh", feature = "async"))]
|
||||
|
@ -497,6 +543,47 @@ mod tests {
|
|||
let pk: crate::ssh::Recipient = crate::ssh::recipient::tests::TEST_SSH_ED25519_PK
|
||||
.parse()
|
||||
.unwrap();
|
||||
recipient_async_round_trip(vec![Box::new(pk)], iter::once(&sk as &dyn Identity));
|
||||
recipient_async_round_trip(iter::once(&pk as _), iter::once(&sk as &dyn Identity));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_recipient_and_passphrase() {
|
||||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
|
||||
let passphrase =
|
||||
crate::scrypt::Recipient::new(SecretString::from("passphrase".to_string()));
|
||||
|
||||
let recipients = [&pk as &dyn Recipient, &passphrase as _];
|
||||
|
||||
assert!(matches!(
|
||||
Encryptor::with_recipients(recipients.into_iter()),
|
||||
Err(EncryptError::MixedRecipientAndPassphrase),
|
||||
));
|
||||
}
|
||||
|
||||
struct IncompatibleRecipient(crate::x25519::Recipient);
|
||||
|
||||
impl Recipient for IncompatibleRecipient {
|
||||
fn wrap_file_key(
|
||||
&self,
|
||||
file_key: &age_core::format::FileKey,
|
||||
) -> Result<(Vec<age_core::format::Stanza>, HashSet<String>), EncryptError> {
|
||||
self.0.wrap_file_key(file_key).map(|(stanzas, mut labels)| {
|
||||
labels.insert("incompatible".into());
|
||||
(stanzas, labels)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incompatible_recipients() {
|
||||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
|
||||
let incompatible = IncompatibleRecipient(pk.clone());
|
||||
|
||||
let recipients = [&pk as &dyn Recipient, &incompatible as _];
|
||||
|
||||
assert!(matches!(
|
||||
Encryptor::with_recipients(recipients.into_iter()),
|
||||
Err(EncryptError::IncompatibleRecipients { .. }),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
//! Decryptors for age.
|
||||
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza},
|
||||
secrecy::SecretString,
|
||||
};
|
||||
use std::io::Read;
|
||||
|
||||
use super::Nonce;
|
||||
use crate::{
|
||||
error::DecryptError,
|
||||
format::Header,
|
||||
keys::v1_payload_key,
|
||||
primitives::stream::{PayloadKey, Stream, StreamReader},
|
||||
scrypt, Identity,
|
||||
};
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
use futures::io::AsyncRead;
|
||||
|
||||
struct BaseDecryptor<R> {
|
||||
/// The age file.
|
||||
input: R,
|
||||
/// The age file's header.
|
||||
header: Header,
|
||||
/// The age file's AEAD nonce
|
||||
nonce: Nonce,
|
||||
}
|
||||
|
||||
impl<R> BaseDecryptor<R> {
|
||||
fn obtain_payload_key<F>(&self, mut filter: F) -> Result<PayloadKey, DecryptError>
|
||||
where
|
||||
F: FnMut(&[Stanza]) -> Option<Result<FileKey, DecryptError>>,
|
||||
{
|
||||
match &self.header {
|
||||
Header::V1(header) => filter(&header.recipients)
|
||||
.unwrap_or(Err(DecryptError::NoMatchingKeys))
|
||||
.and_then(|file_key| v1_payload_key(&file_key, header, &self.nonce)),
|
||||
Header::Unknown(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decryptor for an age file encrypted to a list of recipients.
|
||||
pub struct RecipientsDecryptor<R>(BaseDecryptor<R>);
|
||||
|
||||
impl<R> RecipientsDecryptor<R> {
|
||||
pub(super) fn new(input: R, header: Header, nonce: Nonce) -> Self {
|
||||
RecipientsDecryptor(BaseDecryptor {
|
||||
input,
|
||||
header,
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
|
||||
fn obtain_payload_key<'a>(
|
||||
&self,
|
||||
mut identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) -> Result<PayloadKey, DecryptError> {
|
||||
self.0
|
||||
.obtain_payload_key(|r| identities.find_map(|key| key.unwrap_stanzas(r)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> RecipientsDecryptor<R> {
|
||||
/// Attempts to decrypt the age file.
|
||||
///
|
||||
/// If successful, returns a reader that will provide the plaintext.
|
||||
pub fn decrypt<'a>(
|
||||
self,
|
||||
identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) -> Result<StreamReader<R>, DecryptError> {
|
||||
self.obtain_payload_key(identities)
|
||||
.map(|payload_key| Stream::decrypt(payload_key, self.0.input))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
|
||||
impl<R: AsyncRead + Unpin> RecipientsDecryptor<R> {
|
||||
/// Attempts to decrypt the age file.
|
||||
///
|
||||
/// If successful, returns a reader that will provide the plaintext.
|
||||
pub fn decrypt_async<'a>(
|
||||
self,
|
||||
identities: impl Iterator<Item = &'a dyn Identity>,
|
||||
) -> Result<StreamReader<R>, DecryptError> {
|
||||
self.obtain_payload_key(identities)
|
||||
.map(|payload_key| Stream::decrypt_async(payload_key, self.0.input))
|
||||
}
|
||||
}
|
||||
|
||||
/// Decryptor for an age file encrypted with a passphrase.
|
||||
pub struct PassphraseDecryptor<R>(BaseDecryptor<R>);
|
||||
|
||||
impl<R> PassphraseDecryptor<R> {
|
||||
pub(super) fn new(input: R, header: Header, nonce: Nonce) -> Self {
|
||||
PassphraseDecryptor(BaseDecryptor {
|
||||
input,
|
||||
header,
|
||||
nonce,
|
||||
})
|
||||
}
|
||||
|
||||
fn obtain_payload_key(
|
||||
&self,
|
||||
passphrase: &SecretString,
|
||||
max_work_factor: Option<u8>,
|
||||
) -> Result<PayloadKey, DecryptError> {
|
||||
let identity = scrypt::Identity {
|
||||
passphrase,
|
||||
max_work_factor,
|
||||
};
|
||||
|
||||
self.0.obtain_payload_key(|r| identity.unwrap_stanzas(r))
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> PassphraseDecryptor<R> {
|
||||
/// Attempts to decrypt the age file.
|
||||
///
|
||||
/// `max_work_factor` is the maximum accepted work factor. If `None`, the default
|
||||
/// maximum is adjusted to around 16 seconds of work.
|
||||
///
|
||||
/// If successful, returns a reader that will provide the plaintext.
|
||||
pub fn decrypt(
|
||||
self,
|
||||
passphrase: &SecretString,
|
||||
max_work_factor: Option<u8>,
|
||||
) -> Result<StreamReader<R>, DecryptError> {
|
||||
self.obtain_payload_key(passphrase, max_work_factor)
|
||||
.map(|payload_key| Stream::decrypt(payload_key, self.0.input))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
|
||||
impl<R: AsyncRead + Unpin> PassphraseDecryptor<R> {
|
||||
/// Attempts to decrypt the age file.
|
||||
///
|
||||
/// `max_work_factor` is the maximum accepted work factor. If `None`, the default
|
||||
/// maximum is adjusted to around 16 seconds of work.
|
||||
///
|
||||
/// If successful, returns a reader that will provide the plaintext.
|
||||
pub fn decrypt_async(
|
||||
self,
|
||||
passphrase: &SecretString,
|
||||
max_work_factor: Option<u8>,
|
||||
) -> Result<StreamReader<R>, DecryptError> {
|
||||
self.obtain_payload_key(passphrase, max_work_factor)
|
||||
.map(|payload_key| Stream::decrypt_async(payload_key, self.0.input))
|
||||
}
|
||||
}
|
|
@ -1,11 +1,20 @@
|
|||
//! The "scrypt" passphrase-based recipient type, native to age.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::iter;
|
||||
use std::time::Duration;
|
||||
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza, FILE_KEY_BYTES},
|
||||
primitives::{aead_decrypt, aead_encrypt},
|
||||
secrecy::{ExposeSecret, SecretString},
|
||||
};
|
||||
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||
use rand::{rngs::OsRng, RngCore};
|
||||
use std::time::Duration;
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
rngs::OsRng,
|
||||
RngCore,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::{
|
||||
|
@ -83,41 +92,121 @@ fn target_scrypt_work_factor() -> u8 {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) struct Recipient {
|
||||
pub(crate) passphrase: SecretString,
|
||||
/// A passphrase-based recipient. Anyone with the passphrase can decrypt the file.
|
||||
///
|
||||
/// If an `scrypt::Recipient` is used, it must be the only recipient for the file: it
|
||||
/// can't be mixed with other recipient types and can't be used multiple times for the
|
||||
/// same file.
|
||||
///
|
||||
/// This API should only be used with a passphrase that was provided by (or generated
|
||||
/// for) a human. For programmatic use cases, instead generate an [`x25519::Identity`].
|
||||
///
|
||||
/// [`x25519::Identity`]: crate::x25519::Identity
|
||||
pub struct Recipient {
|
||||
passphrase: SecretString,
|
||||
log_n: u8,
|
||||
}
|
||||
|
||||
impl Recipient {
|
||||
/// Constructs a new `Recipient` with the given passphrase.
|
||||
///
|
||||
/// The scrypt work factor is picked to target about 1 second for encryption or
|
||||
/// decryption on this device. Override it with [`Self::set_work_factor`].
|
||||
pub fn new(passphrase: SecretString) -> Self {
|
||||
Self {
|
||||
passphrase,
|
||||
log_n: target_scrypt_work_factor(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the scrypt work factor to `N = 2^log_n`.
|
||||
///
|
||||
/// This method must be called before [`Self::wrap_file_key`] to have an effect.
|
||||
///
|
||||
/// [`Self::wrap_file_key`]: crate::Recipient::wrap_file_key
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `log_n == 0` or `log_n >= 64`.
|
||||
pub fn set_work_factor(&mut self, log_n: u8) {
|
||||
assert!(0 < log_n && log_n < 64);
|
||||
self.log_n = log_n;
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Recipient for Recipient {
|
||||
fn wrap_file_key(&self, file_key: &FileKey) -> Result<Vec<Stanza>, EncryptError> {
|
||||
fn wrap_file_key(
|
||||
&self,
|
||||
file_key: &FileKey,
|
||||
) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
|
||||
let mut rng = OsRng;
|
||||
|
||||
let mut salt = [0; SALT_LEN];
|
||||
OsRng.fill_bytes(&mut salt);
|
||||
rng.fill_bytes(&mut salt);
|
||||
|
||||
let mut inner_salt = [0; SCRYPT_SALT_LABEL.len() + SALT_LEN];
|
||||
inner_salt[..SCRYPT_SALT_LABEL.len()].copy_from_slice(SCRYPT_SALT_LABEL);
|
||||
inner_salt[SCRYPT_SALT_LABEL.len()..].copy_from_slice(&salt);
|
||||
|
||||
let log_n = target_scrypt_work_factor();
|
||||
|
||||
let enc_key =
|
||||
scrypt(&inner_salt, log_n, self.passphrase.expose_secret()).expect("log_n < 64");
|
||||
scrypt(&inner_salt, self.log_n, self.passphrase.expose_secret()).expect("log_n < 64");
|
||||
let encrypted_file_key = aead_encrypt(&enc_key, file_key.expose_secret());
|
||||
|
||||
let encoded_salt = BASE64_STANDARD_NO_PAD.encode(salt);
|
||||
|
||||
Ok(vec![Stanza {
|
||||
tag: SCRYPT_RECIPIENT_TAG.to_owned(),
|
||||
args: vec![encoded_salt, format!("{}", log_n)],
|
||||
body: encrypted_file_key,
|
||||
}])
|
||||
let label = Alphanumeric.sample_string(&mut rng, 32);
|
||||
|
||||
Ok((
|
||||
vec![Stanza {
|
||||
tag: SCRYPT_RECIPIENT_TAG.to_owned(),
|
||||
args: vec![encoded_salt, format!("{}", self.log_n)],
|
||||
body: encrypted_file_key,
|
||||
}],
|
||||
iter::once(label).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Identity<'a> {
|
||||
pub(crate) passphrase: &'a SecretString,
|
||||
pub(crate) max_work_factor: Option<u8>,
|
||||
/// A passphrase-based identity. Anyone with the passphrase can decrypt the file.
|
||||
///
|
||||
/// The identity caps the amount of work that the [`Decryptor`] might have to do to
|
||||
/// process received files. A fairly high default is used (targeting roughly 16 seconds of
|
||||
/// work per stanza on the current machine), which might not be suitable for systems
|
||||
/// processing untrusted files.
|
||||
///
|
||||
/// [`Decryptor`]: crate::Decryptor
|
||||
pub struct Identity {
|
||||
passphrase: SecretString,
|
||||
target_work_factor: u8,
|
||||
max_work_factor: u8,
|
||||
}
|
||||
|
||||
impl<'a> crate::Identity for Identity<'a> {
|
||||
impl Identity {
|
||||
/// Constructs a new `Identity` with the given passphrase.
|
||||
pub fn new(passphrase: SecretString) -> Self {
|
||||
let target_work_factor = target_scrypt_work_factor();
|
||||
|
||||
// Place bounds on the work factor we will accept (roughly 16 seconds).
|
||||
let max_work_factor = target_work_factor + 4;
|
||||
|
||||
Self {
|
||||
passphrase,
|
||||
target_work_factor,
|
||||
max_work_factor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the maximum accepted scrypt work factor to `N = 2^max_log_n`.
|
||||
///
|
||||
/// This method must be called before [`Self::unwrap_stanza`] to have an effect.
|
||||
///
|
||||
/// [`Self::unwrap_stanza`]: crate::Identity::unwrap_stanza
|
||||
pub fn set_max_work_factor(&mut self, max_log_n: u8) {
|
||||
self.max_work_factor = max_log_n;
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Identity for Identity {
|
||||
fn unwrap_stanza(&self, stanza: &Stanza) -> Option<Result<FileKey, DecryptError>> {
|
||||
if stanza.tag != SCRYPT_RECIPIENT_TAG {
|
||||
return None;
|
||||
|
@ -139,12 +228,10 @@ impl<'a> crate::Identity for Identity<'a> {
|
|||
return Some(Err(DecryptError::InvalidHeader));
|
||||
}
|
||||
|
||||
// Place bounds on the work factor we will accept (roughly 16 seconds).
|
||||
let target = target_scrypt_work_factor();
|
||||
if log_n > self.max_work_factor.unwrap_or(target + 4) {
|
||||
if log_n > self.max_work_factor {
|
||||
return Some(Err(DecryptError::ExcessiveWork {
|
||||
required: log_n,
|
||||
target,
|
||||
target: self.target_work_factor,
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -157,7 +244,7 @@ impl<'a> crate::Identity for Identity<'a> {
|
|||
Err(_) => {
|
||||
return Some(Err(DecryptError::ExcessiveWork {
|
||||
required: log_n,
|
||||
target,
|
||||
target: self.target_work_factor,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
@ -173,9 +260,10 @@ impl<'a> crate::Identity for Identity<'a> {
|
|||
aead_decrypt(&enc_key, FILE_KEY_BYTES, &stanza.body)
|
||||
.map(|mut pt| {
|
||||
// It's ours!
|
||||
let file_key: [u8; FILE_KEY_BYTES] = pt[..].try_into().unwrap();
|
||||
pt.zeroize();
|
||||
file_key.into()
|
||||
FileKey::init_with_mut(|file_key| {
|
||||
file_key.copy_from_slice(&pt);
|
||||
pt.zeroize();
|
||||
})
|
||||
})
|
||||
.map_err(DecryptError::from),
|
||||
)
|
||||
|
|
107
age/src/simple.rs
Normal file
107
age/src/simple.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use std::io::{Read, Write};
|
||||
use std::iter;
|
||||
|
||||
use crate::{
|
||||
error::{DecryptError, EncryptError},
|
||||
Decryptor, Encryptor, Identity, Recipient,
|
||||
};
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
use crate::armor::{ArmoredReader, ArmoredWriter, Format};
|
||||
|
||||
/// Encrypts the given plaintext to the given recipient.
|
||||
///
|
||||
/// To encrypt to more than one recipient, use [`Encryptor::with_recipients`].
|
||||
///
|
||||
/// This function returns binary ciphertext. To obtain an ASCII-armored text string, use
|
||||
/// [`encrypt_and_armor`].
|
||||
pub fn encrypt(recipient: &impl Recipient, plaintext: &[u8]) -> Result<Vec<u8>, EncryptError> {
|
||||
let encryptor =
|
||||
Encryptor::with_recipients(iter::once(recipient as _)).expect("we provided a recipient");
|
||||
|
||||
let mut ciphertext = Vec::with_capacity(plaintext.len());
|
||||
let mut writer = encryptor.wrap_output(&mut ciphertext)?;
|
||||
writer.write_all(plaintext)?;
|
||||
writer.finish()?;
|
||||
|
||||
Ok(ciphertext)
|
||||
}
|
||||
|
||||
/// Encrypts the given plaintext to the given recipient, and wraps the ciphertext in ASCII
|
||||
/// armor.
|
||||
///
|
||||
/// To encrypt to more than one recipient, use [`Encryptor::with_recipients`] along with
|
||||
/// [`ArmoredWriter`].
|
||||
#[cfg(feature = "armor")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "armor")))]
|
||||
pub fn encrypt_and_armor(
|
||||
recipient: &impl Recipient,
|
||||
plaintext: &[u8],
|
||||
) -> Result<String, EncryptError> {
|
||||
let encryptor =
|
||||
Encryptor::with_recipients(iter::once(recipient as _)).expect("we provided a recipient");
|
||||
|
||||
let mut ciphertext = Vec::with_capacity(plaintext.len());
|
||||
let mut writer = encryptor.wrap_output(ArmoredWriter::wrap_output(
|
||||
&mut ciphertext,
|
||||
Format::AsciiArmor,
|
||||
)?)?;
|
||||
writer.write_all(plaintext)?;
|
||||
writer.finish()?.finish()?;
|
||||
|
||||
Ok(String::from_utf8(ciphertext).expect("is armored"))
|
||||
}
|
||||
|
||||
/// Decrypts the given ciphertext with the given identity.
|
||||
///
|
||||
/// If the `armor` feature flag is enabled, this will also handle armored age ciphertexts.
|
||||
///
|
||||
/// To attempt decryption with more than one identity, use [`Decryptor`] (as well as
|
||||
/// [`ArmoredReader`] if the `armor` feature flag is enabled).
|
||||
pub fn decrypt(identity: &impl Identity, ciphertext: &[u8]) -> Result<Vec<u8>, DecryptError> {
|
||||
#[cfg(feature = "armor")]
|
||||
let decryptor = Decryptor::new_buffered(ArmoredReader::new(ciphertext))?;
|
||||
|
||||
#[cfg(not(feature = "armor"))]
|
||||
let decryptor = Decryptor::new_buffered(ciphertext)?;
|
||||
|
||||
let mut plaintext = vec![];
|
||||
let mut reader = decryptor.decrypt(iter::once(identity as _))?;
|
||||
reader.read_to_end(&mut plaintext)?;
|
||||
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{decrypt, encrypt};
|
||||
use crate::x25519;
|
||||
|
||||
#[cfg(feature = "armor")]
|
||||
use super::encrypt_and_armor;
|
||||
|
||||
#[test]
|
||||
fn x25519_round_trip() {
|
||||
let sk: x25519::Identity = crate::x25519::tests::TEST_SK.parse().unwrap();
|
||||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.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 x25519_round_trip_armor() {
|
||||
let sk: x25519::Identity = crate::x25519::tests::TEST_SK.parse().unwrap();
|
||||
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.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[..]);
|
||||
}
|
||||
}
|
|
@ -194,7 +194,7 @@ mod decrypt {
|
|||
}
|
||||
|
||||
mod read_ssh {
|
||||
use age_core::secrecy::Secret;
|
||||
use age_core::secrecy::SecretBox;
|
||||
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
|
@ -349,14 +349,14 @@ mod read_ssh {
|
|||
/// Internal OpenSSH encoding of an Ed25519 private key.
|
||||
///
|
||||
/// - [OpenSSH serialization code](https://github.com/openssh/openssh-portable/blob/4103a3ec7c68493dbc4f0994a229507e943a86d3/sshkey.c#L3277-L3283)
|
||||
fn openssh_ed25519_privkey(input: &[u8]) -> IResult<&[u8], Secret<[u8; 64]>> {
|
||||
fn openssh_ed25519_privkey(input: &[u8]) -> IResult<&[u8], SecretBox<[u8; 64]>> {
|
||||
delimited(
|
||||
string_tag(SSH_ED25519_KEY_PREFIX),
|
||||
map_opt(tuple((string, string)), |(pubkey_bytes, privkey_bytes)| {
|
||||
if privkey_bytes.len() == 64 && pubkey_bytes == &privkey_bytes[32..64] {
|
||||
let mut privkey = [0; 64];
|
||||
let mut privkey = Box::new([0; 64]);
|
||||
privkey.copy_from_slice(privkey_bytes);
|
||||
Some(Secret::new(privkey))
|
||||
Some(SecretBox::new(privkey))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use age_core::{
|
||||
format::{FileKey, Stanza, FILE_KEY_BYTES},
|
||||
primitives::{aead_decrypt, hkdf},
|
||||
secrecy::{ExposeSecret, Secret},
|
||||
secrecy::{ExposeSecret, SecretBox},
|
||||
};
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use nom::{
|
||||
|
@ -32,12 +32,27 @@ use crate::{
|
|||
};
|
||||
|
||||
/// An SSH private key for decrypting an age file.
|
||||
#[derive(Clone)]
|
||||
pub enum UnencryptedKey {
|
||||
/// An ssh-rsa private key.
|
||||
SshRsa(Vec<u8>, Box<rsa::RsaPrivateKey>),
|
||||
/// An ssh-ed25519 key pair.
|
||||
SshEd25519(Vec<u8>, Secret<[u8; 64]>),
|
||||
SshEd25519(Vec<u8>, SecretBox<[u8; 64]>),
|
||||
}
|
||||
|
||||
impl Clone for UnencryptedKey {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::SshRsa(ssh_key, sk) => Self::SshRsa(ssh_key.clone(), sk.clone()),
|
||||
Self::SshEd25519(ssh_key, privkey) => Self::SshEd25519(
|
||||
ssh_key.clone(),
|
||||
SecretBox::new({
|
||||
let mut cloned_privkey = Box::new([0; 64]);
|
||||
cloned_privkey.copy_from_slice(privkey.expose_secret());
|
||||
cloned_privkey
|
||||
}),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UnencryptedKey {
|
||||
|
@ -64,11 +79,18 @@ impl UnencryptedKey {
|
|||
&stanza.body,
|
||||
)
|
||||
.map_err(DecryptError::from)
|
||||
.map(|mut pt| {
|
||||
.and_then(|mut pt| {
|
||||
// It's ours!
|
||||
let file_key: [u8; 16] = pt[..].try_into().unwrap();
|
||||
pt.zeroize();
|
||||
file_key.into()
|
||||
FileKey::try_init_with_mut(|file_key| {
|
||||
let ret = if pt.len() == file_key.len() {
|
||||
file_key.copy_from_slice(&pt);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(DecryptError::DecryptionFailed)
|
||||
};
|
||||
pt.zeroize();
|
||||
ret
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
@ -115,9 +137,10 @@ impl UnencryptedKey {
|
|||
.map_err(DecryptError::from)
|
||||
.map(|mut pt| {
|
||||
// It's ours!
|
||||
let file_key: [u8; FILE_KEY_BYTES] = pt[..].try_into().unwrap();
|
||||
pt.zeroize();
|
||||
file_key.into()
|
||||
FileKey::init_with_mut(|file_key| {
|
||||
file_key.copy_from_slice(&pt);
|
||||
pt.zeroize();
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
@ -354,7 +377,10 @@ pub(crate) fn ssh_identity(input: &str) -> IResult<&str, Identity> {
|
|||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use age_core::secrecy::{ExposeSecret, SecretString};
|
||||
use age_core::{
|
||||
format::FileKey,
|
||||
secrecy::{ExposeSecret, SecretString},
|
||||
};
|
||||
use std::io::BufReader;
|
||||
|
||||
use super::{Identity, UnsupportedKey};
|
||||
|
@ -491,7 +517,7 @@ AwQFBg==
|
|||
}
|
||||
|
||||
fn request_passphrase(&self, _: &str) -> Option<SecretString> {
|
||||
Some(SecretString::new(self.0.to_owned()))
|
||||
Some(SecretString::from(self.0.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -505,9 +531,10 @@ AwQFBg==
|
|||
};
|
||||
let pk: Recipient = TEST_SSH_RSA_PK.parse().unwrap();
|
||||
|
||||
let file_key = [12; 16].into();
|
||||
let file_key = FileKey::new(Box::new([12; 16]));
|
||||
|
||||
let wrapped = pk.wrap_file_key(&file_key).unwrap();
|
||||
let (wrapped, labels) = pk.wrap_file_key(&file_key).unwrap();
|
||||
assert!(labels.is_empty());
|
||||
let unwrapped = identity.unwrap_stanzas(&wrapped);
|
||||
assert_eq!(
|
||||
unwrapped.unwrap().unwrap().expose_secret(),
|
||||
|
@ -531,9 +558,10 @@ AwQFBg==
|
|||
let identity = identity.with_callbacks(TestPassphrase("passphrase"));
|
||||
let pk: Recipient = TEST_SSH_ED25519_PK.parse().unwrap();
|
||||
|
||||
let file_key = [12; 16].into();
|
||||
let file_key = FileKey::new(Box::new([12; 16]));
|
||||
|
||||
let wrapped = pk.wrap_file_key(&file_key).unwrap();
|
||||
let (wrapped, labels) = pk.wrap_file_key(&file_key).unwrap();
|
||||
assert!(labels.is_empty());
|
||||
let unwrapped = identity.unwrap_stanzas(&wrapped);
|
||||
assert_eq!(
|
||||
unwrapped.unwrap().unwrap().expose_secret(),
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza},
|
||||
primitives::{aead_encrypt, hkdf},
|
||||
|
@ -18,7 +21,6 @@ use nom::{
|
|||
use rand::rngs::OsRng;
|
||||
use rsa::{traits::PublicKeyParts, Oaep};
|
||||
use sha2::Sha256;
|
||||
use std::fmt;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey, StaticSecret};
|
||||
|
||||
use super::{
|
||||
|
@ -144,10 +146,13 @@ impl TryFrom<Identity> for Recipient {
|
|||
}
|
||||
|
||||
impl crate::Recipient for Recipient {
|
||||
fn wrap_file_key(&self, file_key: &FileKey) -> Result<Vec<Stanza>, EncryptError> {
|
||||
fn wrap_file_key(
|
||||
&self,
|
||||
file_key: &FileKey,
|
||||
) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
|
||||
let mut rng = OsRng;
|
||||
|
||||
match self {
|
||||
let stanzas = match self {
|
||||
Recipient::SshRsa(ssh_key, pk) => {
|
||||
let encrypted_file_key = pk
|
||||
.encrypt(
|
||||
|
@ -159,11 +164,11 @@ impl crate::Recipient for Recipient {
|
|||
|
||||
let encoded_tag = BASE64_STANDARD_NO_PAD.encode(ssh_tag(ssh_key));
|
||||
|
||||
Ok(vec![Stanza {
|
||||
vec![Stanza {
|
||||
tag: SSH_RSA_RECIPIENT_TAG.to_owned(),
|
||||
args: vec![encoded_tag],
|
||||
body: encrypted_file_key,
|
||||
}])
|
||||
}]
|
||||
}
|
||||
Recipient::SshEd25519(ssh_key, ed25519_pk) => {
|
||||
let pk: X25519PublicKey = ed25519_pk.to_montgomery().to_bytes().into();
|
||||
|
@ -190,13 +195,15 @@ impl crate::Recipient for Recipient {
|
|||
let encoded_tag = BASE64_STANDARD_NO_PAD.encode(ssh_tag(ssh_key));
|
||||
let encoded_epk = BASE64_STANDARD_NO_PAD.encode(epk.as_bytes());
|
||||
|
||||
Ok(vec![Stanza {
|
||||
vec![Stanza {
|
||||
tag: SSH_ED25519_RECIPIENT_TAG.to_owned(),
|
||||
args: vec![encoded_tag, encoded_epk],
|
||||
body: encrypted_file_key,
|
||||
}])
|
||||
}]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((stanzas, HashSet::new()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
//! The "x25519" recipient type, native to age.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use age_core::{
|
||||
format::{FileKey, Stanza, FILE_KEY_BYTES},
|
||||
primitives::{aead_decrypt, aead_encrypt, hkdf},
|
||||
|
@ -8,7 +11,6 @@ use age_core::{
|
|||
use base64::{prelude::BASE64_STANDARD_NO_PAD, Engine};
|
||||
use bech32::{ToBase32, Variant};
|
||||
use rand::rngs::OsRng;
|
||||
use std::fmt;
|
||||
use subtle::ConstantTimeEq;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret};
|
||||
use zeroize::Zeroize;
|
||||
|
@ -66,7 +68,7 @@ impl Identity {
|
|||
let sk_base32 = sk_bytes.to_base32();
|
||||
let mut encoded =
|
||||
bech32::encode(SECRET_KEY_PREFIX, sk_base32, Variant::Bech32).expect("HRP is valid");
|
||||
let ret = SecretString::new(encoded.to_uppercase());
|
||||
let ret = SecretString::from(encoded.to_uppercase());
|
||||
|
||||
// Clear intermediates
|
||||
sk_bytes.zeroize();
|
||||
|
@ -134,9 +136,10 @@ impl crate::Identity for Identity {
|
|||
.ok()
|
||||
.map(|mut pt| {
|
||||
// It's ours!
|
||||
let file_key: [u8; FILE_KEY_BYTES] = pt[..].try_into().unwrap();
|
||||
pt.zeroize();
|
||||
Ok(file_key.into())
|
||||
Ok(FileKey::init_with_mut(|file_key| {
|
||||
file_key.copy_from_slice(&pt);
|
||||
pt.zeroize();
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +194,10 @@ impl fmt::Debug for Recipient {
|
|||
}
|
||||
|
||||
impl crate::Recipient for Recipient {
|
||||
fn wrap_file_key(&self, file_key: &FileKey) -> Result<Vec<Stanza>, EncryptError> {
|
||||
fn wrap_file_key(
|
||||
&self,
|
||||
file_key: &FileKey,
|
||||
) -> Result<(Vec<Stanza>, HashSet<String>), EncryptError> {
|
||||
let rng = OsRng;
|
||||
let esk = EphemeralSecret::random_from_rng(rng);
|
||||
let epk: PublicKey = (&esk).into();
|
||||
|
@ -220,17 +226,20 @@ impl crate::Recipient for Recipient {
|
|||
|
||||
let encoded_epk = BASE64_STANDARD_NO_PAD.encode(epk.as_bytes());
|
||||
|
||||
Ok(vec![Stanza {
|
||||
tag: X25519_RECIPIENT_TAG.to_owned(),
|
||||
args: vec![encoded_epk],
|
||||
body: encrypted_file_key,
|
||||
}])
|
||||
Ok((
|
||||
vec![Stanza {
|
||||
tag: X25519_RECIPIENT_TAG.to_owned(),
|
||||
args: vec![encoded_epk],
|
||||
body: encrypted_file_key,
|
||||
}],
|
||||
HashSet::new(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use age_core::secrecy::ExposeSecret;
|
||||
use age_core::{format::FileKey, secrecy::ExposeSecret};
|
||||
use proptest::prelude::*;
|
||||
use x25519_dalek::{PublicKey, StaticSecret};
|
||||
|
||||
|
@ -257,18 +266,20 @@ pub(crate) mod tests {
|
|||
proptest! {
|
||||
#[test]
|
||||
fn wrap_and_unwrap(sk_bytes in proptest::collection::vec(any::<u8>(), ..=32)) {
|
||||
let file_key = [7; 16].into();
|
||||
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);
|
||||
StaticSecret::from(tmp)
|
||||
};
|
||||
|
||||
let stanzas = Recipient(PublicKey::from(&sk))
|
||||
let res = Recipient(PublicKey::from(&sk))
|
||||
.wrap_file_key(&file_key);
|
||||
prop_assert!(stanzas.is_ok());
|
||||
prop_assert!(res.is_ok());
|
||||
let (stanzas, labels) = res.unwrap();
|
||||
prop_assert!(labels.is_empty());
|
||||
|
||||
let res = Identity(sk).unwrap_stanzas(&stanzas.unwrap());
|
||||
let res = Identity(sk).unwrap_stanzas(&stanzas);
|
||||
prop_assert!(res.is_some());
|
||||
let res = res.unwrap();
|
||||
prop_assert!(res.is_ok());
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use age_core::secrecy::SecretString;
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
|
||||
use age::scrypt;
|
||||
use age_core::secrecy::SecretString;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "cli-common")]
|
||||
fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
@ -22,30 +24,29 @@ fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
|
|||
let name = path.file_stem().unwrap().to_str().unwrap();
|
||||
let expect_failure = name.starts_with("fail_");
|
||||
|
||||
let res = match age::Decryptor::new(fs::File::open(&path)?)? {
|
||||
age::Decryptor::Recipients(d) => {
|
||||
let identities = age::cli_common::read_identities(
|
||||
vec![format!(
|
||||
"{}/{}_key.txt",
|
||||
path.parent().unwrap().to_str().unwrap(),
|
||||
name
|
||||
)],
|
||||
None,
|
||||
&mut StdinGuard::new(false),
|
||||
)?;
|
||||
d.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity))
|
||||
}
|
||||
age::Decryptor::Passphrase(d) => {
|
||||
let mut passphrase = String::new();
|
||||
fs::File::open(format!(
|
||||
"{}/{}_password.txt",
|
||||
let d = age::Decryptor::new(fs::File::open(&path)?)?;
|
||||
let res = if !d.is_scrypt() {
|
||||
let identities = age::cli_common::read_identities(
|
||||
vec![format!(
|
||||
"{}/{}_key.txt",
|
||||
path.parent().unwrap().to_str().unwrap(),
|
||||
name
|
||||
))?
|
||||
.read_to_string(&mut passphrase)?;
|
||||
let passphrase = SecretString::new(passphrase);
|
||||
d.decrypt(&passphrase, None)
|
||||
}
|
||||
)],
|
||||
None,
|
||||
&mut StdinGuard::new(false),
|
||||
)?;
|
||||
d.decrypt(identities.iter().map(|i| i.as_ref() as &dyn age::Identity))
|
||||
} else {
|
||||
let mut passphrase = String::new();
|
||||
fs::File::open(format!(
|
||||
"{}/{}_password.txt",
|
||||
path.parent().unwrap().to_str().unwrap(),
|
||||
name
|
||||
))?
|
||||
.read_to_string(&mut passphrase)?;
|
||||
let passphrase = SecretString::from(passphrase);
|
||||
let identity = scrypt::Identity::new(passphrase);
|
||||
d.decrypt(Some(&identity as _).into_iter())
|
||||
};
|
||||
|
||||
match (res, expect_failure) {
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
|||
|
||||
use age::{
|
||||
armor::{ArmoredReadError, ArmoredReader},
|
||||
scrypt,
|
||||
secrecy::SecretString,
|
||||
x25519, DecryptError, Decryptor, Identity,
|
||||
};
|
||||
|
@ -131,14 +132,15 @@ fn testkit(filename: &str) {
|
|||
let testfile = TestFile::parse(filename);
|
||||
let comment = format_testkit_comment(&testfile);
|
||||
|
||||
match Decryptor::new(ArmoredReader::new(&testfile.age_file[..])).and_then(|d| match d {
|
||||
Decryptor::Recipients(d) => {
|
||||
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))
|
||||
}
|
||||
Decryptor::Passphrase(d) => {
|
||||
} else {
|
||||
let passphrase = get_testkit_passphrase(&testfile, &comment);
|
||||
d.decrypt(&passphrase, Some(16))
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
identity.set_max_work_factor(16);
|
||||
d.decrypt(Some(&identity as _).into_iter())
|
||||
}
|
||||
}) {
|
||||
Ok(mut r) => {
|
||||
|
@ -268,18 +270,17 @@ fn testkit_buffered(filename: &str) {
|
|||
let testfile = TestFile::parse(filename);
|
||||
let comment = format_testkit_comment(&testfile);
|
||||
|
||||
match Decryptor::new_buffered(ArmoredReader::new(&testfile.age_file[..])).and_then(
|
||||
|d| match d {
|
||||
Decryptor::Recipients(d) => {
|
||||
let identities = get_testkit_identities(filename, &testfile);
|
||||
d.decrypt(identities.iter().map(|i| i as &dyn Identity))
|
||||
}
|
||||
Decryptor::Passphrase(d) => {
|
||||
let passphrase = get_testkit_passphrase(&testfile, &comment);
|
||||
d.decrypt(&passphrase, Some(16))
|
||||
}
|
||||
},
|
||||
) {
|
||||
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))
|
||||
} else {
|
||||
let passphrase = get_testkit_passphrase(&testfile, &comment);
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
identity.set_max_work_factor(16);
|
||||
d.decrypt(Some(&identity as _).into_iter())
|
||||
}
|
||||
}) {
|
||||
Ok(mut r) => {
|
||||
let mut payload = vec![];
|
||||
let res = io::Read::read_to_end(&mut r, &mut payload);
|
||||
|
@ -410,14 +411,15 @@ async fn testkit_async(filename: &str) {
|
|||
|
||||
match Decryptor::new_async(ArmoredReader::from_async_reader(&testfile.age_file[..]))
|
||||
.await
|
||||
.and_then(|d| match d {
|
||||
Decryptor::Recipients(d) => {
|
||||
.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))
|
||||
}
|
||||
Decryptor::Passphrase(d) => {
|
||||
} else {
|
||||
let passphrase = get_testkit_passphrase(&testfile, &comment);
|
||||
d.decrypt_async(&passphrase, Some(16))
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
identity.set_max_work_factor(16);
|
||||
d.decrypt_async(Some(&identity as _).into_iter())
|
||||
}
|
||||
}) {
|
||||
Ok(mut r) => {
|
||||
|
@ -550,14 +552,15 @@ async fn testkit_async_buffered(filename: &str) {
|
|||
|
||||
match Decryptor::new_async_buffered(ArmoredReader::from_async_reader(&testfile.age_file[..]))
|
||||
.await
|
||||
.and_then(|d| match d {
|
||||
Decryptor::Recipients(d) => {
|
||||
.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))
|
||||
}
|
||||
Decryptor::Passphrase(d) => {
|
||||
} else {
|
||||
let passphrase = get_testkit_passphrase(&testfile, &comment);
|
||||
d.decrypt_async(&passphrase, Some(16))
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
identity.set_max_work_factor(16);
|
||||
d.decrypt_async(Some(&identity as _).into_iter())
|
||||
}
|
||||
}) {
|
||||
Ok(mut r) => {
|
||||
|
|
753
fuzz-afl/Cargo.lock
generated
753
fuzz-afl/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@ publish = false
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
afl = "0.8"
|
||||
afl = "0.15"
|
||||
age = { path = "../age" }
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
|
|
628
fuzz/Cargo.lock
generated
628
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -7,12 +7,6 @@ use age::Decryptor;
|
|||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(decryptor) = Decryptor::new(data) {
|
||||
match decryptor {
|
||||
Decryptor::Recipients(d) => {
|
||||
let _ = d.decrypt(iter::empty());
|
||||
}
|
||||
// Don't pay the cost of scrypt while fuzzing.
|
||||
Decryptor::Passphrase(_) => (),
|
||||
}
|
||||
let _ = decryptor.decrypt(iter::empty());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -7,12 +7,6 @@ use age::Decryptor;
|
|||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
if let Ok(decryptor) = Decryptor::new_buffered(data) {
|
||||
match decryptor {
|
||||
Decryptor::Recipients(d) => {
|
||||
let _ = d.decrypt(iter::empty());
|
||||
}
|
||||
// Don't pay the cost of scrypt while fuzzing.
|
||||
Decryptor::Passphrase(_) => (),
|
||||
}
|
||||
let _ = decryptor.decrypt(iter::empty());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,13 +10,20 @@ to 1.0.0 are beta releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1] - 2024-11-18
|
||||
## [0.6.1, 0.7.2, 0.8.2, 0.9.3, 0.10.1, 0.11.1] - 2024-12-18
|
||||
### Security
|
||||
- The age plugin protocol previously allowed plugin names that could be
|
||||
interpreted as file paths. Under certain conditions, this could lead to a
|
||||
different binary being executed as an age plugin than intended. Plugin names
|
||||
are now required to only contain alphanumeric characters or the four special
|
||||
characters `+-._`.
|
||||
- Fixed a security vulnerability that could allow an attacker to execute an
|
||||
arbitrary binary under certain conditions. See GHSA-4fg7-vxc8-qx5w. Plugin
|
||||
names are now required to only contain alphanumeric characters or the four
|
||||
special characters `+-._`. Thanks to ⬡-49016 for reporting this issue.
|
||||
|
||||
## [0.11.0] - 2024-11-03
|
||||
### Added
|
||||
- Partial French translation!
|
||||
|
||||
### Fixed
|
||||
- [Unix] Files can now be encrypted with `rage --passphrase` when piped over
|
||||
stdin, without requiring an explicit `-` argument as `INPUT`.
|
||||
|
||||
## [0.10.0] - 2024-02-04
|
||||
### Added
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "rage"
|
||||
description = "[BETA] A simple, secure, and modern encryption tool."
|
||||
version = "0.10.1"
|
||||
version = "0.11.1"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
readme = "../README.md"
|
||||
|
|
|
@ -146,14 +146,6 @@ err-ux-B = Tell us
|
|||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Identity file '{$filename}' contains identities for '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Try using '{-age-plugin-}{$plugin_name}' to convert this identity to a recipient.
|
||||
|
||||
err-no-identities-in-file = No identities found in file '{$filename}'.
|
||||
err-no-identities-in-stdin = No identities found in standard input.
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Could not write to stdout: {$err}
|
||||
|
@ -161,7 +153,6 @@ rec-enc-broken-stdout = Are you piping to a program that isn't reading from stdi
|
|||
|
||||
err-enc-broken-file = Could not write to file: {$err}
|
||||
|
||||
err-enc-missing-recipients = Missing recipients.
|
||||
rec-enc-missing-recipients = Did you forget to specify {-flag-recipient}?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} can't be used with {-flag-passphrase}.
|
||||
|
|
|
@ -120,7 +120,6 @@ rec-enc-broken-stdout = Estás enviando por pipe a un programa que no está leye
|
|||
|
||||
err-enc-broken-file = No se pudo escribir al archivo: {$err}
|
||||
|
||||
err-enc-missing-recipients = No se encontraron destinatarios.
|
||||
rec-enc-missing-recipients = ¿Te olvidaste de especificar {-flag-recipient}?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} no puede ser usado con {-flag-passphrase}.
|
||||
|
|
505
rage/i18n/fr/rage.ftl
Normal file
505
rage/i18n/fr/rage.ftl
Normal file
|
@ -0,0 +1,505 @@
|
|||
# Copyright 2020 Jack Grigg
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
# http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
# <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
# option. This file may not be copied, modified, or distributed
|
||||
# except according to those terms.
|
||||
|
||||
### Localization for strings in the rage CLI tools
|
||||
|
||||
## Terms (not to be localized)
|
||||
|
||||
-age = age
|
||||
-age-plugin- = age-plugin-
|
||||
-rage = rage
|
||||
-rage-keygen = rage-keygen
|
||||
-stdin = "-"
|
||||
-recipient-prefix = age1
|
||||
-identity-prefix = AGE-SECRET-KEY-1
|
||||
-armor-pem-type = AGE ENCRYPTED FILE
|
||||
|
||||
-rage-mount = rage-mount
|
||||
|
||||
-ssh-rsa = ssh-rsa
|
||||
-ssh-ed25519 = ssh-ed25519
|
||||
-ssh-authorized-keys = authorized_keys
|
||||
-dot-keys = .keys
|
||||
-ssh = ssh(1)
|
||||
-authorized-keys-file-format = AUTHORIZED_KEYS FILE FORMAT
|
||||
-sshd = sshd(8)
|
||||
-ssh-agent = ssh-agent(1)
|
||||
|
||||
-example = example
|
||||
-example-r = age1example1
|
||||
-example-i = AGE-PLUGIN-EXAMPLE-1
|
||||
|
||||
-yubikey = yubikey
|
||||
|
||||
## CLI flags (not to be localized)
|
||||
|
||||
-flag-armor = -a/--armor
|
||||
-flag-decrypt = -d/--decrypt
|
||||
-flag-encrypt = -e/--encrypt
|
||||
-flag-identity = -i/--identity
|
||||
-flag-output = -o/--output
|
||||
-flag-recipient = -r/--recipient
|
||||
-flag-recipients-file = -R/--recipients-file
|
||||
-flag-passphrase = -p/--passphrase
|
||||
-flag-plugin-name = -j
|
||||
-flag-max-work-factor = --max-work-factor
|
||||
-flag-unstable = --features unstable
|
||||
|
||||
-flag-convert = -y
|
||||
|
||||
-flag-mnt-types = -t/--types
|
||||
|
||||
## Helper variables, to be localized
|
||||
|
||||
# Used throughout to indicate that a flag X cannot be used with another flag Y
|
||||
-cantuse = ne peut pas être utilisé avec
|
||||
|
||||
## Usage
|
||||
|
||||
usage-header = Utilisation
|
||||
|
||||
recipient = RECIPIENT
|
||||
recipients-file = PATH
|
||||
identity = IDENTITY
|
||||
plugin-name = PLUGIN-NAME
|
||||
input = INPUT
|
||||
output = OUTPUT
|
||||
|
||||
args-header = Arguments
|
||||
|
||||
help-arg-input = Chemin vers un fichier à lire.
|
||||
|
||||
flags-header = Options
|
||||
|
||||
help-flag-help = Affiche ce message d'aide et quitte.
|
||||
help-flag-version = Affiche les informations de version et quitte.
|
||||
help-flag-encrypt = Chiffre l'input (l'option par défaut).
|
||||
help-flag-decrypt = Déchiffre l'input.
|
||||
help-flag-passphrase = Chiffre avec une phrase secrète au lieu de destinataires.
|
||||
help-flag-max-work-factor = Facteur d'effort maximum à autoriser pour déchiffrer avec une phrase secrète.
|
||||
help-flag-armor = Chiffre au format d'encodage PEM.
|
||||
help-flag-recipient = Chiffre pour le {destinataire} spécifié. Peut être répété.
|
||||
help-flag-recipients-file = Chiffre pour les destinataires listés dans le fichier {recipients-file}. Peut être répété.
|
||||
help-flag-identity = Utilise le fichier d'identité {identity}. Peut être répété.
|
||||
help-flag-plugin-name = Utilise {-age-plugin-}{plugin-name} dans son mode par défaut en tant qu'identité.
|
||||
help-flag-output = Ecrit le résultat dans le fichier situé au chemin {output}.
|
||||
|
||||
rage-after-help-content =
|
||||
{input} est par défaut l'entrée standard (stdin), tandis que {output} est par défaut la sortie standard (stdout).
|
||||
Si {output} existe, il sera écrasé.
|
||||
|
||||
{recipient} peut être:
|
||||
- Une clef publique {-age}, telle que générée par {$keygen_name} ({$example_age_pubkey}).
|
||||
- Une clef publique SSH ({$example_ssh_pubkey}).
|
||||
|
||||
{recipients-file} est le chemin vers un fichier contenant des destinataires {-age}, un par ligne
|
||||
(en ignorant les lignes vides et les commentaires préfixés par "#"). {-stdin} peut être utilisé
|
||||
pour lire des destinataires depuis l'entrée standard.
|
||||
|
||||
{identity} est un chemin vers un fichier avec des identités {-age}, une par ligne
|
||||
(en ignorant les lignes vides et les commentaires préfixés par "#"), ou vers un ficher de clef SSH.
|
||||
Les fichiers d'identité {-age} protégé par phrase secrète peuvent être utilisé comme fichier d'identité.
|
||||
Plusieurs identités peuvent être fournis, et les inutilisées seront ignorées.
|
||||
{-stdin} peut être utilisé pour lire des identités depuis l'entrée standard.
|
||||
|
||||
rage-after-help-example =
|
||||
Exemple:
|
||||
{" "}{$example_a}
|
||||
{" "}{tty-pubkey}: {$example_a_output}
|
||||
{" "}{$example_b}
|
||||
{" "}{$example_c}
|
||||
|
||||
keygen-help-flag-output = {help-flag-output} Par défaut, la sortie standard.
|
||||
keygen-help-flag-convert = Convertit un fichier d'identité en un fichier de destinataires.
|
||||
|
||||
## Formatting
|
||||
|
||||
warning-msg = Attention: {$warning}
|
||||
|
||||
## Keygen messages
|
||||
|
||||
tty-pubkey = Clef publique
|
||||
identity-file-created = créée
|
||||
identity-file-pubkey = clef publique
|
||||
|
||||
## Encryption messages
|
||||
|
||||
autogenerated-passphrase = Utilisé une phrase secrète auto-générée:
|
||||
type-passphrase = Ecrivez la phrase secrète
|
||||
prompt-passphrase = Phrase secrète
|
||||
|
||||
warn-double-encrypting = Chiffrement d'un fichier déjà chiffré
|
||||
|
||||
## General errors
|
||||
|
||||
err-failed-to-open-input = Echec d'ouverture de l'entrée: {$err}
|
||||
err-failed-to-open-output = Echec d'ouverture de la sortie: {$err}
|
||||
err-failed-to-read-input = Echec de lecture de l'entrée: {$err}
|
||||
err-failed-to-write-output = Echec d'écriture vers la sortie: {$err}
|
||||
err-identity-ambiguous = {-flag-identity} nécessite {-flag-encrypt} ou {-flag-decrypt}.
|
||||
err-mixed-encrypt-decrypt = {-flag-encrypt} {-cantuse} {-flag-decrypt}.
|
||||
err-passphrase-timed-out = Délai dépassé lors de l'attente d'entrée de la phrase secrète.
|
||||
err-same-input-and-output = L'entrée et la sortie sont le même fichier {$filename}'.
|
||||
|
||||
err-ux-A = Est-ce que {-rage} n'a pas fait ce que vous escomptiez ? Est-ce qu'une erreur serait plus utile ?
|
||||
err-ux-B = Dites-le nous
|
||||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = N'a pas pu écrire sur stdout: {$err}
|
||||
rec-enc-broken-stdout = Etes-vous en train de piper vers programme qui ne lit pas depuis stdin ?
|
||||
|
||||
err-enc-broken-file = N'a pas pu écrire dans le fichier: {$err}
|
||||
|
||||
rec-enc-missing-recipients = Avez-vous oublié de spécifier {-flag-recipient} ?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} {-cantuse} {-flag-passphrase}.
|
||||
err-enc-mixed-recipient-passphrase = {-flag-recipient} {-cantuse} {-flag-passphrase}.
|
||||
err-enc-mixed-recipients-file-passphrase = {-flag-recipients-file} {-cantuse} {-flag-passphrase}.
|
||||
err-enc-passphrase-without-file = Un fichier à chiffrer doit être passé en argument lors de l'utilisation de {-flag-passphrase}.
|
||||
|
||||
err-enc-plugin-name-flag = {-flag-plugin-name} {-cantuse} {-flag-encrypt}.
|
||||
|
||||
## Decryption errors
|
||||
|
||||
err-detected-powershell-corruption = Il semblerait que ce fichier ait été corrompu par une redirection PowerShell.
|
||||
rec-detected-powershell-corruption = Essayez d'utiliser {-flag-output} ou {-flag-armor} pour chiffrer des fichiers dans PowerShell.
|
||||
|
||||
rec-dec-excessive-work = Pour déchiffrer, réessayez avec {-flag-max-work-factor} {$wf}
|
||||
|
||||
err-dec-armor-flag = {-flag-armor} {-cantuse} {-flag-decrypt}.
|
||||
rec-dec-armor-flag = Note that armored files are detected automatically.
|
||||
|
||||
err-dec-missing-identities = Identités manquantes.
|
||||
rec-dec-missing-identities = Avez-vous oublié de spécifier {-flag-identity} ?
|
||||
rec-dec-missing-identities-stdin = Avez-vous oublié de fournir une identité via l'entrée standard ?
|
||||
|
||||
err-dec-mixed-identity-passphrase = {-flag-identity} {-cantuse} des fichiers chiffrés avec une phrase secrète.
|
||||
|
||||
err-mixed-identity-and-plugin-name = {-flag-identity} {-cantuse} {-flag-plugin-name}.
|
||||
|
||||
err-dec-passphrase-flag = {-flag-passphrase} {-cantuse} {-flag-decrypt}.
|
||||
rec-dec-passphrase-flag = Notez que les fichiers chiffrés avec une phrase secrète sont détectés automatiquement.
|
||||
|
||||
err-dec-passphrase-without-file-win =
|
||||
Ce fichier requière une phrase secrète, et, sur Windows,
|
||||
le fichier à déchiffrer doit être passé en tant qu'argument
|
||||
positionnel pour déchiffrer avec une phrase secrète.
|
||||
|
||||
err-dec-recipient-flag = {-flag-recipient} {-cantuse} {-flag-decrypt}.
|
||||
err-dec-recipients-file-flag = {-flag-recipients-file} {-cantuse} {-flag-decrypt}.
|
||||
rec-dec-recipient-flag = Vouliez-vous peut-être utiliser {-flag-identity} pour spécifier une clef privée ?
|
||||
|
||||
## rage-mount strings
|
||||
|
||||
mnt-filename = FILENAME
|
||||
mnt-mountpoint = MOUNTPOINT
|
||||
mnt-types = TYPES
|
||||
|
||||
help-arg-mnt-filename = Le système de fichier chiffré à monter.
|
||||
help-arg-mnt-mountpoint = Le dossier vers lequel monter le système de fichier.
|
||||
help-arg-mnt-types = Indique le type de système de fichier (parmis {$types}).
|
||||
|
||||
info-decrypting = Déchiffrement de {$filename}
|
||||
info-mounting-as-fuse = Montage en tant que système de fichier FUSE
|
||||
|
||||
err-mnt-missing-filename = Il manque un nom de fichier.
|
||||
err-mnt-missing-mountpoint = Il manque un point de montage.
|
||||
err-mnt-missing-types = Il manque le fanion {-flag-mnt-types}.
|
||||
err-mnt-unknown-type = Type de système de fichier inconnu "{$fs_type}"
|
||||
|
||||
## Unstable features
|
||||
|
||||
test-unstable = Pour tester cela, il faut compiler {-rage} avec {-flag-unstable}.
|
||||
|
||||
## rage manpage
|
||||
|
||||
recipients = RECIPIENTS
|
||||
identities = IDENTITIES
|
||||
|
||||
man-rage-about = Un outil de chiffrement simple, sécurisé et moderne.
|
||||
|
||||
man-rage-description =
|
||||
{-rage} encrypts or decrypts {input} to {output}. The {input} argument is
|
||||
optional and defaults to standard input. Only a single {input} file may be
|
||||
specified. If {-flag-output} is not specified, {output} defaults to standard
|
||||
output.
|
||||
|
||||
If {-flag-passphrase} is specified, the file is encrypted with a passphrase
|
||||
requested interactively. Otherwise, it's encrypted to one or more
|
||||
{recipients} specified with {-flag-recipient} or
|
||||
{-flag-recipients-file}. Every recipient can decrypt the file.
|
||||
|
||||
In {-flag-decrypt} mode, passphrase-encrypted files are detected automatically
|
||||
and the passphrase is requested interactively. Otherwise, one or more
|
||||
{identities} specified with {-flag-identity} are used to decrypt the file.
|
||||
|
||||
{-age} encrypted files are binary and not malleable, with around 200 bytes of
|
||||
overhead per recipient, plus 16 bytes every 64KiB of plaintext.
|
||||
|
||||
man-rage-flag-output =
|
||||
Write encrypted or decrypted file to {output} instead of standard output.
|
||||
If {output} already exists it will be overwritten.
|
||||
|
||||
If encrypting without {-flag-armor}, {-rage} will refuse to output binary to a
|
||||
TTY. This can be forced by specifying {-stdin} as {output}.
|
||||
|
||||
man-rage-encryption-options = Encryption options
|
||||
|
||||
man-rage-flag-encrypt =
|
||||
Encrypt {input} to {output}. This is the default.
|
||||
|
||||
man-rage-flag-recipient =
|
||||
Encrypt to the explicitly specified {recipient}. See the
|
||||
{man-rage-recipients-and-identities-heading} section for possible recipient
|
||||
formats.
|
||||
|
||||
This option can be repeated and combined with other recipient flags,
|
||||
and the file can be decrypted by all provided recipients independently.
|
||||
|
||||
man-rage-flag-recipients-file =
|
||||
Encrypt to the {recipients} listed in the file at {recipients-file}, one per
|
||||
line. Empty lines and lines starting with "#" are ignored as comments.
|
||||
|
||||
If {recipients-file} is {-stdin}, the recipients are read from standard
|
||||
input. In this case, the {input} argument must be specified.
|
||||
|
||||
This option can be repeated and combined with other recipient flags,
|
||||
and the file can be decrypted by all provided recipients independently.
|
||||
|
||||
man-rage-flag-passphrase =
|
||||
Encrypt with a passphrase, requested interactively from the terminal.
|
||||
{-rage} will offer to auto-generate a secure passphrase.
|
||||
|
||||
Cette option ne peut pas être utilisée avec d'autre fanion (flag).
|
||||
|
||||
man-rage-flag-armor =
|
||||
Encrypt to an ASCII-only "armored" encoding.
|
||||
|
||||
{-age} armor is a strict version of PEM with type "{-armor-pem-type}",
|
||||
canonical "strict" Base64, no headers, and no support for leading and
|
||||
trailing extra data.
|
||||
|
||||
Decryption transparently detects and decodes ASCII armoring.
|
||||
|
||||
man-rage-flag-identity-encrypt =
|
||||
Encrypt to the {recipients} corresponding to the {identities} listed in the
|
||||
file at {identity}. This is equivalent to converting the file at {identity}
|
||||
to a recipients file with '{-rage-keygen} {-flag-convert}' and then passing that to
|
||||
{-flag-recipients-file}.
|
||||
|
||||
For the format of {identity}, see the definition of {-flag-identity} in the
|
||||
{man-rage-decryption-options} section.
|
||||
|
||||
{-flag-encrypt} must be explicitly specified when using {-flag-identity}
|
||||
in encryption mode to avoid confusion.
|
||||
|
||||
man-rage-flag-plugin-encrypt =
|
||||
Encrypt using the data-less plugin {plugin-name}.
|
||||
|
||||
This is equivalent to using {-flag-identity} with a file that contains a
|
||||
single plugin {identity} that encodes no plugin-specific data.
|
||||
|
||||
{-flag-encrypt} must be explicitly specified when using {-flag-plugin-name}
|
||||
in encryption mode to avoid confusion.
|
||||
|
||||
man-rage-decryption-options = Decryption options
|
||||
|
||||
man-rage-flag-decrypt =
|
||||
Decrypt {input} to {output}.
|
||||
|
||||
If {input} is passphrase encrypted, it will be automatically detected
|
||||
and the passphrase will be requested interactively. Otherwise, the
|
||||
{identities} specified with {-flag-identity} are used.
|
||||
|
||||
ASCII armoring is transparently detected and decoded.
|
||||
|
||||
man-rage-flag-identity-decrypt =
|
||||
Decrypt using the {identities} at {identity}.
|
||||
|
||||
{identity} may be one of the following:
|
||||
|
||||
a. A file listing {identities} one per line. Empty lines and lines starting
|
||||
with "#" are ignored as comments.
|
||||
|
||||
b. A passphrase encrypted age file, containing {identities} one per
|
||||
line like above. The passphrase is requested interactively. Note that
|
||||
passphrase-protected identity files are not necessary for most use cases,
|
||||
where access to the encrypted identity file implies access to the whole
|
||||
system.
|
||||
|
||||
c. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
|
||||
If the private key is password-protected, the password is requested
|
||||
interactively only if the SSH identity matches the file. See the
|
||||
{man-rage-ssh-keys-heading} section for more information, including
|
||||
supported key types.
|
||||
|
||||
d. {-stdin}, causing one of the options above to be read from standard input.
|
||||
In this case, the {input} argument must be specified.
|
||||
|
||||
This option can be repeated. Identities are tried in the order in which are
|
||||
provided, and the first one matching one of the file's recipients is used.
|
||||
Unused identities are ignored, but it is an error if the {input} file is
|
||||
passphrase-encrypted and {-flag-identity} is specified.
|
||||
|
||||
man-rage-flag-plugin-decrypt =
|
||||
Decrypt using the data-less plugin {plugin-name}.
|
||||
|
||||
This is equivalent to using {-flag-identity} with a file that contains a
|
||||
single plugin {identity} that encodes no plugin-specific data.
|
||||
|
||||
man-rage-recipients-and-identities-heading = RECIPIENTS AND IDENTITIES
|
||||
man-rage-recipients-and-identities =
|
||||
{recipients} are public values, like a public key, that a file can be encrypted
|
||||
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 =
|
||||
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.
|
||||
|
||||
A {recipient} encoding begins with "{-recipient-prefix}" and looks like the
|
||||
following:
|
||||
|
||||
{" "}{$example_age_recipient}
|
||||
|
||||
An {identity} encoding begins with "{-identity-prefix}" and looks like the
|
||||
following:
|
||||
|
||||
{" "}{$example_age_identity}
|
||||
|
||||
An encrypted file can't be linked to the native recipient it's encrypted to
|
||||
without access to the corresponding identity.
|
||||
|
||||
man-rage-ssh-keys-heading = SSH keys
|
||||
man-rage-ssh-keys =
|
||||
As a convenience feature, {-rage} also supports encrypting to RSA or Ed25519
|
||||
{-ssh} keys. RSA keys must be at least 2048 bits. This feature employs more
|
||||
complex cryptography, and should only be used when a native key is not available
|
||||
for the recipient. Note that SSH keys might not be protected long-term by the
|
||||
recipient, since they are revokable when used only for authentication.
|
||||
|
||||
A {recipient} encoding is an SSH public key in "{-ssh-authorized-keys}" format
|
||||
(see the "{-authorized-keys-file-format}" section of {-sshd}), starting with
|
||||
"{-ssh-rsa}" or "{-ssh-ed25519}", like the following:
|
||||
|
||||
{" "}{$example_ssh_rsa}
|
||||
{" "}{$example_ssh_ed25519}
|
||||
|
||||
The comment at the end of the line, if present, is ignored.
|
||||
|
||||
In recipient files passed to {-flag-recipients-file}, unsupported but valid
|
||||
SSH public keys are ignored with a warning, to facilitate using
|
||||
"{-ssh-authorized-keys}" or GitHub "{-dot-keys}" files. (See {man-examples-heading}.)
|
||||
|
||||
An {identity} is an SSH private key _file_ passed individually to
|
||||
{-flag-identity}. Note that keys held on hardware tokens such as YubiKeys
|
||||
or accessed via {-ssh-agent} are not supported.
|
||||
|
||||
An encrypted file _can_ be linked to the SSH public key it was encrypted to.
|
||||
This is so that {-rage} can identify the correct SSH private key before
|
||||
requesting its password, if any.
|
||||
|
||||
man-rage-plugins-heading = Plugins
|
||||
man-rage-plugins =
|
||||
{-rage} can be extended through plugins. A plugin is only loaded if a corresponding
|
||||
{recipient} or {identity} is specified. (Simply decrypting a file encrypted with
|
||||
a plugin will not cause it to load, for security reasons among others.)
|
||||
|
||||
A {recipient} for a plugin named "{-example}" starts with "{-example-r}", while an
|
||||
{identity} starts with "{-example-i}". They both encode arbitrary plugin-specific
|
||||
data, and are generated by the plugin.
|
||||
|
||||
When either is specified, {-rage} searches for {-age-plugin-}{-example} in the PATH
|
||||
and executes it to perform the file header encryption or decryption. The plugin
|
||||
may request input from the user through {-rage} to complete the operation.
|
||||
|
||||
Plugins can be freely mixed with other plugins or natively supported keys.
|
||||
|
||||
A plugin is not bound to only encrypt or decrypt files meant for or generated by
|
||||
the plugin. For example, a plugin can be used to decrypt files encrypted to a
|
||||
native X25519 {recipient} or even with a passphrase. Similarly, a plugin can
|
||||
encrypt a file such that it can be decrypted without the use of any plugin.
|
||||
|
||||
Plugins for which the {identity}/{recipient} distinction doesn't make sense
|
||||
(such as a symmetric encryption plugin) may generate only an {identity} and
|
||||
instruct the user to perform encryption with the {-flag-encrypt} and
|
||||
{-flag-identity} flags. Plugins for which the concept of separate identities
|
||||
doesn't make sense (such as a password-encryption plugin) may instruct the user
|
||||
to use the {-flag-plugin-name} flag.
|
||||
|
||||
man-examples-heading = EXAMPLES
|
||||
|
||||
man-rage-example-single = Generate a new identity, encrypt data, and decrypt
|
||||
man-rage-example-enc-multiple = Encrypt {$input} to multiple recipients and output to {$output}
|
||||
man-rage-example-enc-list = Encrypt to a list of recipients
|
||||
man-rage-example-password = Encrypt and decrypt a file using a passphrase
|
||||
man-rage-example-identity-passphrase = Encrypt and decrypt with a passphrase-protected identity file
|
||||
man-rage-example-ssh = Encrypt and decrypt with an SSH public key
|
||||
man-rage-example-yubikey = Encrypt and decrypt with {-age-plugin-}{-yubikey}
|
||||
man-rage-example-yubikey-setup = Run interactive setup, generate identity file and obtain recipient.
|
||||
man-rage-example-enc-github = Encrypt to the SSH keys of a GitHub user
|
||||
|
||||
man-see-also-heading = SEE ALSO
|
||||
|
||||
## rage-keygen manpage
|
||||
|
||||
man-keygen-about = Generate age-compatible encryption key pairs
|
||||
|
||||
man-keygen-description =
|
||||
{-rage-keygen} generates a new native {-age} key pair, and outputs the identity to
|
||||
standard output or to the {output} file. The output includes the public key and
|
||||
the current time as comments.
|
||||
|
||||
If the output is not going to a terminal, {-rage-keygen} prints the public key to
|
||||
standard error.
|
||||
|
||||
man-keygen-flag-output =
|
||||
Write the identity to {output} instead of standard output.
|
||||
|
||||
If {output} already exists, it is not overwritten.
|
||||
|
||||
man-keygen-flag-convert =
|
||||
Read an identity file from {input} or from standard input and output the
|
||||
corresponding recipient(s), one per line, with no comments.
|
||||
|
||||
man-keygen-example-stdout = Generate a new identity
|
||||
man-keygen-example-file = Write a new identity to "{$filename}"
|
||||
man-keygen-example-convert = Convert an identity to a recipient
|
||||
|
||||
## rage-mount manpage
|
||||
|
||||
man-mount-about = Mount an {-age} encrypted filesystem
|
||||
|
||||
man-mount-description =
|
||||
{-rage-mount} decrypts the {-age} encrypted filesystem at {mnt-filename} on the
|
||||
fly, and mounts it as a directory on the local filesystem at {mnt-mountpoint}.
|
||||
|
||||
Passphrase-encrypted files are detected automatically and the passphrase is
|
||||
requested interactively. Otherwise, one or more {identities} specified with
|
||||
{-flag-identity} are used to decrypt the file.
|
||||
|
||||
The previous contents (if any) and owner and mode of {mnt-mountpoint} become
|
||||
invisible, and as long as this filesystem remains mounted, the pathname
|
||||
{mnt-mountpoint} refers to the root of the filesystem on {mnt-filename}.
|
||||
|
||||
man-mount-flag-types =
|
||||
Set the filesystem type. The following types are currently supported: {$types}.
|
||||
|
||||
This option is required. {-rage-mount} does not attempt to guess the filesystem
|
||||
format.
|
||||
|
||||
In theory, any efficiently-seekable filesystem format can be supported. At
|
||||
present, {-rage-mount} only supports seekable archive formats.
|
||||
|
||||
man-mount-example-identity = Mounting an archive encrypted to a recipient
|
||||
man-mount-example-passphrase = Mounting an archive encrypted with a passphrase
|
|
@ -145,14 +145,6 @@ err-ux-B = Faccelo sapere
|
|||
# Put (len(A) - len(B) - 32) spaces here.
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Il file '{$filename}' contiene identità per '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Prova a usare '{-age-plugin-}{$plugin_name}' per convertire questa identità in destinatario.
|
||||
|
||||
err-no-identities-in-file = Nessuna identità trovata nel file '{$filename}'.
|
||||
err-no-identities-in-stdin = Nessuna identità trovata tramite standard input.
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Impossibile scrivere sullo standard output: {$err}
|
||||
|
@ -160,7 +152,6 @@ rec-enc-broken-stdout = Stai usando una pipe verso un programma che non sta legg
|
|||
|
||||
err-enc-broken-file = Impossibile scrivere sul file: {$err}
|
||||
|
||||
err-enc-missing-recipients = Destinatari mancanti.
|
||||
rec-enc-missing-recipients = Hai dimenticato di specificare {-flag-recipient}?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} non può essere usato assieme a {-flag-passphrase}.
|
||||
|
|
|
@ -147,14 +147,6 @@ err-ux-B = Сообщите нам
|
|||
# Поставьте здесь пробелы (len(A) - len(B) - 32).
|
||||
err-ux-C = {" "}
|
||||
|
||||
## Keygen errors
|
||||
|
||||
err-identity-file-contains-plugin = Файл идентификации '{$filename}' содержит идентификаторы для '{-age-plugin-}{$plugin_name}'.
|
||||
rec-identity-file-contains-plugin = Попробуйте использовать '{-age-plugin-}{$plugin_name}' для преобразования этого идентификатора в получателя.
|
||||
|
||||
err-no-identities-in-file = Идентификаторы в файле '{$filename}' не найдены.
|
||||
err-no-identities-in-stdin = Идентификаторы в стандартном вводе не найдены.
|
||||
|
||||
## Encryption errors
|
||||
|
||||
err-enc-broken-stdout = Не удалось записать в stdout: {$err}
|
||||
|
@ -162,7 +154,6 @@ rec-enc-broken-stdout = Вы передаете данные в программ
|
|||
|
||||
err-enc-broken-file = Не удалось записать в файл: {$err}
|
||||
|
||||
err-enc-missing-recipients = Отсутствуют получатели.
|
||||
rec-enc-missing-recipients = Вы забыли указать {-flag-recipient}?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} не может использоваться с {-flag-passphrase}.
|
||||
|
|
|
@ -118,7 +118,6 @@ rec-enc-broken-stdout = 您是否输出至非从 stdin 读取数据的程序?
|
|||
|
||||
err-enc-broken-file = 未能写入文件: {$err}
|
||||
|
||||
err-enc-missing-recipients = 缺少接收方。
|
||||
rec-enc-missing-recipients = 您是否忘记指定 {-flag-recipient} 标记?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} 和 {-flag-passphrase} 标记不可联用。
|
||||
|
|
|
@ -118,7 +118,6 @@ rec-enc-broken-stdout = 您是否輸出至非從 stdin 讀取數據的程序?
|
|||
|
||||
err-enc-broken-file = 未能寫入文件: {$err}
|
||||
|
||||
err-enc-missing-recipients = 缺少接收方。
|
||||
rec-enc-missing-recipients = 您是否忘記指定 {-flag-recipient} 標記?
|
||||
|
||||
err-enc-mixed-identity-passphrase = {-flag-identity} 和 {-flag-passphrase} 標記不可聯用。
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use clap::{builder::Styles, ArgAction, Parser};
|
||||
use clap::{
|
||||
builder::{Styles, ValueHint},
|
||||
ArgAction, Parser,
|
||||
};
|
||||
|
||||
use crate::fl;
|
||||
|
||||
|
@ -22,6 +25,7 @@ pub(crate) struct AgeOptions {
|
|||
#[arg(help_heading = fl!("args-header"))]
|
||||
#[arg(value_name = fl!("input"))]
|
||||
#[arg(help = fl!("help-arg-input"))]
|
||||
#[arg(value_hint = ValueHint::FilePath)]
|
||||
pub(crate) input: Option<String>,
|
||||
|
||||
#[arg(action = ArgAction::Help, short, long)]
|
||||
|
@ -35,6 +39,7 @@ pub(crate) struct AgeOptions {
|
|||
#[arg(short, long)]
|
||||
#[arg(value_name = fl!("output"))]
|
||||
#[arg(help = fl!("keygen-help-flag-output"))]
|
||||
#[arg(value_hint = ValueHint::DirPath)]
|
||||
pub(crate) output: Option<String>,
|
||||
|
||||
#[arg(short = 'y')]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use std::fmt;
|
||||
use std::io;
|
||||
|
||||
use age::IdentityFileConvertError;
|
||||
|
||||
macro_rules! wlnfl {
|
||||
($f:ident, $message_id:literal) => {
|
||||
writeln!($f, "{}", $crate::fl!($message_id))
|
||||
|
@ -16,13 +18,7 @@ pub(crate) enum Error {
|
|||
FailedToOpenOutput(io::Error),
|
||||
FailedToReadInput(io::Error),
|
||||
FailedToWriteOutput(io::Error),
|
||||
IdentityFileContainsPlugin {
|
||||
filename: Option<String>,
|
||||
plugin_name: String,
|
||||
},
|
||||
NoIdentities {
|
||||
filename: Option<String>,
|
||||
},
|
||||
IdentityFileConvert(IdentityFileConvertError),
|
||||
}
|
||||
|
||||
// Rust only supports `fn main() -> Result<(), E: Debug>`, so we implement `Debug`
|
||||
|
@ -42,28 +38,7 @@ impl fmt::Debug for Error {
|
|||
Error::FailedToWriteOutput(e) => {
|
||||
wlnfl!(f, "err-failed-to-write-output", err = e.to_string())?
|
||||
}
|
||||
Error::IdentityFileContainsPlugin {
|
||||
filename,
|
||||
plugin_name,
|
||||
} => {
|
||||
wlnfl!(
|
||||
f,
|
||||
"err-identity-file-contains-plugin",
|
||||
filename = filename.as_deref().unwrap_or_default(),
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)?;
|
||||
wlnfl!(
|
||||
f,
|
||||
"rec-identity-file-contains-plugin",
|
||||
plugin_name = plugin_name.as_str(),
|
||||
)?
|
||||
}
|
||||
Error::NoIdentities { filename } => match filename {
|
||||
Some(filename) => {
|
||||
wlnfl!(f, "err-no-identities-in-file", filename = filename.as_str())?
|
||||
}
|
||||
None => wlnfl!(f, "err-no-identities-in-stdin")?,
|
||||
},
|
||||
Error::IdentityFileConvert(e) => writeln!(f, "{e}")?,
|
||||
}
|
||||
writeln!(f)?;
|
||||
writeln!(f, "[ {} ]", crate::fl!("err-ux-A"))?;
|
||||
|
|
|
@ -73,33 +73,14 @@ fn generate(mut output: file_io::OutputWriter) -> io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn convert(
|
||||
filename: Option<String>,
|
||||
mut output: file_io::OutputWriter,
|
||||
) -> Result<(), error::Error> {
|
||||
fn convert(filename: Option<String>, output: file_io::OutputWriter) -> Result<(), error::Error> {
|
||||
let file = age::IdentityFile::from_input_reader(
|
||||
file_io::InputReader::new(filename.clone()).map_err(error::Error::FailedToOpenInput)?,
|
||||
file_io::InputReader::new(filename).map_err(error::Error::FailedToOpenInput)?,
|
||||
)
|
||||
.map_err(error::Error::FailedToReadInput)?;
|
||||
|
||||
let identities = file.into_identities();
|
||||
if identities.is_empty() {
|
||||
return Err(error::Error::NoIdentities { filename });
|
||||
}
|
||||
|
||||
for identity in identities {
|
||||
match identity {
|
||||
age::IdentityFileEntry::Native(sk) => {
|
||||
writeln!(output, "{}", sk.to_public()).map_err(error::Error::FailedToWriteOutput)?
|
||||
}
|
||||
age::IdentityFileEntry::Plugin(id) => {
|
||||
return Err(error::Error::IdentityFileContainsPlugin {
|
||||
filename,
|
||||
plugin_name: id.plugin().to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
file.write_recipients_file(output)
|
||||
.map_err(error::Error::IdentityFileConvert)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use clap::{builder::Styles, ArgAction, Parser};
|
||||
use clap::{
|
||||
builder::{Styles, ValueHint},
|
||||
ArgAction, Parser,
|
||||
};
|
||||
|
||||
use crate::fl;
|
||||
|
||||
|
@ -24,11 +27,13 @@ pub(crate) struct AgeMountOptions {
|
|||
#[arg(help_heading = fl!("args-header"))]
|
||||
#[arg(value_name = fl!("mnt-filename"))]
|
||||
#[arg(help = fl!("help-arg-mnt-filename"))]
|
||||
#[arg(value_hint = ValueHint::FilePath)]
|
||||
pub(crate) filename: String,
|
||||
|
||||
#[arg(help_heading = fl!("args-header"))]
|
||||
#[arg(value_name = fl!("mnt-mountpoint"))]
|
||||
#[arg(help = fl!("help-arg-mnt-mountpoint"))]
|
||||
#[arg(value_hint = ValueHint::DirPath)]
|
||||
pub(crate) mountpoint: String,
|
||||
|
||||
#[arg(action = ArgAction::Help, short, long)]
|
||||
|
@ -51,5 +56,6 @@ pub(crate) struct AgeMountOptions {
|
|||
#[arg(short, long)]
|
||||
#[arg(value_name = fl!("identity"))]
|
||||
#[arg(help = fl!("help-flag-identity"))]
|
||||
#[arg(value_hint = ValueHint::FilePath)]
|
||||
pub(crate) identity: Vec<String>,
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
use age::{
|
||||
armor::ArmoredReader,
|
||||
cli_common::{read_identities, read_secret, StdinGuard},
|
||||
scrypt,
|
||||
stream::StreamReader,
|
||||
};
|
||||
use clap::{CommandFactory, Parser};
|
||||
|
@ -209,28 +210,33 @@ fn main() -> Result<(), Error> {
|
|||
|
||||
let mut stdin_guard = StdinGuard::new(false);
|
||||
|
||||
match age::Decryptor::new_buffered(ArmoredReader::new(file))? {
|
||||
age::Decryptor::Passphrase(decryptor) => {
|
||||
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
|
||||
Ok(passphrase) => decryptor
|
||||
.decrypt(&passphrase, opts.max_work_factor)
|
||||
let decryptor = age::Decryptor::new_buffered(ArmoredReader::new(file))?;
|
||||
|
||||
if decryptor.is_scrypt() {
|
||||
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
|
||||
Ok(passphrase) => {
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
if let Some(max_work_factor) = opts.max_work_factor {
|
||||
identity.set_max_work_factor(max_work_factor);
|
||||
}
|
||||
|
||||
decryptor
|
||||
.decrypt(Some(&identity as _).into_iter())
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|stream| mount_stream(stream, types, mountpoint)),
|
||||
Err(_) => Ok(()),
|
||||
.and_then(|stream| mount_stream(stream, types, mountpoint))
|
||||
}
|
||||
Err(_) => Ok(()),
|
||||
}
|
||||
age::Decryptor::Recipients(decryptor) => {
|
||||
let identities =
|
||||
read_identities(opts.identity, opts.max_work_factor, &mut stdin_guard)?;
|
||||
} else {
|
||||
let identities = read_identities(opts.identity, opts.max_work_factor, &mut stdin_guard)?;
|
||||
|
||||
if identities.is_empty() {
|
||||
return Err(Error::MissingIdentities);
|
||||
}
|
||||
|
||||
decryptor
|
||||
.decrypt(identities.iter().map(|i| &**i))
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|stream| mount_stream(stream, types, mountpoint))
|
||||
if identities.is_empty() {
|
||||
return Err(Error::MissingIdentities);
|
||||
}
|
||||
|
||||
decryptor
|
||||
.decrypt(identities.iter().map(|i| &**i))
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|stream| mount_stream(stream, types, mountpoint))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use std::path::Path;
|
||||
|
||||
use clap::{builder::Styles, ArgAction, Parser};
|
||||
use clap::{
|
||||
builder::{Styles, ValueHint},
|
||||
ArgAction, Parser,
|
||||
};
|
||||
|
||||
use crate::fl;
|
||||
|
||||
|
@ -99,6 +102,7 @@ pub(crate) struct AgeOptions {
|
|||
#[arg(help_heading = fl!("args-header"))]
|
||||
#[arg(value_name = fl!("input"))]
|
||||
#[arg(help = fl!("help-arg-input"))]
|
||||
#[arg(value_hint = ValueHint::FilePath)]
|
||||
pub(crate) input: Option<String>,
|
||||
|
||||
#[arg(action = ArgAction::Help, short, long)]
|
||||
|
@ -137,11 +141,13 @@ pub(crate) struct AgeOptions {
|
|||
#[arg(short = 'R', long)]
|
||||
#[arg(value_name = fl!("recipients-file"))]
|
||||
#[arg(help = fl!("help-flag-recipients-file"))]
|
||||
#[arg(value_hint = ValueHint::FilePath)]
|
||||
pub(crate) recipients_file: Vec<String>,
|
||||
|
||||
#[arg(short, long)]
|
||||
#[arg(value_name = fl!("identity"))]
|
||||
#[arg(help = fl!("help-flag-identity"))]
|
||||
#[arg(value_hint = ValueHint::FilePath)]
|
||||
pub(crate) identity: Vec<String>,
|
||||
|
||||
#[arg(short = 'j')]
|
||||
|
@ -152,5 +158,6 @@ pub(crate) struct AgeOptions {
|
|||
#[arg(short, long)]
|
||||
#[arg(value_name = fl!("output"))]
|
||||
#[arg(help = fl!("help-flag-output"))]
|
||||
#[arg(value_hint = ValueHint::AnyPath)]
|
||||
pub(crate) output: Option<String>,
|
||||
}
|
||||
|
|
|
@ -23,14 +23,17 @@ macro_rules! wlnfl {
|
|||
|
||||
pub(crate) enum EncryptError {
|
||||
Age(age::EncryptError),
|
||||
BrokenPipe { is_stdout: bool, source: io::Error },
|
||||
BrokenPipe {
|
||||
is_stdout: bool,
|
||||
source: io::Error,
|
||||
},
|
||||
IdentityRead(age::cli_common::ReadError),
|
||||
Io(io::Error),
|
||||
MissingRecipients,
|
||||
MixedIdentityAndPassphrase,
|
||||
MixedRecipientAndPassphrase,
|
||||
MixedRecipientsFileAndPassphrase,
|
||||
PassphraseTimedOut,
|
||||
#[cfg(not(unix))]
|
||||
PassphraseWithoutFileArgument,
|
||||
PluginNameFlag,
|
||||
}
|
||||
|
@ -59,6 +62,10 @@ impl From<io::Error> for EncryptError {
|
|||
impl fmt::Display for EncryptError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
EncryptError::Age(e @ age::EncryptError::MissingRecipients) => {
|
||||
writeln!(f, "{}", e)?;
|
||||
wfl!(f, "rec-enc-missing-recipients")
|
||||
}
|
||||
EncryptError::Age(e) => write!(f, "{}", e),
|
||||
EncryptError::BrokenPipe { is_stdout, source } => {
|
||||
if *is_stdout {
|
||||
|
@ -70,10 +77,6 @@ impl fmt::Display for EncryptError {
|
|||
}
|
||||
EncryptError::IdentityRead(e) => write!(f, "{}", e),
|
||||
EncryptError::Io(e) => write!(f, "{}", e),
|
||||
EncryptError::MissingRecipients => {
|
||||
wlnfl!(f, "err-enc-missing-recipients")?;
|
||||
wfl!(f, "rec-enc-missing-recipients")
|
||||
}
|
||||
EncryptError::MixedIdentityAndPassphrase => {
|
||||
wfl!(f, "err-enc-mixed-identity-passphrase")
|
||||
}
|
||||
|
@ -84,6 +87,7 @@ impl fmt::Display for EncryptError {
|
|||
wfl!(f, "err-enc-mixed-recipients-file-passphrase")
|
||||
}
|
||||
EncryptError::PassphraseTimedOut => wfl!(f, "err-passphrase-timed-out"),
|
||||
#[cfg(not(unix))]
|
||||
EncryptError::PassphraseWithoutFileArgument => {
|
||||
wfl!(f, "err-enc-passphrase-without-file")
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ lazy_static! {
|
|||
}
|
||||
|
||||
/// Selects the most suitable available language in order of preference by
|
||||
/// `requested_languages`, and loads it using the `rage` [`LANGUAGE_LOADER`] from the
|
||||
/// `requested_languages`, and loads it using the `rage` [`static@LANGUAGE_LOADER`] from the
|
||||
/// languages available in `rage/i18n/`.
|
||||
///
|
||||
/// Returns the available languages that were negotiated as being the most suitable to be
|
||||
|
|
|
@ -6,7 +6,7 @@ use age::{
|
|||
file_io, read_identities, read_or_generate_passphrase, read_recipients, read_secret,
|
||||
Passphrase, StdinGuard, UiCallbacks,
|
||||
},
|
||||
plugin,
|
||||
plugin, scrypt,
|
||||
secrecy::ExposeSecret,
|
||||
Identity,
|
||||
};
|
||||
|
@ -108,6 +108,7 @@ fn encrypt(opts: AgeOptions) -> Result<(), error::EncryptError> {
|
|||
(Format::Binary, file_io::OutputFormat::Binary)
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
let has_file_argument = opts.input.is_some();
|
||||
|
||||
let (input, output) = set_up_io(opts.input, opts.output, output_format)?;
|
||||
|
@ -134,8 +135,13 @@ fn encrypt(opts: AgeOptions) -> Result<(), error::EncryptError> {
|
|||
return Err(error::EncryptError::MixedRecipientsFileAndPassphrase);
|
||||
}
|
||||
|
||||
if !has_file_argument {
|
||||
return Err(error::EncryptError::PassphraseWithoutFileArgument);
|
||||
// The `rpassword` crate opens `/dev/tty` directly on Unix, so we don't have
|
||||
// any conflict with stdin.
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if !has_file_argument {
|
||||
return Err(error::EncryptError::PassphraseWithoutFileArgument);
|
||||
}
|
||||
}
|
||||
|
||||
match read_or_generate_passphrase() {
|
||||
|
@ -166,19 +172,20 @@ fn encrypt(opts: AgeOptions) -> Result<(), error::EncryptError> {
|
|||
} else {
|
||||
if opts.recipient.is_empty() && opts.recipients_file.is_empty() && opts.identity.is_empty()
|
||||
{
|
||||
return Err(error::EncryptError::MissingRecipients);
|
||||
return Err(error::EncryptError::Age(
|
||||
age::EncryptError::MissingRecipients,
|
||||
));
|
||||
}
|
||||
|
||||
match age::Encryptor::with_recipients(read_recipients(
|
||||
let recipients = read_recipients(
|
||||
opts.recipient,
|
||||
opts.recipients_file,
|
||||
opts.identity,
|
||||
opts.max_work_factor,
|
||||
&mut stdin_guard,
|
||||
)?) {
|
||||
Some(encryptor) => encryptor,
|
||||
None => return Err(error::EncryptError::MissingRecipients),
|
||||
}
|
||||
)?;
|
||||
|
||||
age::Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref() as _))?
|
||||
};
|
||||
|
||||
let mut output = encryptor.wrap_output(ArmoredWriter::wrap_output(output, format)?)?;
|
||||
|
@ -305,55 +312,61 @@ fn decrypt(opts: AgeOptions) -> Result<(), error::DecryptError> {
|
|||
],
|
||||
);
|
||||
|
||||
match age::Decryptor::new_buffered(ArmoredReader::new(input))? {
|
||||
age::Decryptor::Passphrase(decryptor) => {
|
||||
if identities_were_provided {
|
||||
return Err(error::DecryptError::MixedIdentityAndPassphrase);
|
||||
}
|
||||
let decryptor = age::Decryptor::new_buffered(ArmoredReader::new(input))?;
|
||||
|
||||
// The `rpassword` crate opens `/dev/tty` directly on Unix, so we don't have
|
||||
// any conflict with stdin.
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if !has_file_argument {
|
||||
return Err(error::DecryptError::PassphraseWithoutFileArgument);
|
||||
if decryptor.is_scrypt() {
|
||||
if identities_were_provided {
|
||||
return Err(error::DecryptError::MixedIdentityAndPassphrase);
|
||||
}
|
||||
|
||||
// The `rpassword` crate opens `/dev/tty` directly on Unix, so we don't have
|
||||
// any conflict with stdin.
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
if !has_file_argument {
|
||||
return Err(error::DecryptError::PassphraseWithoutFileArgument);
|
||||
}
|
||||
}
|
||||
|
||||
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
|
||||
Ok(passphrase) => {
|
||||
let mut identity = scrypt::Identity::new(passphrase);
|
||||
if let Some(max_work_factor) = opts.max_work_factor {
|
||||
identity.set_max_work_factor(max_work_factor);
|
||||
}
|
||||
}
|
||||
|
||||
match read_secret(&fl!("type-passphrase"), &fl!("prompt-passphrase"), None) {
|
||||
Ok(passphrase) => decryptor
|
||||
.decrypt(&passphrase, opts.max_work_factor)
|
||||
decryptor
|
||||
.decrypt(Some(&identity as _).into_iter())
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|input| write_output(input, output)),
|
||||
Err(pinentry::Error::Cancelled) => Ok(()),
|
||||
Err(pinentry::Error::Timeout) => Err(error::DecryptError::PassphraseTimedOut),
|
||||
Err(pinentry::Error::Encoding(e)) => {
|
||||
// Pretend it is an I/O error
|
||||
Err(error::DecryptError::Io(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
e,
|
||||
)))
|
||||
}
|
||||
Err(pinentry::Error::Gpg(e)) => {
|
||||
// Pretend it is an I/O error
|
||||
Err(error::DecryptError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("{}", e),
|
||||
)))
|
||||
}
|
||||
Err(pinentry::Error::Io(e)) => Err(error::DecryptError::Io(e)),
|
||||
.and_then(|input| write_output(input, output))
|
||||
}
|
||||
Err(pinentry::Error::Cancelled) => Ok(()),
|
||||
Err(pinentry::Error::Timeout) => Err(error::DecryptError::PassphraseTimedOut),
|
||||
Err(pinentry::Error::Encoding(e)) => {
|
||||
// Pretend it is an I/O error
|
||||
Err(error::DecryptError::Io(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
e,
|
||||
)))
|
||||
}
|
||||
Err(pinentry::Error::Gpg(e)) => {
|
||||
// Pretend it is an I/O error
|
||||
Err(error::DecryptError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("{}", e),
|
||||
)))
|
||||
}
|
||||
Err(pinentry::Error::Io(e)) => Err(error::DecryptError::Io(e)),
|
||||
}
|
||||
} else {
|
||||
if identities.is_empty() {
|
||||
return Err(error::DecryptError::MissingIdentities { stdin_identity });
|
||||
}
|
||||
age::Decryptor::Recipients(decryptor) => {
|
||||
if identities.is_empty() {
|
||||
return Err(error::DecryptError::MissingIdentities { stdin_identity });
|
||||
}
|
||||
|
||||
decryptor
|
||||
.decrypt(identities.iter().map(|i| i.as_ref() as &dyn Identity))
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|input| write_output(input, output))
|
||||
}
|
||||
decryptor
|
||||
.decrypt(identities.iter().map(|i| i.as_ref() as &dyn Identity))
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|input| write_output(input, output))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
#[test]
|
||||
fn cli_tests() {
|
||||
trycmd::TestCases::new().case("tests/cmd/*/*.toml");
|
||||
let tests = trycmd::TestCases::new();
|
||||
|
||||
tests.case("tests/cmd/*/*.toml");
|
||||
|
||||
#[cfg(unix)]
|
||||
tests.case("tests/unix/*/*.toml");
|
||||
|
||||
#[cfg(not(unix))]
|
||||
tests.case("tests/windows/*/*.toml");
|
||||
}
|
||||
|
|
10
rage/tests/cmd/rage-keygen/gen-output-invalid-filename.toml
Normal file
10
rage/tests/cmd/rage-keygen/gen-output-invalid-filename.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
bin.name = "rage-keygen"
|
||||
args = "-o ''"
|
||||
status = "failed"
|
||||
stdout = ""
|
||||
stderr = """
|
||||
Error: Failed to open output: invalid filename ''.
|
||||
|
||||
[ Did rage not do what you expected? Could an error be more useful? ]
|
||||
[ Tell us: https://str4d.xyz/rage/report ]
|
||||
"""
|
10
rage/tests/cmd/rage-keygen/gen-output-missing-directory.toml
Normal file
10
rage/tests/cmd/rage-keygen/gen-output-missing-directory.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
bin.name = "rage-keygen"
|
||||
args = "-o does-not-exist/key.txt"
|
||||
status = "failed"
|
||||
stdout = ""
|
||||
stderr = """
|
||||
Error: Failed to open output: directory 'does-not-exist' does not exist.
|
||||
|
||||
[ Did rage not do what you expected? Could an error be more useful? ]
|
||||
[ Tell us: https://str4d.xyz/rage/report ]
|
||||
"""
|
|
@ -1,6 +1,6 @@
|
|||
bin.name = "rage-keygen"
|
||||
args = "--version"
|
||||
stdout = """
|
||||
rage-keygen 0.10.1
|
||||
rage-keygen 0.11.1
|
||||
"""
|
||||
stderr = ""
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
bin.name = "rage-mount"
|
||||
args = "--version"
|
||||
stdout = """
|
||||
rage-mount 0.10.1
|
||||
rage-mount 0.11.1
|
||||
"""
|
||||
stderr = ""
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHUGc3Zlhpekp0K012aXdu
|
||||
T1VZN0lmWlRmNjdLYVB4RldkTFVLTkNDUXlBCmJjRUcrM3E0a0U0N3IyK1JsTitG
|
||||
dHVTd0N6TVFRTWgzdG5uSzJmNm9YMTgKLT4gQXQ1WWAtZ3JlYXNlIDxodGFSVHJg
|
||||
IFg0cWYsO0ogZ2Fzc1EKZGtPSTB3Ci0tLSBKazRIaHJxdnNJcHpyclRkQjg3QW5r
|
||||
SVE2MHdtWkErYTNrNWJibWd1bmNBCkK9FoOkiLB93gD79vNed8L3LM9rhKm5qma2
|
||||
lSiwRx/aM1DKaZO0CMmYQkoM2tPReA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
|
@ -0,0 +1,7 @@
|
|||
bin.name = "rage"
|
||||
args = "-d -i - file.age.txt"
|
||||
stdin = "AGE-SECRET-KEY-1SRQGS50G584HFA5JG9D6D9S6639VVHJUE5XHHKJET9DRU76HK4RQP0X5Q3"
|
||||
stdout = """
|
||||
Test plaintext.
|
||||
"""
|
||||
stderr = ""
|
|
@ -1,6 +1,6 @@
|
|||
bin.name = "rage"
|
||||
args = "--version"
|
||||
stdout = """
|
||||
rage 0.10.1
|
||||
rage 0.11.1
|
||||
"""
|
||||
stderr = ""
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
bin.name = "rage"
|
||||
args = "-p"
|
||||
status = "failed"
|
||||
stdin = ""
|
||||
stdout = ""
|
||||
stderr = """
|
||||
Error: Parsing Error: Error { input: "", code: Tag }
|
||||
|
||||
[ Did rage not do what you expected? Could an error be more useful? ]
|
||||
[ Tell us: https://str4d.xyz/rage/report ]
|
||||
"""
|
||||
|
||||
# We get an error from the `pinentry` crate because we've passed a real but invalid binary
|
||||
# that does not speak the pinentry protocol.
|
||||
[env.add]
|
||||
PINENTRY_PROGRAM = "true"
|
|
@ -6,6 +6,12 @@ description = "The cryptographic code in this crate has been reviewed for correc
|
|||
|
||||
[audits]
|
||||
|
||||
[[trusted.pinentry]]
|
||||
criteria = "safe-to-deploy"
|
||||
user-id = 6289 # Jack Grigg (str4d)
|
||||
start = "2020-01-12"
|
||||
end = "2025-11-03"
|
||||
|
||||
[[trusted.windows-sys]]
|
||||
criteria = "safe-to-deploy"
|
||||
user-id = 64539 # Kenny Kerr (kennykerr)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# cargo-vet config file
|
||||
|
||||
[cargo-vet]
|
||||
version = "0.9"
|
||||
version = "0.10"
|
||||
|
||||
[imports.bytecode-alliance]
|
||||
url = "https://raw.githubusercontent.com/bytecodealliance/wasmtime/main/supply-chain/audits.toml"
|
||||
|
@ -53,10 +53,6 @@ criteria = "safe-to-deploy"
|
|||
version = "0.10.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.ahash]]
|
||||
version = "0.8.6"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.aho-corasick]]
|
||||
version = "1.1.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -86,13 +82,21 @@ version = "1.0.2"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.arc-swap]]
|
||||
version = "1.6.0"
|
||||
version = "1.7.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.backtrace]]
|
||||
version = "0.3.73"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.base64ct]]
|
||||
version = "1.6.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.basic-toml]]
|
||||
version = "0.1.9"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.bcrypt-pbkdf]]
|
||||
version = "0.10.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -101,10 +105,6 @@ criteria = "safe-to-deploy"
|
|||
version = "0.9.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.bitflags]]
|
||||
version = "1.3.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.block]]
|
||||
version = "0.1.6"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -117,14 +117,6 @@ criteria = "safe-to-deploy"
|
|||
version = "0.9.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.bytemuck]]
|
||||
version = "1.14.1"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.byteorder]]
|
||||
version = "1.4.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.bzip2]]
|
||||
version = "0.4.4"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -133,14 +125,14 @@ criteria = "safe-to-deploy"
|
|||
version = "0.1.11+1.0.8"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.cast]]
|
||||
version = "0.3.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.cbc]]
|
||||
version = "0.1.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.cc]]
|
||||
version = "1.1.34"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.chacha20]]
|
||||
version = "0.9.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -150,7 +142,7 @@ version = "0.10.1"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.chrono]]
|
||||
version = "0.4.33"
|
||||
version = "0.4.38"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.ciborium]]
|
||||
|
@ -165,10 +157,6 @@ criteria = "safe-to-run"
|
|||
version = "0.2.2"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.cipher]]
|
||||
version = "0.3.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.clap]]
|
||||
version = "4.3.24"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -193,6 +181,10 @@ criteria = "safe-to-deploy"
|
|||
version = "0.2.12"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.colorchoice]]
|
||||
version = "1.0.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.console]]
|
||||
version = "0.15.8"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -210,27 +202,19 @@ version = "0.2.4"
|
|||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.cookie-factory]]
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.cpp_demangle]]
|
||||
version = "0.4.3"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.cpufeatures]]
|
||||
version = "0.2.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.crc32fast]]
|
||||
version = "1.3.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.criterion]]
|
||||
version = "0.3.6"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.criterion-cycles-per-byte]]
|
||||
version = "0.6.0"
|
||||
version = "0.6.1"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.criterion-plot]]
|
||||
|
@ -258,7 +242,7 @@ version = "0.1.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.dashmap]]
|
||||
version = "5.5.3"
|
||||
version = "6.1.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.der]]
|
||||
|
@ -269,8 +253,12 @@ criteria = "safe-to-deploy"
|
|||
version = "0.9.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.displaydoc]]
|
||||
version = "0.2.5"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.dunce]]
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.encode_unicode]]
|
||||
|
@ -282,7 +270,7 @@ version = "0.10.2"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.filetime]]
|
||||
version = "0.2.23"
|
||||
version = "0.2.25"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.find-crate]]
|
||||
|
@ -293,8 +281,16 @@ criteria = "safe-to-deploy"
|
|||
version = "0.10.2"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.flate2]]
|
||||
version = "1.0.28"
|
||||
[[exemptions.fluent]]
|
||||
version = "0.16.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.fluent-bundle]]
|
||||
version = "0.15.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.fluent-syntax]]
|
||||
version = "0.11.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.fuse_mt]]
|
||||
|
@ -305,18 +301,6 @@ criteria = "safe-to-deploy"
|
|||
version = "0.13.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.futures]]
|
||||
version = "0.3.30"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.futures-executor]]
|
||||
version = "0.3.30"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.futures-io]]
|
||||
version = "0.3.30"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.futures-macro]]
|
||||
version = "0.3.30"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -345,20 +329,24 @@ criteria = "safe-to-deploy"
|
|||
version = "0.2.10"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.gimli]]
|
||||
version = "0.28.1"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.half]]
|
||||
version = "2.2.1"
|
||||
criteria = "safe-to-run"
|
||||
[[exemptions.ghash]]
|
||||
version = "0.5.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.hashbrown]]
|
||||
version = "0.14.3"
|
||||
version = "0.14.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.hashbrown]]
|
||||
version = "0.15.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.hermit-abi]]
|
||||
version = "0.3.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.hermit-abi]]
|
||||
version = "0.3.4"
|
||||
version = "0.4.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.hkdf]]
|
||||
|
@ -378,39 +366,47 @@ version = "1.1.1"
|
|||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.i18n-config]]
|
||||
version = "0.4.5"
|
||||
version = "0.4.7"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.i18n-embed]]
|
||||
version = "0.14.1"
|
||||
version = "0.15.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.i18n-embed-fl]]
|
||||
version = "0.7.0"
|
||||
version = "0.9.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.i18n-embed-impl]]
|
||||
version = "0.8.3"
|
||||
version = "0.8.4"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.iana-time-zone]]
|
||||
version = "0.1.61"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.indexmap]]
|
||||
version = "2.0.0"
|
||||
criteria = "safe-to-deploy"
|
||||
version = "2.6.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.inferno]]
|
||||
version = "0.11.19"
|
||||
version = "0.11.17"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.intl-memoizer]]
|
||||
version = "0.5.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.io_tee]]
|
||||
version = "0.1.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.is-terminal]]
|
||||
version = "0.4.10"
|
||||
version = "0.4.13"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.jobserver]]
|
||||
version = "0.1.26"
|
||||
version = "0.1.24"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.js-sys]]
|
||||
|
@ -418,15 +414,19 @@ version = "0.3.60"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.libc]]
|
||||
version = "0.2.153"
|
||||
version = "0.2.158"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.libm]]
|
||||
version = "0.2.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.libredox]]
|
||||
version = "0.0.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.linux-raw-sys]]
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.locale_config]]
|
||||
|
@ -434,17 +434,13 @@ version = "0.3.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.lock_api]]
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.memchr]]
|
||||
version = "2.6.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.memmap2]]
|
||||
version = "0.9.4"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.minimal-lexical]]
|
||||
version = "0.2.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -453,10 +449,6 @@ criteria = "safe-to-deploy"
|
|||
version = "0.26.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.nom]]
|
||||
version = "7.1.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.num-bigint-dig]]
|
||||
version = "0.8.4"
|
||||
criteria = "safe-to-deploy"
|
||||
|
@ -482,7 +474,7 @@ version = "0.1.1"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.object]]
|
||||
version = "0.32.2"
|
||||
version = "0.36.5"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.once_cell]]
|
||||
|
@ -490,7 +482,7 @@ version = "1.15.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.os_pipe]]
|
||||
version = "1.1.5"
|
||||
version = "1.2.1"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.page_size]]
|
||||
|
@ -498,11 +490,11 @@ version = "0.5.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.parking_lot]]
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.parking_lot_core]]
|
||||
version = "0.9.9"
|
||||
version = "0.9.10"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.password-hash]]
|
||||
|
@ -518,15 +510,11 @@ version = "0.12.2"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.pin-project]]
|
||||
version = "1.1.4"
|
||||
version = "1.1.5"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.pin-project-internal]]
|
||||
version = "1.1.4"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.pinentry]]
|
||||
version = "0.5.0"
|
||||
version = "1.1.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.pkcs1]]
|
||||
|
@ -537,20 +525,16 @@ criteria = "safe-to-deploy"
|
|||
version = "0.10.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.pkg-config]]
|
||||
version = "0.3.29"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.plotters]]
|
||||
version = "0.3.5"
|
||||
version = "0.3.7"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.plotters-backend]]
|
||||
version = "0.3.5"
|
||||
version = "0.3.7"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.plotters-svg]]
|
||||
version = "0.3.5"
|
||||
version = "0.3.6"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.poly1305]]
|
||||
|
@ -558,7 +542,7 @@ version = "0.8.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.polyval]]
|
||||
version = "0.6.1"
|
||||
version = "0.6.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.pprof]]
|
||||
|
@ -566,15 +550,19 @@ version = "0.13.0"
|
|||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.ppv-lite86]]
|
||||
version = "0.2.16"
|
||||
version = "0.2.20"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.proc-macro-error]]
|
||||
version = "1.0.4"
|
||||
[[exemptions.proc-macro-error-attr2]]
|
||||
version = "2.0.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.proc-macro-error2]]
|
||||
version = "2.0.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.proptest]]
|
||||
version = "1.2.0"
|
||||
version = "1.5.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.quick-error]]
|
||||
|
@ -590,7 +578,7 @@ version = "0.8.5"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.redox_syscall]]
|
||||
version = "0.4.1"
|
||||
version = "0.5.7"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.regex]]
|
||||
|
@ -606,7 +594,7 @@ version = "0.7.2"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.rgb]]
|
||||
version = "0.8.37"
|
||||
version = "0.8.50"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.roff]]
|
||||
|
@ -626,19 +614,19 @@ version = "0.0.2"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.rust-embed]]
|
||||
version = "8.2.0"
|
||||
version = "8.3.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.rust-embed-impl]]
|
||||
version = "8.2.0"
|
||||
version = "8.3.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.rust-embed-utils]]
|
||||
version = "8.2.0"
|
||||
version = "8.3.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.rustix]]
|
||||
version = "0.38.31"
|
||||
version = "0.38.34"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.rusty-fork]]
|
||||
|
@ -666,7 +654,7 @@ version = "0.11.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.secrecy]]
|
||||
version = "0.8.0"
|
||||
version = "0.10.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.self_cell]]
|
||||
|
@ -674,35 +662,23 @@ version = "0.10.3"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.self_cell]]
|
||||
version = "1.0.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.semver]]
|
||||
version = "1.0.21"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.serde]]
|
||||
version = "1.0.136"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.serde_derive]]
|
||||
version = "1.0.136"
|
||||
version = "1.0.4"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.serde_spanned]]
|
||||
version = "0.6.3"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.sha1]]
|
||||
version = "0.10.6"
|
||||
criteria = "safe-to-deploy"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.sha2]]
|
||||
version = "0.10.8"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.shlex]]
|
||||
version = "1.3.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.similar]]
|
||||
version = "2.4.0"
|
||||
version = "2.6.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.slab]]
|
||||
|
@ -710,7 +686,7 @@ version = "0.4.9"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.smallvec]]
|
||||
version = "1.13.1"
|
||||
version = "1.11.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.snapbox]]
|
||||
|
@ -722,7 +698,7 @@ version = "0.3.4"
|
|||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.spin]]
|
||||
version = "0.5.2"
|
||||
version = "0.9.8"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.spki]]
|
||||
|
@ -733,24 +709,20 @@ criteria = "safe-to-deploy"
|
|||
version = "0.1.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.strsim]]
|
||||
version = "0.10.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.symbolic-common]]
|
||||
version = "12.8.0"
|
||||
version = "12.12.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.symbolic-demangle]]
|
||||
version = "12.8.0"
|
||||
version = "12.12.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.syn]]
|
||||
version = "1.0.102"
|
||||
version = "2.0.87"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.tar]]
|
||||
version = "0.4.40"
|
||||
version = "0.4.43"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.tempfile]]
|
||||
|
@ -781,36 +753,28 @@ criteria = "safe-to-deploy"
|
|||
version = "0.1.44"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.tinytemplate]]
|
||||
version = "1.2.1"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.tokio]]
|
||||
version = "1.35.0"
|
||||
version = "1.38.1"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.tokio-macros]]
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.toml]]
|
||||
version = "0.5.9"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.toml]]
|
||||
version = "0.7.6"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.toml_edit]]
|
||||
version = "0.19.14"
|
||||
criteria = "safe-to-deploy"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.trycmd]]
|
||||
version = "0.14.16"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.type-map]]
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.typenum]]
|
||||
|
@ -821,24 +785,20 @@ criteria = "safe-to-deploy"
|
|||
version = "0.1.4"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.unic-langid]]
|
||||
version = "0.9.4"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.unic-langid-impl]]
|
||||
version = "0.9.4"
|
||||
[[exemptions.utf8parse]]
|
||||
version = "0.2.2"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.uuid]]
|
||||
version = "1.7.0"
|
||||
version = "1.11.0"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.wait-timeout]]
|
||||
version = "0.2.0"
|
||||
criteria = "safe-to-run"
|
||||
[[exemptions.version_check]]
|
||||
version = "0.9.5"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.walkdir]]
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.wasi]]
|
||||
|
@ -846,23 +806,19 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.wasm-bindgen]]
|
||||
version = "0.2.89"
|
||||
version = "0.2.92"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.wasm-bindgen-backend]]
|
||||
version = "0.2.89"
|
||||
version = "0.2.88"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.wasm-bindgen-macro]]
|
||||
version = "0.2.87"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.wasm-bindgen-macro-support]]
|
||||
version = "0.2.87"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.web-sys]]
|
||||
version = "0.3.66"
|
||||
version = "0.3.65"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.which]]
|
||||
|
@ -878,7 +834,7 @@ version = "0.4.0"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.winapi-util]]
|
||||
version = "0.1.6"
|
||||
version = "0.1.9"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.winapi-x86_64-pc-windows-gnu]]
|
||||
|
@ -889,32 +845,40 @@ criteria = "safe-to-deploy"
|
|||
version = "0.52.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.winnow]]
|
||||
version = "0.5.37"
|
||||
[[exemptions.windows_i686_gnullvm]]
|
||||
version = "0.52.6"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.winnow]]
|
||||
version = "0.5.40"
|
||||
criteria = "safe-to-run"
|
||||
|
||||
[[exemptions.wsl]]
|
||||
version = "0.1.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.x25519-dalek]]
|
||||
version = "2.0.0"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.xattr]]
|
||||
version = "1.3.1"
|
||||
version = "2.0.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zerocopy]]
|
||||
version = "0.6.6"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zerocopy]]
|
||||
version = "0.7.35"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zerocopy-derive]]
|
||||
version = "0.6.6"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zerocopy-derive]]
|
||||
version = "0.7.35"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zeroize]]
|
||||
version = "1.7.0"
|
||||
version = "1.8.1"
|
||||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zeroize_derive]]
|
||||
|
@ -934,5 +898,5 @@ version = "5.0.2+zstd.1.5.2"
|
|||
criteria = "safe-to-deploy"
|
||||
|
||||
[[exemptions.zstd-sys]]
|
||||
version = "2.0.9+zstd.1.5.5"
|
||||
version = "2.0.13+zstd.1.5.6"
|
||||
criteria = "safe-to-deploy"
|
||||
|
|
File diff suppressed because it is too large
Load diff
3
tap_migrations.json
Normal file
3
tap_migrations.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"rage": "homebrew/core"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue