Shared passphrase documents

Copy Markdown View Source

Use this pattern when a document should be decryptable by anyone who knows the same high-entropy shared secret.

This is the direct payload-encryption model. For alias-based sharing through sessions, with passphrase replacement/revocation without re-encrypting document ciphertext, use Factor-bound aliases.

KMS API:

PDK means password-derived key. KMS derives an inner data key from the caller-supplied secret and protects the payload with the configured RMK provider.

When to use

Good fit:

  • shared vault documents;
  • export bundles protected by a customer-held secret;
  • invite links where the link carries or derives a high-entropy secret;
  • data that should not decrypt from KMS/database alone.

Poor fit:

  • low-entropy human passwords;
  • secrets users commonly forget;
  • data that needs per-user revocation without re-encryption;
  • public web forms where the passphrase may be logged by proxies or analytics.

encrypt_rmk_pdk/3 enforces KMS secret policy for encryption. You can use a generated human-friendly passphrase:

shared_secret = KMS.Passphrase.generate()
# => "ba-la-na-pa-ra ..."

KMS.Passphrase.generate/1 uses Diceware-style five-die rolls with 10 words by default, enough to satisfy KMS' high-entropy secret policy.

For machine-only flows, 32 random bytes encoded for transport also work:

shared_secret = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)

Encrypt a document

document_id = Ecto.UUID.generate()
aad = "my-app:v1:document:#{document_id}:field:body"

{:ok, encrypted_body} =
  KMS.encrypt_rmk_pdk(
    "shared secret document body",
    shared_secret,
    aad: aad
  )

Store:

  • document_id
  • encrypted_body
  • enough stable metadata to reconstruct aad

Do not store shared_secret next to the document.

Decrypt a document

aad = "my-app:v1:document:#{document_id}:field:body"

{:ok, body} = KMS.decrypt_rmk_pdk(encrypted_body, shared_secret, aad: aad)

Wrong secret or wrong AAD fails:

{:error, :decryption_failed} =
  KMS.decrypt_rmk_pdk(encrypted_body, shared_secret, aad: "my-app:v1:document:other")

Phoenix schema shape

schema "shared_documents" do
  field :title, :string
  field :encrypted_body, :string, redact: true
  field :body, :string, virtual: true, redact: true

  timestamps(type: :utc_datetime)
end

Context outline:

def create_shared_document(attrs, shared_secret) do
  id = Ecto.UUID.generate()
  aad = shared_document_aad(id)

  with {:ok, encrypted_body} <- KMS.encrypt_rmk_pdk(attrs.body, shared_secret, aad: aad) do
    insert_document(%{id: id, title: attrs.title, encrypted_body: encrypted_body})
  end
end

def open_shared_document(document, shared_secret) do
  with {:ok, body} <- KMS.decrypt_rmk_pdk(document.encrypted_body, shared_secret, aad: shared_document_aad(document.id)) do
    {:ok, %{document | body: body}}
  end
end

defp shared_document_aad(id), do: "my-app:v1:shared-document:#{id}:field:body"

Security notes

  • AAD is not secret; it is context that must match on decrypt.
  • The shared secret is a credential. Do not log it.
  • Low-entropy passwords are not enough for this API.
  • Rotating the shared secret requires decrypting with the old secret and re-encrypting with the new one.
  • Anyone with the shared secret can decrypt. KMS bindings do not identify or revoke individual holders of that secret.

Use multi-user encryption instead when per-user authorization, audit, or revocation matters.