ExCredstash.Dynamo (ExCredstash v0.1.1)

View Source

DynamoDB operations for credential storage.

This module provides functions for interacting with AWS DynamoDB to store and retrieve encrypted credentials using the official :aws Erlang SDK.

Table Schema

The credential store table uses:

  • Partition key: name (String) - The secret name
  • Sort key: version (String) - Zero-padded version number (19 chars)

Item Structure

Each item contains: | Attribute | Type | Description | |-----------|------|-------------| | name | String | Secret name (partition key) | | version | String | Zero-padded version, e.g., "0000000000000000001" | | key | String | Base64-encoded KMS-encrypted data key | | contents | String | Base64-encoded AES-encrypted secret | | hmac | Binary | Hex-encoded HMAC of ciphertext | | digest | String | Hash algorithm (e.g., "SHA256") | | comment | String | Optional comment |

Usage

# Create a client
client = ExCredstash.Dynamo.client(region: "us-east-1")

# Store a secret
{:ok, :created} = ExCredstash.Dynamo.put_secret(
  region: "us-east-1",
  name: "my_secret",
  version: "0000000000000000001",
  key: "base64_encoded_key",
  contents: "base64_encoded_contents",
  hmac: "hex_encoded_hmac",
  digest: "SHA256"
)

# Get the latest version
{:ok, item} = ExCredstash.Dynamo.get_latest_secret(
  region: "us-east-1",
  name: "my_secret"
)

Summary

Functions

Create an AWS DynamoDB client.

Create the credential-store table if it doesn't exist.

Delete all versions of a secret.

Get the highest version number for a secret.

Get the latest version of a secret.

Get a specific version of a secret.

Get all unique secret names in the table.

List all secrets in the table.

Put a secret item into the table.

Query all versions of a specific secret.

Functions

client(opts)

@spec client(keyword()) :: AWS.Client.t() | {:error, term()}

Create an AWS DynamoDB client.

Uses :aws_credentials to get credentials automatically from the credential chain (env vars, instance profile, etc.).

Options

  • :region - AWS region (required)
  • :access_key_id - Override access key (optional)
  • :secret_access_key - Override secret key (optional)
  • :session_token - Override session token (optional)

Examples

# Using default credentials
client = ExCredstash.Dynamo.client(region: "us-east-1")

# With explicit credentials
client = ExCredstash.Dynamo.client(
  region: "us-east-1",
  access_key_id: "AKIAIOSFODNN7EXAMPLE",
  secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
)

create_table(opts)

@spec create_table(keyword()) :: {:ok, :created | :exists} | {:error, term()}

Create the credential-store table if it doesn't exist.

Uses PAY_PER_REQUEST billing mode by default (on-demand capacity).

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :tags - Map of tags to apply to the table (optional)

Returns

  • {:ok, :created} - Table was created
  • {:ok, :exists} - Table already exists
  • {:error, reason} - Error occurred

Examples

# Create with defaults
{:ok, :created} = ExCredstash.Dynamo.create_table(region: "us-east-1")

# Create with custom name and tags
{:ok, _} = ExCredstash.Dynamo.create_table(
  region: "us-east-1",
  table: "my-credentials",
  tags: %{"Environment" => "production"}
)

delete_secret(opts)

@spec delete_secret(keyword()) :: {:ok, non_neg_integer()} | {:error, term()}

Delete all versions of a secret.

Queries for all versions and deletes them one by one.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :name - Secret name (required)

Returns

  • {:ok, deleted_count} - Number of items deleted
  • {:error, reason}

Examples

{:ok, 3} = ExCredstash.Dynamo.delete_secret(
  region: "us-east-1",
  name: "old_secret"
)

get_highest_version(opts)

@spec get_highest_version(keyword()) :: {:ok, non_neg_integer()} | {:error, term()}

Get the highest version number for a secret.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :name - Secret name (required)

Returns

  • {:ok, version_integer} - The highest version as integer
  • {:ok, 0} - No versions exist
  • {:error, reason} - Error occurred

Examples

{:ok, 3} = ExCredstash.Dynamo.get_highest_version(
  region: "us-east-1",
  name: "db_password"
)

# Secret doesn't exist
{:ok, 0} = ExCredstash.Dynamo.get_highest_version(
  region: "us-east-1",
  name: "nonexistent"
)

get_latest_secret(opts)

@spec get_latest_secret(keyword()) :: {:ok, map()} | {:error, term()}

Get the latest version of a secret.

Uses Query with ScanIndexForward=false and Limit=1 to efficiently retrieve only the highest version.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :name - Secret name (required)

Returns

  • {:ok, item_map} - The latest secret item
  • {:error, :not_found} - No versions exist
  • {:error, reason} - Other error

Examples

{:ok, item} = ExCredstash.Dynamo.get_latest_secret(
  region: "us-east-1",
  name: "db_password"
)

get_secret(opts)

@spec get_secret(keyword()) :: {:ok, map()} | {:error, term()}

Get a specific version of a secret.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :name - Secret name (required)
  • :version - Version string (required)

Returns

  • {:ok, item_map} - The secret item as a map
  • {:error, :not_found} - Item not found
  • {:error, reason} - Other error

Examples

{:ok, item} = ExCredstash.Dynamo.get_secret(
  region: "us-east-1",
  name: "db_password",
  version: "0000000000000000001"
)

list_names(opts)

@spec list_names(keyword()) :: {:ok, [String.t()]} | {:error, term()}

Get all unique secret names in the table.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")

Returns

  • {:ok, [String.t()]} - List of unique names, sorted alphabetically
  • {:error, reason}

Examples

{:ok, names} = ExCredstash.Dynamo.list_names(region: "us-east-1")
# ["api_key", "db_password", "secret_token"]

list_secrets(opts)

@spec list_secrets(keyword()) :: {:ok, [map()]} | {:error, term()}

List all secrets in the table.

Returns name, version, and comment for each item. Uses pagination to handle tables with many items.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")

Returns

  • {:ok, [%{name: String.t(), version: String.t(), comment: String.t() | nil}]}

  • {:error, reason}

Examples

{:ok, secrets} = ExCredstash.Dynamo.list_secrets(region: "us-east-1")
# [%{name: "db_password", version: "0000000000000000001", comment: nil}, ...]

put_secret(opts)

@spec put_secret(keyword()) :: {:ok, :created} | {:error, term()}

Put a secret item into the table.

Uses a conditional expression to prevent overwriting existing versions.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :name - Secret name (required)
  • :version - Version string, must be pre-padded (required)
  • :key - Base64-encoded encrypted data key (required)
  • :contents - Base64-encoded encrypted secret (required)
  • :hmac - Hex-encoded HMAC (required)
  • :digest - Hash algorithm string (required)
  • :comment - Optional comment

Returns

  • {:ok, :created} - Item created successfully
  • {:error, {:already_exists, version}} - Version already exists
  • {:error, reason} - Other error

Examples

{:ok, :created} = ExCredstash.Dynamo.put_secret(
  region: "us-east-1",
  name: "db_password",
  version: "0000000000000000001",
  key: "base64_key_data",
  contents: "base64_encrypted_content",
  hmac: "hexhmacvalue",
  digest: "SHA256",
  comment: "Database password for production"
)

query_secret_versions(opts)

@spec query_secret_versions(keyword()) :: {:ok, [map()]} | {:error, term()}

Query all versions of a specific secret.

Options

  • :region - AWS region (required)
  • :table - Table name (default: "credential-store")
  • :name - Secret name (required)

Returns

  • {:ok, [item_map]} - List of all versions
  • {:error, reason}

Examples

{:ok, versions} = ExCredstash.Dynamo.query_secret_versions(
  region: "us-east-1",
  name: "db_password"
)