Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove split between recipient and passphrase encryption #507

Merged
merged 7 commits into from
Aug 4, 2024

Conversation

str4d
Copy link
Owner

@str4d str4d commented Jul 29, 2024

Closes #504.

@str4d str4d added this to the rage 0.11.0 milestone Jul 29, 2024
Copy link

codecov bot commented Jul 29, 2024

Codecov Report

Attention: Patch coverage is 60.80000% with 49 lines in your changes missing coverage. Please review.

Project coverage is 51.72%. Comparing base (e568a64) to head (f69c29b).

Files Patch % Lines
rage/src/bin/rage/main.rs 30.00% 21 Missing ⚠️
rage/src/bin/rage-mount/main.rs 12.50% 14 Missing ⚠️
age/src/encrypted.rs 36.36% 7 Missing ⚠️
age/src/protocol.rs 89.65% 3 Missing ⚠️
age/src/error.rs 0.00% 2 Missing ⚠️
age/src/format.rs 93.33% 1 Missing ⚠️
age/src/scrypt.rs 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #507      +/-   ##
==========================================
- Coverage   52.01%   51.72%   -0.30%     
==========================================
  Files          43       42       -1     
  Lines        3937     3938       +1     
==========================================
- Hits         2048     2037      -11     
- Misses       1889     1901      +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

} else {
Err(DecryptError::InvalidHeader)
}
}

/// Returns `true` if the age file is encrypted to a passphrase.
pub fn is_scrypt(&self) -> bool {
Copy link
Owner Author

@str4d str4d Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only API that I'm not entirely sure about exposing in the public API.

I'm defining / checking structural validity of the header (if an scrypt stanza is present, it must be the only stanza) in once place (HeaderV1), and enforcing it in two places:

  • in the Encryptor via the HeaderV1::new constructor;
  • in the Decryptor constructors via Decryptor::from_v1_header.

During encryption, we still have the passphrase-specific helper, but it is effectively a thin wrapper around the Recipient API, and we now throw an error if the caller mixes things up (vs being infallible previously).

This method exposes which of the two states we are in during decryption (replacing what I previously exposed with an enum), so that we can report "mixed identity and passphrase" before asking the user for a passphrase.

@FiloSottile AFAICT you don't expose a similar API, and instead check for scrypt stanza presence or absence in multiple places:

AFAICT these are functionally equivalent to my approach: given that anyone can define an Identity and place it first, they can use that to extract stanza types. So my API isn't exposing more power than is already exposed. But does it still make sense to special-case scrypt in this way?

I might also just leave this API in the public API until I implement labels (#403), and re-evaluate then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the main difference is that the cmd/age approach is a CLI special case, not a library special case.

rejectScryptIdentity and LazyScryptIdentity are CLI UX affordances: the first to print a helpful error, the second to avoid asking for a passphrase if we are going to error out sooner. Neither are concerned with scrypt stanzas being the only one in the file.

The structural check is built into the scrypt Identity implementation on the decryption side: it just receives all the stanzas, and errors out if it sees multiple. Note that this is not security load-bearing, since an attacker that was a co-recipient can strip the other stanzas.

On the encryption side, I used to have a special case to prevent library users from mixing scrypt with other stanzas, but that is now replaced with labels.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structural check is built into the scrypt Identity implementation on the decryption side: it just receives all the stanzas, and errors out if it sees multiple.

Aha, this is where we differ. My Identity trait does have an Identity::unwrap_stanzas method, but it was added as a performance optimization in #129 for identities that can benefit from pre-computations (such as plugin identities, where you want to provide all stanzas to the same plugin invocation - this is specifically what I added it for). The default implementation of it just passes each stanza individually to Identity::unwrap_stanza and returns the file key from the first stanza that succeeds.

Identity was the first trait I added (in #117), which I introduced as part of building the plugin functionality. I suspect that I limited it to one stanza at a time in order to limit how much logic would be implemented behind it (leaving most of the restrictions implemented in the header logic, like where I currently implement the scrypt restriction for decryption). However, given that multi-stanza unwrapping was later added specifically for plugins, that on its own isn't a strong reason to keep it that way

I think I also see the benefit of Recipient and Identity being symmetric. Originally Recipient only returned a single stanza (#119) and thus was symmetric with Identity, but that changed in #129 as well to enable encrypt-time coalescing by allowing a Recipient impl to represent multiple recipients (and thus generate multiple stanzas).

So I think what I will do after this PR is refactor Identity so that its trait is documented as always receiving all stanzas associated with an individual file.

Note that this is not security load-bearing, since an attacker that was a co-recipient can strip the other stanzas.

Yep, which is partly why I don't implement that restriction in my scrypt impl, and instead implement it in the header parser.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened #509 and #510 for tracking the above.

@str4d str4d merged commit a510e76 into main Aug 4, 2024
36 of 37 checks passed
@str4d str4d deleted the 504-the-day-of-reckoning branch August 4, 2024 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Remove split between recipient and passphrase encryption
2 participants