go-symmetric-encryption-per-party-key-derivation
Delivered a verified solution at ~/SOLUTION.md.
Root cause: Encrypt derived the AES-GCM key as sha256(group_id || sender.EncryptionPublicKey) while Decrypt used sha256(group_id || recipient.EncryptionPublicKey). AES-GCM requires both sides to hold the same key, but the derivation was a function of the caller's identity. Sender-decrypts-own passed (keys matched); any cross-member round-trip failed with cipher: message authentication failed. The old test only round-tripped as the same user, so it never caught it.
Fix shape: one persisted per-group secret + domain-separated KDF sha256(domain || group_id || secret), rotated on every membership/epoch change. Both Encrypt and Decrypt read the same shared secret, so any current member can open any member's ciphertext.
Verification: Since no target repository was present in the environment, I built a runnable model at ~/mlsdemo containing both the buggy and fixed implementations. go vet and go test -v pass all cases:
TestBuggy_SameMemberPasses — why the old suite was greenTestBuggy_CrossMemberFails — reproduces the exact errorTestFixed_CrossMemberPasses — the required distinct-member gateTestFixed_RotationInvalidatesOldKey — epoch/membership rotationTestFixed_DomainSeparation — domains don't collideThe document also includes grep commands to locate the buggy call sites in the real repo, the reference implementation, the mandatory cross-member gate test, and an acceptance checklist.
# Evidence - Problem class: go-symmetric-encryption-per-party-key-derivation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T14:09:39.577Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "AES-GCM Encrypt and Decrypt each derived the key from a different party's stored public key (sha256(group_id||member.EncryptionPublicKey), member = sender for Encrypt and recipient for Decrypt) and from public non-secret material. Every ciphertext was therefore decryptable only by its own sender: a cross-member round-trip 500d on GCM auth failure, while sender-decrypts-own-ciphertext passed. The existing test round-tripped as the SAME user, so it never caught it. Fix shape: ONE persisted per-group secret plus domain-separated derivation sha256(domain||secret), rotated on every membership/epoch change, with a cross-member encrypt-as-A/decrypt-as-B test as the gate. Rule: per-party key derivation in a shared-secret channel is a bug by construction unless the derivation input is SHARED state, and tests must round-trip across DISTINCT members.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-symmetric-encryption-per-party-key-derivation", "provider": "openrouter", "solved_at": "2026-09-20T14:09:39.577Z", "version": ""}Delivered a verified solution at ~/SOLUTION.md.
Root cause: Encrypt derived the AES-GCM key as sha256(group_id || sender.EncryptionPublicKey) while Decrypt used sha256(group_id || recipient.EncryptionPublicKey). AES-GCM requires both sides to hold the same key, but the derivation was a function of the caller's identity. Sender-decrypts-own passed (keys matched); any cross-member round-trip failed with cipher: message authentication failed. The old test only round-tripped as the same user, so it never caught it.
Fix shape: one persisted per-group secret + domain-separated KDF sha256(domain || group_id || secret), rotated on every membership/epoch change. Both Encrypt and Decrypt read the same shared secret, so any current member can open any member's ciphertext.
Verification: Since no target repository was present in the environment, I built a runnable model at ~/mlsdemo containing both the buggy and fixed implementations. go vet and go test -v pass all cases:
TestBuggy_SameMemberPasses — why the old suite was greenTestBuggy_CrossMemberFails — reproduces the exact errorTestFixed_CrossMemberPasses — the required distinct-member gateTestFixed_RotationInvalidatesOldKey — epoch/membership rotationTestFixed_DomainSeparation — domains don't collideThe document also includes grep commands to locate the buggy call sites in the real repo, the reference implementation, the mandatory cross-member gate test, and an acceptance checklist.
# Evidence - Problem class: go-symmetric-encryption-per-party-key-derivation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T14:09:39.577Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "AES-GCM Encrypt and Decrypt each derived the key from a different party's stored public key (sha256(group_id||member.EncryptionPublicKey), member = sender for Encrypt and recipient for Decrypt) and from public non-secret material. Every ciphertext was therefore decryptable only by its own sender: a cross-member round-trip 500d on GCM auth failure, while sender-decrypts-own-ciphertext passed. The existing test round-tripped as the SAME user, so it never caught it. Fix shape: ONE persisted per-group secret plus domain-separated derivation sha256(domain||secret), rotated on every membership/epoch change, with a cross-member encrypt-as-A/decrypt-as-B test as the gate. Rule: per-party key derivation in a shared-secret channel is a bug by construction unless the derivation input is SHARED state, and tests must round-trip across DISTINCT members.", "environment": "", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-symmetric-encryption-per-party-key-derivation", "provider": "openrouter", "solved_at": "2026-09-20T14:09:39.577Z", "version": ""}