Skip to content

Encryption with your own key

With SSE-C, the client sends a 256-bit key with each request, and leBLOB encrypts the object with it before writing it to disk. The server never stores the key, only a keyed check that tells it whether the next request brings the same one. A stolen disk or a stolen metadata backup holds neither readable data nor the key.

This is the only server-side encryption leBLOB offers. SSE-S3 and SSE-KMS, where the server manages the keys, aren't built: a request asking for them gets 501 NotImplemented, so no data is stored in clear.

Losing the key loses the object

There is no way to read an object without the key, by design. Keep the keys somewhere other than the store.

Using it

import os

key = os.urandom(32)
s3.put_object(
    Bucket="my-bucket", Key="secret.txt", Body=b"...",
    SSECustomerAlgorithm="AES256", SSECustomerKey=key,
)
s3.get_object(
    Bucket="my-bucket", Key="secret.txt",
    SSECustomerAlgorithm="AES256", SSECustomerKey=key,
)

boto3 encodes the key and adds the key's MD5 itself.

head -c 32 /dev/urandom > key.bin
aws --endpoint-url https://s3.example.org s3 cp secret.txt s3://my-bucket/ \
    --sse-c AES256 --sse-c-key fileb://key.bin
aws --endpoint-url https://s3.example.org s3 cp s3://my-bucket/secret.txt . \
    --sse-c AES256 --sse-c-key fileb://key.bin

On the wire, a request has three headers: x-amz-server-side-encryption-customer-algorithm: AES256, x-amz-server-side-encryption-customer-key with the key in base64, and x-amz-server-side-encryption-customer-key-MD5.

The key goes with every request that reads or writes the object's bytes: PutObject, GetObject, HeadObject, CreateMultipartUpload and each UploadPart. CopyObject and UploadPartCopy take the destination's key in the same headers. The source's key goes in x-amz-copy-source-server-side-encryption-customer-*, so a copy can decrypt with one key and encrypt with another, or with none.

What is refused

  • A wrong or missing key on an encrypted object gets 400, before anything is read. So does a key sent for an object that isn't encrypted.
  • A key over plain HTTP is refused, as on AWS, because anyone on the way could read it. Serve HTTPS, or, if a reverse proxy in front of leBLOB terminates TLS, start the server with --allow-sse-c-over-http. Use that option for nothing else.
  • A key on a request that can't use it gets 501 NotImplemented, since the client would otherwise believe it had encrypted something.
  • A form upload can't be encrypted yet: a form asking for it is refused.

What changes for the object

  • The ETag is the MD5 of what is stored, which is the encrypted bytes, so it isn't the MD5 of your data. Checks the client asks for, such as Content-MD5 or x-amz-checksum-*, are made over your data, as usual.
  • An encrypted object takes a little more room on disk than its size: a 16-byte salt per blob, and 16 bytes per 64 KiB.
  • Range reads work, and decrypt only the part they touch.
  • Scrub, repair, copies on other disks, fsck and backups all work without the key, since they handle stored bytes only.

After the rename to leBLOB

Objects stored with SSE-C by a version from before the project was renamed can no longer be read. See the changelog.

How it is built

Each blob starts with a 16-byte random salt, then the data in AES-256-GCM chunks of 64 KiB. Each blob is encrypted with a separate key, derived from the client's key and the salt with HMAC, so a chunk's number can serve as the nonce without ever repeating. Each chunk authenticates its number and whether it is the last, so chunks can't be reordered or dropped without the read failing. Each part of a multipart upload is a separate blob, with a separate salt.