From 545a45b3d397fb390a8a13e3c56d1945c14d8d63 Mon Sep 17 00:00:00 2001 From: Joseph O'Brien <98370624+89jobrien@users.noreply.github.com> Date: Wed, 1 Apr 2026 20:57:22 -0400 Subject: [PATCH] =?UTF-8?q?feat(notsecrets):=20EncryptedIdentity=20stub=20?= =?UTF-8?q?=E2=80=94=20passphrase-protected=20identity=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/notsecrets/src/identities/encrypted.rs | 38 +++++++++++++++++++ crates/notsecrets/src/identities/mod.rs | 3 ++ crates/notsecrets/src/lib.rs | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 crates/notsecrets/src/identities/encrypted.rs diff --git a/crates/notsecrets/src/identities/encrypted.rs b/crates/notsecrets/src/identities/encrypted.rs new file mode 100644 index 0000000..5e1ef7f --- /dev/null +++ b/crates/notsecrets/src/identities/encrypted.rs @@ -0,0 +1,38 @@ +use crate::error::AgeError; +use crate::identities::{FileKey, Identity, Stanza}; + +/// An identity whose key material is stored in an age-encrypted file. +/// +/// The outer file is decrypted on each call to `unwrap_file_key` using +/// `passphrase_identity`. The decrypted content must be an `AGE-SECRET-KEY-1...` +/// bech32 string, which is parsed as an `X25519Identity`. +pub struct EncryptedIdentity { + /// Raw bytes of the age-encrypted identity file. + encrypted_data: Vec, + /// Identity used to decrypt the outer age file (typically `ScryptIdentity`). + passphrase_identity: Box, +} + +impl EncryptedIdentity { + pub fn new(encrypted_data: Vec, passphrase_identity: Box) -> Self { + Self { + encrypted_data, + passphrase_identity, + } + } +} + +impl Identity for EncryptedIdentity { + fn unwrap_file_key(&self, _stanza: &Stanza) -> Option> { + // Full implementation in Task 9 after Decryptor is available. + // The integration test below is marked #[ignore] until then. + let _ = &self.encrypted_data; + let _ = self.passphrase_identity.as_ref(); + Some(Err(AgeError::CryptoError( + "EncryptedIdentity requires Decryptor — implemented in Task 9".to_string(), + ))) + } +} + +// Integration test for EncryptedIdentity lives in tests/integration.rs and is +// activated in Task 9 once Encryptor and Decryptor are available. diff --git a/crates/notsecrets/src/identities/mod.rs b/crates/notsecrets/src/identities/mod.rs index 5fa4728..d836d95 100644 --- a/crates/notsecrets/src/identities/mod.rs +++ b/crates/notsecrets/src/identities/mod.rs @@ -12,6 +12,9 @@ pub use scrypt::ScryptIdentity; pub mod ssh_rsa; pub use ssh_rsa::SshRsaIdentity; +pub mod encrypted; +pub use encrypted::EncryptedIdentity; + /// The symmetric file encryption key — 16 random bytes. #[derive(Clone, Debug)] pub struct FileKey([u8; 16]); diff --git a/crates/notsecrets/src/lib.rs b/crates/notsecrets/src/lib.rs index 1d80cd4..d917e1c 100644 --- a/crates/notsecrets/src/lib.rs +++ b/crates/notsecrets/src/lib.rs @@ -6,7 +6,7 @@ pub mod sources; pub mod _legacy; pub use error::AgeError; -pub use identities::{FileKey, Header, Identity, Stanza, X25519Identity, ScryptIdentity, SshEd25519Identity, SshRsaIdentity}; +pub use identities::{FileKey, Header, Identity, Stanza, X25519Identity, ScryptIdentity, SshEd25519Identity, SshRsaIdentity, EncryptedIdentity}; pub use recipients::{Recipient, X25519Recipient, ScryptRecipient, SshEd25519Recipient, SshRsaRecipient}; pub use sources::{BitwardenSource, FileSource, PromptSource};