AwsEncryptionSdk.Stream.Decryptor (AWS Encryption SDK v1.2.2)

View Source

Streaming decryptor state machine for incremental ciphertext processing.

When to Use Streaming

Use Stream.Decryptor instead of Client.decrypt/2 when:

  • Decrypting large files that don't fit in memory
  • Processing encrypted data from network streams
  • Working with ciphertext sources that produce chunks incrementally
  • Memory constraints require bounded memory usage

For small messages (< 1MB), the simpler Client.decrypt/2 API is recommended.

Memory Efficiency

The streaming decryptor maintains constant memory usage:

  • Buffers only data needed to parse the current frame
  • Emits plaintext incrementally after frame authentication
  • No need to load entire ciphertext into memory

Memory usage is bounded by the frame size plus header size, regardless of total message size.

Non-Framed Messages

A non-framed message (content type 0x01, which other SDKs may write) has one body: an IV, an 8-byte content length, the ciphertext and one authentication tag over all of it. That tag cannot be checked until the whole body has arrived, so the decryptor buffers the body and releases the plaintext whole, in one piece, only after the tag verifies (and, on a signed suite, only after the footer's signature verifies, as a framed message's final frame is held). The buffer is bounded by the content length the body declares; a declared length above the format's limit of 2^36 - 32 bytes is refused with {:error, :content_too_large} before the body is buffered.

Plaintext Verification Status

Decrypted plaintext is tagged with verification status:

  • :verified - Plaintext is authenticated and safe to use

    • For unsigned suites: immediately after frame authentication
    • For signed suites: after signature verification completes
  • :unverified - Plaintext not yet cryptographically verified

    • Only for signed algorithm suites
    • Signature verification happens at end of stream
    • Do not use unverified plaintext until signature validates

Handling Signed Suites

For signed algorithm suites (ECDSA P-384), you must handle verification:

Option 1: Fail immediately (safest):

{:ok, dec} = Decryptor.init(
  get_materials: materials_fn,
  fail_on_signed: true
)

Option 2: Buffer unverified plaintext (for streaming):

plaintexts = []
for {plaintext, status} <- decrypted_chunks do
  case status do
    :verified -> use_plaintext(plaintext)
    :unverified -> plaintexts = [plaintext | plaintexts]
  end
end
# At end of stream, all buffered plaintext is verified

Option 3: Use high-level API (recommended):

Use AwsEncryptionSdk.Stream.decrypt/3 which handles verification automatically.

Integration with Elixir Streams

Designed to work seamlessly with Stream module:

File.stream!("encrypted.bin", [], 4096)
|> AwsEncryptionSdk.Stream.decrypt(client)
|> Stream.map(fn {plaintext, _status} -> plaintext end)
|> Stream.into(File.stream!("decrypted.bin"))
|> Stream.run()

See AwsEncryptionSdk.Stream for high-level streaming API.

State Machine

The decryptor progresses through these states:

  1. :init - Not started, awaiting ciphertext
  2. :reading_header - Accumulating header bytes
  3. :decrypting - Processing frames, or a non-framed body
  4. :reading_footer - Accumulating footer (signed suites only)
  5. :done - Decryption complete

Low-Level Example

For custom streaming logic, use the state machine directly:

get_materials = fn header ->
  # Obtain decryption materials from CMM
  cmm.get_decryption_materials(...)
end

{:ok, dec} = Decryptor.init(get_materials: get_materials)

# Process ciphertext chunks
{:ok, dec, plaintexts1} = Decryptor.update(dec, chunk1)
{:ok, dec, plaintexts2} = Decryptor.update(dec, chunk2)
{:ok, dec, final_plaintexts} = Decryptor.finalize(dec)

# Each plaintexts is a list of {binary, :verified | :unverified} tuples

Security

  • Never release unauthenticated plaintext to untrusted contexts
  • For signed suites, verify signature before using plaintext
  • The decryptor validates authentication tags before emitting plaintext
  • Commitment verification happens during header processing

See Also

Summary

Functions

Finalizes decryption.

Returns the parsed header, if available.

Initializes a new streaming decryptor.

Returns the current state.

Processes ciphertext chunk.

Types

plaintext_status()

@type plaintext_status() :: :verified | :unverified

state()

@type state() :: :init | :reading_header | :decrypting | :reading_footer | :done

t()

@type t() :: %AwsEncryptionSdk.Stream.Decryptor{
  buffer: binary(),
  derived_key: binary() | nil,
  expected_sequence: pos_integer(),
  fail_on_signed: boolean(),
  final_frame_plaintext: binary() | nil,
  get_materials:
    (AwsEncryptionSdk.Format.Header.t() ->
       {:ok, AwsEncryptionSdk.Materials.DecryptionMaterials.t()}
       | {:error, term()})
    | nil,
  header: AwsEncryptionSdk.Format.Header.t() | nil,
  materials: AwsEncryptionSdk.Materials.DecryptionMaterials.t() | nil,
  signature_acc: AwsEncryptionSdk.Stream.SignatureAccumulator.t() | nil,
  state: state()
}

Functions

finalize(dec)

@spec finalize(t()) :: {:ok, t(), [{binary(), plaintext_status()}]} | {:error, term()}

Finalizes decryption.

Verifies no trailing bytes remain and completes signature verification for signed suites. Returns {:ok, updated_decryptor, final_plaintexts}.

header(decryptor)

@spec header(t()) :: AwsEncryptionSdk.Format.Header.t() | nil

Returns the parsed header, if available.

init(opts \\ [])

@spec init(keyword()) :: {:ok, t()} | {:error, term()}

Initializes a new streaming decryptor.

Options

  • :get_materials - Function (header) -> {:ok, materials} | {:error, reason} to obtain decryption materials after header is parsed. Required.

  • :fail_on_signed - If true, fails immediately when a signed algorithm suite is detected. Default: false.

state(decryptor)

@spec state(t()) :: state()

Returns the current state.

update(dec, ciphertext)

@spec update(t(), binary()) ::
  {:ok, t(), [{binary(), plaintext_status()}]} | {:error, term()}

Processes ciphertext chunk.

Returns {:ok, updated_decryptor, plaintexts} where plaintexts is a list of {plaintext_binary, status} tuples. Status is :verified for unsigned suites or final frame after signature verification, :unverified otherwise.

For unsigned suites, plaintext is released immediately after frame authentication. A non-framed body is released whole once its tag (and, on a signed suite, the footer's signature) verifies.