Migrate to secrecy 0.10

This commit is contained in:
Jack Grigg 2024-11-03 05:32:37 +00:00
parent a59f0479d0
commit 93fa28ad78
27 changed files with 155 additions and 93 deletions

10
Cargo.lock generated
View file

@ -1844,9 +1844,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pinentry"
version = "0.5.1"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72268b7db3a2075ea65d4b93b755d086e99196e327837e690db6559b393a8d69"
checksum = "c1ecb857a7b11a03e8872c704d0a1ae1efc20533b3be98338008527a1928ffa6"
dependencies = [
"log",
"nom",
@ -2344,9 +2344,9 @@ dependencies = [
[[package]]
name = "secrecy"
version = "0.8.0"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]
@ -2979,7 +2979,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View file

@ -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"

View file

@ -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.

View file

@ -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))
}
}

View file

@ -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};

View file

@ -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(())
} else {
panic!("File key is wrong length")
}
})
});
break;
}

View file

@ -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))),
Err(e) => Ok(Err(e)),
})
}

View file

@ -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,
};
@ -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))),
Err(e) => Ok(Err(e)),
})
}
@ -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(())
} else {
Err(Error::Internal {
message: "invalid file key length".to_owned(),
})
}
})
}),
(Some(EXTENSION_LABELS), |_| Ok(())),
)?;

View file

@ -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.

View file

@ -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.)

View file

@ -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))
}
}

View file

@ -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(),
))
}
@ -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());

View file

@ -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(

View file

@ -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);
//!
@ -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());
//!
@ -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[..])?;
//!

View file

@ -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(())
} else {
Err(DecryptError::DecryptionFailed)
}
}));
reply.ok(None)
}
CMD_ERROR => {

View file

@ -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)`
///

View file

@ -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> {

View file

@ -477,7 +477,7 @@ mod tests {
fn scrypt_round_trip() {
let test_msg = b"This is a test message. For testing.";
let mut recipient = scrypt::Recipient::new(SecretString::new("passphrase".to_string()));
let mut recipient = scrypt::Recipient::new(SecretString::from("passphrase".to_string()));
// Override to something very fast for testing.
recipient.set_work_factor(2);
@ -492,7 +492,7 @@ mod tests {
let d = Decryptor::new(&encrypted[..]).unwrap();
let mut r = d
.decrypt(
Some(&scrypt::Identity::new(SecretString::new("passphrase".to_string())) as _)
Some(&scrypt::Identity::new(SecretString::from("passphrase".to_string())) as _)
.into_iter(),
)
.unwrap();
@ -549,7 +549,8 @@ mod tests {
#[test]
fn mixed_recipient_and_passphrase() {
let pk: x25519::Recipient = crate::x25519::tests::TEST_PK.parse().unwrap();
let passphrase = crate::scrypt::Recipient::new(SecretString::new("passphrase".to_string()));
let passphrase =
crate::scrypt::Recipient::new(SecretString::from("passphrase".to_string()));
let recipients = [&pk as &dyn Recipient, &passphrase as _];

View file

@ -260,9 +260,10 @@ impl crate::Identity for Identity {
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),
)

View file

@ -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
}

View file

@ -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,7 +531,7 @@ 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, labels) = pk.wrap_file_key(&file_key).unwrap();
assert!(labels.is_empty());
@ -532,7 +558,7 @@ 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, labels) = pk.wrap_file_key(&file_key).unwrap();
assert!(labels.is_empty());

View file

@ -68,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();
@ -136,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();
}))
})
}
}
@ -238,7 +239,7 @@ impl crate::Recipient for Recipient {
#[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};
@ -265,7 +266,7 @@ 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);

View file

@ -44,7 +44,7 @@ fn age_test_vectors() -> Result<(), Box<dyn std::error::Error>> {
name
))?
.read_to_string(&mut passphrase)?;
let passphrase = SecretString::new(passphrase);
let passphrase = SecretString::from(passphrase);
let identity = scrypt::Identity::new(passphrase);
d.decrypt(Some(&identity as _).into_iter())
};

4
fuzz-afl/Cargo.lock generated
View file

@ -879,9 +879,9 @@ dependencies = [
[[package]]
name = "secrecy"
version = "0.8.0"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]

4
fuzz/Cargo.lock generated
View file

@ -884,9 +884,9 @@ dependencies = [
[[package]]
name = "secrecy"
version = "0.8.0"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]

View file

@ -654,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]]

View file

@ -16,8 +16,8 @@ user-login = "jrmuizel"
user-name = "Jeff Muizelaar"
[[publisher.pinentry]]
version = "0.5.1"
when = "2024-08-31"
version = "0.6.0"
when = "2024-11-03"
user-id = 6289
user-login = "str4d"
user-name = "Jack Grigg"