feat(secrets): owner-discriminated AAD (SecretOwner) for group secrets
The crypto foundation for group-owned secrets (the §5.3 'single hardest
correctness detail'), done first and proven before the storage/resolution
layer.
- SecretOwner{App(AppId)|Group(GroupId)}; secret_aad/seal/open take the
owner. The app AAD is byte-identical to the pre-Phase-3
'secret:{app_id}:{name}', so every existing v1 row decrypts unchanged;
group secrets use a disjoint 'secret:group:{group_id}:{name}' namespace
(the 'group:' infix separates app/group AAD even for equal UUIDs).
- All callers (SDK get/set, secrets_api, apply email-secret read) wrapped
in SecretOwner::App — behavior identical for app secrets.
- New audit test aad_distinguishes_app_and_group_owner proves the two
namespaces don't collide (both directions, even reusing the UUID); the
existing cross-app/cross-name swap audit tests stay green.
16 secrets_service tests pass; clippy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -827,8 +827,13 @@ impl ApplyService {
|
||||
(push it with `pic secret set {name}`)"
|
||||
))
|
||||
})?;
|
||||
let plaintext = crate::secrets_service::open(&self.master_key, app_id, name, &stored)
|
||||
.map_err(|e| ApplyError::Invalid(format!("could not read secret `{name}`: {e}")))?;
|
||||
let plaintext = crate::secrets_service::open(
|
||||
&self.master_key,
|
||||
crate::secrets_service::SecretOwner::App(app_id),
|
||||
name,
|
||||
&stored,
|
||||
)
|
||||
.map_err(|e| ApplyError::Invalid(format!("could not read secret `{name}`: {e}")))?;
|
||||
// The inbound HMAC must be a non-empty string — an empty/whitespace
|
||||
// key is forgeable (preserves the spirit of audit 2026-06-11 H-B2).
|
||||
match plaintext.as_str() {
|
||||
|
||||
Reference in New Issue
Block a user