# Lock encrypted attachments, version 1

Implementation notes, not an independent security audit. Last updated: 2026-10-03.

New web, iPhone and macOS attachment uploads use this format. Legacy web files
uploaded through public storage were not encrypted this way and are not migrated
by this release. Deploy the updated web bundle before relying on this behavior.

## Before any upload

The client generates an independent, random 32-byte key for each file, plus three
independent random 12-byte nonces. AES-256-GCM encrypts the file and JSON metadata
with that file key. AES-256-GCM wraps the raw file key with the message's 32-byte
key. Each operation uses a 128-bit authentication tag, appended to ciphertext.

Metadata JSON contains `filename`, `mime_type`, and the plaintext byte `size`.
Metadata is encrypted, not sent as an upload filename or HTTP content type.

Additional authenticated data is UTF-8, exactly:

* File: `lock:attachment:v1:{id}:file`
* Metadata: `lock:attachment:v1:{id}:metadata`
* Wrapped file key: `lock:attachment:v1:{id}:key`

`{id}` is a lowercase canonical UUID. The reference embedded in the encrypted
message payload contains exactly `id`, `version` (integer 1), `cipher` (`A256GCM`),
`file_iv`, `metadata_ciphertext`, `metadata_iv`, `wrapped_key`, and `key_iv`.
Binary reference fields use standard base64 (not base64url).

## Storage and retrieval

`PUT /api/v1/teams/{team}/conversations/{conversation}/attachment-uploads/{id}`
receives only ciphertext with content type `application/octet-stream`.
The authenticated API stores it privately, stages it for up to 60 minutes, and
atomically consumes it when a validated message is committed. A message may have
up to 5 attachments, each at most 20 MiB before encryption, within a 100 MiB
ciphertext total. The server can see ciphertext size, uploader, conversation,
message association, timing and membership. Encryption does not hide this metadata.

Downloads require authenticated conversation access. The client unwraps the key,
authenticates metadata, verifies ciphertext size equals plaintext size plus 16,
and authenticates/decrypts file contents before offering a local download.
New web downloads are not public storage links.

## Reproducible evidence

`attachment-v1-test-vector.json` contains an intentionally public test message key,
reference, ciphertext and expected plaintext. It was generated with Node's
AES-GCM implementation and is checked with WebCrypto in the automated tests.
`verify-attachment.swift` independently checks the vector with Apple CryptoKit:

```sh
swift verify-attachment.swift attachment-v1-test-vector.json
```

Never use the vector's keys or nonces for actual files. These checks demonstrate
format compatibility and authenticated decryption; they are not an audit, a proof
of the entire product's security, or an authenticated web-to-iPhone runtime test.

## Trust limits

Message keys are wrapped for conversation recipients. Current messaging uses RSA
recovery capsules, with optional P-256/HKDF prekey capsules on the web. RSA recovery
means these messages do not provide strict forward secrecy against later theft
of a recipient's RSA private key. Recipient public keys are delivered by the
server without independently verified fingerprints.

Password-protected private-key backups use PBKDF2-HMAC-SHA256 (600,000 iterations)
and AES-256-GCM. The same account password is submitted to the authentication
server for sign-in. This is not a zero-knowledge password-authentication protocol:
a compromised authentication server could capture that password and decrypt its
stored key envelope. Browser-delivered code and unlocked devices must also be
trusted. Account resets alone cannot recreate missing encryption keys.
