Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ jobs:
needs: compile
runs-on: ubuntu-latest

env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
KMS_KEY_ARN: ${{ secrets.KMS_KEY_ARN }}
AWS_REGION: us-east-1

strategy:
fail-fast: false
matrix:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Integration tests now run by default in CI (#68)
- Coverage threshold adjusted from 94% to 92%

### Added
- Comprehensive streaming error tests covering all error paths (#68)
- CMM dispatch tests for RequiredEncryptionContext and Caching CMMs (#68)
- Caching CMM for reducing expensive key provider calls (#61)
- CacheEntry struct with TTL and usage limit tracking
- CryptographicMaterialsCache behaviour defining cache interface
Expand Down Expand Up @@ -43,6 +49,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Comprehensive test suite with 41 streaming tests (edge cases, integration, signed suites)
- Edge case tests for empty plaintext, single byte, exact frame multiples, byte-by-byte input

### Fixed
- KMS integration tests skip gracefully when AWS credentials unavailable (#68)
- Unused default parameter warning in caching_test.exs (#68)

### Removed
- All temporary coveralls-ignore markers (42 markers across 4 files) (#68)
- Default exclusion of :integration tag from test suite (#68)

## [0.5.0] - 2026-01-28

### Added
Expand Down
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ coverage:
status:
project:
default:
target: 94%
target: 92%
threshold: 1%
patch:
default:
Expand Down
2 changes: 1 addition & 1 deletion coveralls.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"coverage_options": {
"minimum_coverage": 94,
"minimum_coverage": 92,
"treat_no_relevant_lines_as_covered": true
},
"skip_files": [
Expand Down
9 changes: 9 additions & 0 deletions lib/aws_encryption_sdk/cmm/required_encryption_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ defmodule AwsEncryptionSdk.Cmm.RequiredEncryptionContext do
@behaviour AwsEncryptionSdk.Cmm.Behaviour

alias AwsEncryptionSdk.Cmm.Behaviour, as: CmmBehaviour
alias AwsEncryptionSdk.Cmm.Caching
alias AwsEncryptionSdk.Cmm.Default

@type t :: %__MODULE__{
Expand Down Expand Up @@ -168,6 +169,10 @@ defmodule AwsEncryptionSdk.Cmm.RequiredEncryptionContext do
get_encryption_materials(cmm, request)
end

defp call_underlying_cmm_encrypt(%Caching{} = cmm, request) do
Caching.get_encryption_materials(cmm, request)
end

defp call_underlying_cmm_encrypt(cmm, _request) do
{:error, {:unsupported_cmm_type, cmm.__struct__}}
end
Expand Down Expand Up @@ -206,6 +211,10 @@ defmodule AwsEncryptionSdk.Cmm.RequiredEncryptionContext do
get_decryption_materials(cmm, request)
end

defp call_underlying_cmm_decrypt(%Caching{} = cmm, request) do
Caching.get_decryption_materials(cmm, request)
end

defp call_underlying_cmm_decrypt(cmm, _request) do
{:error, {:unsupported_cmm_type, cmm.__struct__}}
end
Expand Down
8 changes: 0 additions & 8 deletions lib/aws_encryption_sdk/keyring/kms_client/ex_aws.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ defmodule AwsEncryptionSdk.Keyring.KmsClient.ExAws do
{:ok, %__MODULE__{region: region, config: config}}
end

# NOTE: This code is tested via integration tests (test/aws_encryption_sdk/keyring/kms_client/ex_aws_integration_test.exs)
# Run with: source .env && mix test --only integration
# Coverage is excluded here to allow local development without AWS credentials.
# CI should run integration tests for full coverage.
# coveralls-ignore-start

@impl KmsClient
def generate_data_key(
%__MODULE__{} = client,
Expand Down Expand Up @@ -235,6 +229,4 @@ defmodule AwsEncryptionSdk.Keyring.KmsClient.ExAws do
|> Macro.underscore()
|> String.to_atom()
end

# coveralls-ignore-stop
end
22 changes: 0 additions & 22 deletions lib/aws_encryption_sdk/stream.ex
Original file line number Diff line number Diff line change
Expand Up @@ -105,16 +105,12 @@ defmodule AwsEncryptionSdk.Stream do
{:ok, enc, ciphertext} ->
{[header, ciphertext], enc}

# coveralls-ignore-start
{:error, reason} ->
raise "Encryption failed: #{inspect(reason)}"
# coveralls-ignore-stop
end

# coveralls-ignore-start
{:error, reason} ->
raise "Failed to generate header: #{inspect(reason)}"
# coveralls-ignore-stop
end
end

Expand All @@ -126,10 +122,8 @@ defmodule AwsEncryptionSdk.Stream do
{:ok, enc, ciphertext} ->
{[ciphertext], enc}

# coveralls-ignore-start
{:error, reason} ->
raise "Encryption failed: #{inspect(reason)}"
# coveralls-ignore-stop
end
end

Expand All @@ -141,16 +135,12 @@ defmodule AwsEncryptionSdk.Stream do
{:ok, _enc, final} ->
{[header, final], enc}

# coveralls-ignore-start
{:error, reason} ->
raise "Finalization failed: #{inspect(reason)}"
# coveralls-ignore-stop
end

# coveralls-ignore-start
{:error, reason} ->
raise "Failed to generate header: #{inspect(reason)}"
# coveralls-ignore-stop
end
end

Expand All @@ -159,10 +149,8 @@ defmodule AwsEncryptionSdk.Stream do
{:ok, _enc, final} ->
{[final], enc}

# coveralls-ignore-start
{:error, reason} ->
raise "Finalization failed: #{inspect(reason)}"
# coveralls-ignore-stop
end
end

Expand Down Expand Up @@ -195,10 +183,8 @@ defmodule AwsEncryptionSdk.Stream do
{:ok, dec, plaintexts} ->
{[plaintexts], dec}

# coveralls-ignore-start
{:error, reason} ->
raise "Decryption failed: #{inspect(reason)}"
# coveralls-ignore-stop
end
end

Expand All @@ -207,10 +193,8 @@ defmodule AwsEncryptionSdk.Stream do
{:ok, _dec, final} ->
{[final], dec}

# coveralls-ignore-start
{:error, reason} ->
raise "Finalization failed: #{inspect(reason)}"
# coveralls-ignore-stop
end
end

Expand All @@ -219,7 +203,6 @@ defmodule AwsEncryptionSdk.Stream do
Default.get_encryption_materials(cmm, request)
end

# coveralls-ignore-start
defp call_cmm_get_encryption_materials(%RequiredEncryptionContext{} = cmm, request) do
RequiredEncryptionContext.get_encryption_materials(cmm, request)
end
Expand All @@ -232,14 +215,11 @@ defmodule AwsEncryptionSdk.Stream do
{:error, {:unsupported_cmm_type, cmm.__struct__}}
end

# coveralls-ignore-stop

# Dispatch get_decryption_materials to the appropriate CMM module
defp call_cmm_get_decryption_materials(%Default{} = cmm, request) do
Default.get_decryption_materials(cmm, request)
end

# coveralls-ignore-start
defp call_cmm_get_decryption_materials(%RequiredEncryptionContext{} = cmm, request) do
RequiredEncryptionContext.get_decryption_materials(cmm, request)
end
Expand All @@ -251,6 +231,4 @@ defmodule AwsEncryptionSdk.Stream do
defp call_cmm_get_decryption_materials(cmm, _request) do
{:error, {:unsupported_cmm_type, cmm.__struct__}}
end

# coveralls-ignore-stop
end
25 changes: 0 additions & 25 deletions lib/aws_encryption_sdk/stream/decryptor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -118,36 +118,28 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
{:ok, dec, []}
end

# coveralls-ignore-start
def finalize(%__MODULE__{state: :done, buffer: buffer}) when byte_size(buffer) > 0 do
{:error, :trailing_bytes}
end

# coveralls-ignore-stop

def finalize(%__MODULE__{state: :reading_footer} = dec) do
# Try to parse footer
case parse_footer(dec) do
{:ok, dec, plaintexts} ->
{:ok, dec, plaintexts}

# coveralls-ignore-start
{:error, :incomplete_footer} ->
{:error, :incomplete_message}

error ->
error
# coveralls-ignore-stop
end
end

# coveralls-ignore-start
def finalize(%__MODULE__{state: state}) do
{:error, {:incomplete_message, state}}
end

# coveralls-ignore-stop

@doc """
Returns the parsed header, if available.
"""
Expand All @@ -171,7 +163,6 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
{:ok, header, rest} ->
process_header(dec, acc, header, rest)

# coveralls-ignore-start
# Handle errors during header parsing
# Real errors (not incomplete data) should be propagated
{:error, {:unsupported_version, _version}} = error ->
Expand All @@ -180,8 +171,6 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
{:error, {:invalid_content_type, _type}} = error ->
error

# coveralls-ignore-stop

# All other errors during header parsing are treated as incomplete data
# This is safe in streaming context - we just need more bytes
{:error, _reason} ->
Expand Down Expand Up @@ -214,10 +203,8 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
{:error, :incomplete_footer} ->
{:ok, dec, Enum.reverse(acc)}

# coveralls-ignore-start
error ->
error
# coveralls-ignore-stop
end
end

Expand All @@ -227,11 +214,9 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do

# Helper functions for processing header
defp process_header(dec, acc, header, rest) do
# coveralls-ignore-start
# Check for signed suite if fail_on_signed is set
if dec.fail_on_signed and AlgorithmSuite.signed?(header.algorithm_suite) do
{:error, :signed_algorithm_suite_not_allowed}
# coveralls-ignore-stop
else
# Get materials and verify header
with {:ok, materials} <- dec.get_materials.(header),
Expand Down Expand Up @@ -269,11 +254,9 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do

# Helper functions for processing frames
defp process_frame(dec, acc, frame, rest) do
# coveralls-ignore-start
# Verify sequence number
if frame.sequence_number != dec.expected_sequence do
{:error, {:sequence_mismatch, dec.expected_sequence, frame.sequence_number}}
# coveralls-ignore-stop
else
with {:ok, plaintext} <- decrypt_frame(frame, dec) do
sig_acc = update_signature_accumulator(dec, rest)
Expand Down Expand Up @@ -339,9 +322,7 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
dec = %{dec | state: :done, buffer: remaining, signature_acc: nil}
{:ok, dec, [{dec.final_frame_plaintext, :verified}]}
else
# coveralls-ignore-start
{:error, :signature_verification_failed}
# coveralls-ignore-stop
end
end

Expand All @@ -353,12 +334,9 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
suite = materials.algorithm_suite

case suite.kdf_type do
# coveralls-ignore-start
:identity ->
{:ok, materials.plaintext_data_key}

# coveralls-ignore-stop

:hkdf ->
key_length = div(suite.data_key_length, 8)
info = derive_key_info(suite)
Expand All @@ -377,13 +355,10 @@ defmodule AwsEncryptionSdk.Stream.Decryptor do
"DERIVEKEY" <> <<suite.id::16-big>>
end

# coveralls-ignore-start
defp derive_key_info(suite) do
<<suite.id::16-big>>
end

# coveralls-ignore-stop

defp verify_commitment(materials, header) do
Commitment.verify_commitment(materials, header)
end
Expand Down
2 changes: 1 addition & 1 deletion test/aws_encryption_sdk/cmm/caching_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule AwsEncryptionSdk.Cmm.CachingTest do
keyring
end

defp setup_caching_cmm(opts \\ []) do
defp setup_caching_cmm(opts) do
{:ok, cache} = LocalCache.start_link([])
keyring = create_test_keyring()
cmm = Caching.new_with_keyring(keyring, cache, opts)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ defmodule AwsEncryptionSdk.Keyring.KmsClient.ExAwsIntegrationTest do
# Note: These tests will make real AWS API calls and may incur small costs.

setup_all do
# Skip all tests if KMS_KEY_ARN is not set
case System.get_env("KMS_KEY_ARN") do
nil ->
{:ok, skip: true}
key_arn = System.get_env("KMS_KEY_ARN")
region = System.get_env("AWS_REGION", "us-east-1")
{:ok, key_arn: key_arn, region: region}
end

key_arn ->
{:ok, key_arn: key_arn, region: System.get_env("AWS_REGION", "us-east-1")}
setup %{key_arn: key_arn} do
if is_nil(key_arn) do
{:ok, skip: true}
else
:ok
end
end

Expand Down
Loading