Implement client-side encryption of the contacts blob on the extension. Completes the auth + crypto plumbing (alongside #2769 auth keypair and #2770 JWT generation) before any contacts UI.
Derive the contacts encryption key via HMAC-SHA256(seedBytes, "freighter-contacts-key") — domain-separated from the auth keypair. Encrypt/decrypt the contacts-list JSON with AES-256-GCM. Wire shape: {"contactCipher": "<base64>"} — base64-encode ciphertext (nonce ‖ tag ‖ body) on write, base64-decode then decrypt on read.
See Cross-Platform Contact Sync design doc — Phase 2 § Contact blob encrypt/decrypt + FR-6.3/6.4/6.5 of the FE spec.
Acceptance:
- Round-trip: same seed + JSON → encrypt → base64 → PUT → GET → base64-decode → decrypt → identical JSON.
- Fresh 96-bit nonce per write.
- Wrong-seed decryption fails with a clear, distinct error (not garbage output, not a silent empty blob).
- Empty
contactCipher (new user) decodes to an empty contacts blob without error.
- Key cached in memory for the session only; never persisted.
Implement client-side encryption of the contacts blob on the extension. Completes the auth + crypto plumbing (alongside #2769 auth keypair and #2770 JWT generation) before any contacts UI.
Derive the contacts encryption key via
HMAC-SHA256(seedBytes, "freighter-contacts-key")— domain-separated from the auth keypair. Encrypt/decrypt the contacts-list JSON with AES-256-GCM. Wire shape:{"contactCipher": "<base64>"}— base64-encode ciphertext (nonce ‖ tag ‖ body) on write, base64-decode then decrypt on read.See Cross-Platform Contact Sync design doc — Phase 2 § Contact blob encrypt/decrypt + FR-6.3/6.4/6.5 of the FE spec.
Acceptance:
contactCipher(new user) decodes to an empty contacts blob without error.