Skip to content

Commit

Permalink
Migrate to secrecy 0.10
Browse files Browse the repository at this point in the history
  • Loading branch information
str4d committed Nov 3, 2024
1 parent a59f047 commit 93fa28a
Show file tree
Hide file tree
Showing 27 changed files with 155 additions and 93 deletions.
10 changes: 5 additions & 5 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ 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"

Expand Down
7 changes: 6 additions & 1 deletion age-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ to 1.0.0 are beta releases.

## [Unreleased]
### Added
- `age_core::format::is_arbitrary_string`
- `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.

Expand Down
25 changes: 20 additions & 5 deletions age-core/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "-> ";
Expand All @@ -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))
}
}

Expand Down
2 changes: 1 addition & 1 deletion age-core/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
11 changes: 8 additions & 3 deletions age-plugin/examples/age-plugin-unencrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,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(())

Check warning on line 181 in age-plugin/examples/age-plugin-unencrypted.rs

View check run for this annotation

Codecov / codecov/patch

age-plugin/examples/age-plugin-unencrypted.rs#L178-L181

Added lines #L178 - L181 were not covered by tests
} else {
panic!("File key is wrong length")

Check warning on line 183 in age-plugin/examples/age-plugin-unencrypted.rs

View check run for this annotation

Codecov / codecov/patch

age-plugin/examples/age-plugin-unencrypted.rs#L183

Added line #L183 was not covered by tests
}
})
});
break;
}
Expand Down
2 changes: 1 addition & 1 deletion age-plugin/src/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,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))),

Check warning on line 138 in age-plugin/src/identity.rs

View check run for this annotation

Codecov / codecov/patch

age-plugin/src/identity.rs#L138

Added line #L138 was not covered by tests
Err(e) => Ok(Err(e)),
})
}
Expand Down
19 changes: 12 additions & 7 deletions age-plugin/src/recipient.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Recipient plugin helpers.

use age_core::{
format::{is_arbitrary_string, FileKey, Stanza, FILE_KEY_BYTES},
format::{is_arbitrary_string, FileKey, Stanza},
plugin::{self, BidirSend, Connection},
secrecy::SecretString,
};
Expand Down Expand Up @@ -183,7 +183,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))),

Check warning on line 186 in age-plugin/src/recipient.rs

View check run for this annotation

Codecov / codecov/patch

age-plugin/src/recipient.rs#L186

Added line #L186 was not covered by tests
Err(e) => Ok(Err(e)),
})
}
Expand Down Expand Up @@ -281,11 +281,16 @@ 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(())

Check warning on line 287 in age-plugin/src/recipient.rs

View check run for this annotation

Codecov / codecov/patch

age-plugin/src/recipient.rs#L284-L287

Added lines #L284 - L287 were not covered by tests
} else {
Err(Error::Internal {
message: "invalid file key length".to_owned(),

Check warning on line 290 in age-plugin/src/recipient.rs

View check run for this annotation

Codecov / codecov/patch

age-plugin/src/recipient.rs#L289-L290

Added lines #L289 - L290 were not covered by tests
})
}
})
}),
(Some(EXTENSION_LABELS), |_| Ok(())),
)?;
Expand Down
2 changes: 1 addition & 1 deletion age/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ to 1.0.0 are beta releases.
- Partial French translation!

### Changed
- Migrated to `i18n-embed 0.15`.
- 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.
Expand Down
2 changes: 1 addition & 1 deletion age/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
6 changes: 3 additions & 3 deletions age/src/cli_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Check warning on line 128 in age/src/cli_common.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common.rs#L128

Added line #L128 was not covered by tests
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)?;

Check warning on line 131 in age/src/cli_common.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common.rs#L131

Added line #L131 was not covered by tests

if !bool::from(
passphrase
Expand Down Expand Up @@ -199,7 +199,7 @@ impl Passphrase {
acc + "-" + s
}
});
Passphrase::Generated(SecretString::new(new_passphrase))
Passphrase::Generated(SecretString::from(new_passphrase))

Check warning on line 202 in age/src/cli_common.rs

View check run for this annotation

Codecov / codecov/patch

age/src/cli_common.rs#L202

Added line #L202 was not covered by tests
}
}

Expand Down
6 changes: 4 additions & 2 deletions age/src/encrypted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,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(),
))
}
Expand All @@ -248,8 +248,10 @@ 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 file_key = FileKey::new(Box::new([12; 16]));
let (wrapped, labels) = pk.wrap_file_key(&file_key).unwrap();
assert!(labels.is_empty());

Expand Down
10 changes: 4 additions & 6 deletions age/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use age_core::{
format::FileKey,
primitives::hkdf,
secrecy::{ExposeSecret, Secret},
secrecy::{ExposeSecret, SecretBox},
};
use rand::{rngs::OsRng, RngCore};

Expand All @@ -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(
Expand Down
12 changes: 6 additions & 6 deletions age/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,10 @@
//! ## Passphrase-based encryption
//!
//! ```
//! use age::secrecy::Secret;
//! use age::secrecy::SecretString;
//!
//! # fn run_main() -> Result<(), ()> {
//! let passphrase = Secret::new("this is not a good passphrase".to_owned());
//! 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);
//!
Expand Down Expand Up @@ -152,16 +152,16 @@
//! ## 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 = Secret::new("this is not a good passphrase".to_owned());
//! let passphrase = SecretString::from("this is not a good passphrase".to_owned());
//!
//! // Encrypt the plaintext to a ciphertext using the passphrase...
//! # fn encrypt(passphrase: Secret<String>, 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(passphrase.clone());
//!
Expand All @@ -176,7 +176,7 @@
//! # }
//!
//! // ... and decrypt the ciphertext to the plaintext again using the same passphrase.
//! # fn decrypt(passphrase: Secret<String>, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
//! # fn decrypt(passphrase: SecretString, encrypted: Vec<u8>) -> Result<Vec<u8>, age::DecryptError> {
//! let decrypted = {
//! let decryptor = age::Decryptor::new(&encrypted[..])?;
//!
Expand Down
13 changes: 8 additions & 5 deletions age/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,11 +649,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(())

Check warning on line 655 in age/src/plugin.rs

View check run for this annotation

Codecov / codecov/patch

age/src/plugin.rs#L652-L655

Added lines #L652 - L655 were not covered by tests
} else {
Err(DecryptError::DecryptionFailed)

Check warning on line 657 in age/src/plugin.rs

View check run for this annotation

Codecov / codecov/patch

age/src/plugin.rs#L657

Added line #L657 was not covered by tests
}
}));
reply.ok(None)
}
CMD_ERROR => {
Expand Down
4 changes: 2 additions & 2 deletions age/src/primitives.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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)`
///
Expand Down
8 changes: 4 additions & 4 deletions age/src/primitives/stream.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(|_| {
Expand All @@ -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();

Expand Down Expand Up @@ -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> {
Expand Down
Loading

0 comments on commit 93fa28a

Please sign in to comment.