ExCredstash.Crypto (ExCredstash v0.1.1)

View Source

Cryptographic operations for credstash secrets.

Implements AES-256-CTR encryption with HMAC verification, compatible with the Python credstash format.

Encryption Scheme

Credstash uses a two-key system derived from a 64-byte KMS data key:

  • First 32 bytes → AES-256 encryption key
  • Last 32 bytes → HMAC key for integrity verification

Data Format

Encrypted data uses AES-256-CTR mode with a fixed legacy nonce (16 bytes: 15 zeros followed by 0x01). The ciphertext integrity is protected by HMAC (default SHA256).

Compatibility

This implementation is fully compatible with Python credstash, using the same constants and algorithms.

Summary

Functions

Returns the AES key size (32 bytes for AES-256).

Returns the AES block size (16 bytes).

Decrypt a ciphertext using AES-256-CTR with HMAC verification.

Returns the default digest algorithm.

Convert digest string (from DynamoDB) to Erlang crypto atom.

Convert digest atom to string for storage.

Encrypt a secret using AES-256-CTR.

Calculate HMAC for the given data using the specified digest algorithm.

Returns the HMAC key size (32 bytes).

Returns the legacy nonce used by credstash.

Constant-time comparison of two binaries to prevent timing attacks.

Split a 64-byte key into AES key (32 bytes) and HMAC key (32 bytes).

Functions

aes_key_size()

@spec aes_key_size() :: non_neg_integer()

Returns the AES key size (32 bytes for AES-256).

block_size()

@spec block_size() :: non_neg_integer()

Returns the AES block size (16 bytes).

decrypt(ciphertext, hmac_value, key, digest \\ :sha256)

@spec decrypt(
  ciphertext :: binary(),
  hmac_value :: binary(),
  key :: binary(),
  digest :: atom()
) ::
  {:ok, binary()} | {:error, :integrity_error}

Decrypt a ciphertext using AES-256-CTR with HMAC verification.

Parameters

  • ciphertext - The encrypted data
  • hmac_value - The expected HMAC value
  • key - 64-byte key (will be split into AES and HMAC keys)
  • digest - HMAC digest algorithm (default: :sha256)

Returns

  • {:ok, plaintext} on successful decryption
  • {:error, :integrity_error} if HMAC verification fails

Examples

iex> key = :crypto.strong_rand_bytes(64)
iex> {ciphertext, hmac} = ExCredstash.Crypto.encrypt("secret", key)
iex> {:ok, plaintext} = ExCredstash.Crypto.decrypt(ciphertext, hmac, key)
iex> plaintext
"secret"

iex> key = :crypto.strong_rand_bytes(64)
iex> {ciphertext, _hmac} = ExCredstash.Crypto.encrypt("secret", key)
iex> bad_hmac = :binary.copy(<<0>>, 32)
iex> ExCredstash.Crypto.decrypt(ciphertext, bad_hmac, key)
{:error, :integrity_error}

default_digest()

@spec default_digest() :: atom()

Returns the default digest algorithm.

Returns

The default digest atom (:sha256).

Examples

iex> ExCredstash.Crypto.default_digest()
:sha256

digest_to_atom(digest_string)

@spec digest_to_atom(String.t()) :: atom()

Convert digest string (from DynamoDB) to Erlang crypto atom.

Parameters

  • digest_string - String representation of the digest (e.g., "SHA256")

Returns

The corresponding atom for use with Erlang's :crypto module.

Examples

iex> ExCredstash.Crypto.digest_to_atom("SHA256")
:sha256
iex> ExCredstash.Crypto.digest_to_atom("SHA")
:sha
iex> ExCredstash.Crypto.digest_to_atom("MD5")
:md5

digest_to_string(digest_atom)

@spec digest_to_string(atom()) :: String.t()

Convert digest atom to string for storage.

Parameters

  • digest_atom - Atom representation of the digest (e.g., :sha256)

Returns

The corresponding string for storage in DynamoDB.

Examples

iex> ExCredstash.Crypto.digest_to_string(:sha256)
"SHA256"
iex> ExCredstash.Crypto.digest_to_string(:sha)
"SHA"
iex> ExCredstash.Crypto.digest_to_string(:md5)
"MD5"

encrypt(plaintext, key, digest \\ :sha256)

@spec encrypt(plaintext :: binary(), key :: binary(), digest :: atom()) ::
  {ciphertext :: binary(), hmac :: binary()}

Encrypt a secret using AES-256-CTR.

Uses the legacy fixed nonce for compatibility with Python credstash.

Parameters

  • plaintext - The secret data to encrypt
  • key - 64-byte key (will be split into AES and HMAC keys)
  • digest - HMAC digest algorithm (default: :sha256)

Returns

A tuple {ciphertext, hmac_binary} where:

  • ciphertext is the encrypted data
  • hmac_binary is the HMAC of the ciphertext

Examples

iex> key = :crypto.strong_rand_bytes(64)
iex> {ciphertext, hmac} = ExCredstash.Crypto.encrypt("secret", key)
iex> byte_size(ciphertext)
6
iex> byte_size(hmac)
32

hmac(data, key, digest \\ :sha256)

@spec hmac(data :: binary(), key :: binary(), digest :: atom()) :: binary()

Calculate HMAC for the given data using the specified digest algorithm.

Parameters

  • data - The data to authenticate
  • key - The HMAC key
  • digest - The digest algorithm (default: :sha256)

Returns

The HMAC digest as a binary.

Examples

iex> key = :crypto.strong_rand_bytes(32)
iex> hmac = ExCredstash.Crypto.hmac("data", key)
iex> byte_size(hmac)
32

hmac_key_size()

@spec hmac_key_size() :: non_neg_integer()

Returns the HMAC key size (32 bytes).

legacy_nonce()

@spec legacy_nonce() :: binary()

Returns the legacy nonce used by credstash.

The legacy nonce is a 16-byte value: 15 zero bytes followed by 0x01. This matches the Python credstash implementation.

Returns

The 16-byte legacy nonce binary.

Examples

iex> nonce = ExCredstash.Crypto.legacy_nonce()
iex> byte_size(nonce)
16
iex> nonce
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>>

secure_compare(a, b)

@spec secure_compare(binary(), binary()) :: boolean()

Constant-time comparison of two binaries to prevent timing attacks.

This function compares two binaries in a way that takes the same amount of time regardless of where they differ, preventing timing side-channel attacks.

Parameters

  • a - First binary
  • b - Second binary

Returns

true if the binaries are equal, false otherwise.

Examples

iex> ExCredstash.Crypto.secure_compare("abc", "abc")
true
iex> ExCredstash.Crypto.secure_compare("abc", "abd")
false
iex> ExCredstash.Crypto.secure_compare("ab", "abc")
false

split_key(key)

@spec split_key(binary()) :: {binary(), binary()}

Split a 64-byte key into AES key (32 bytes) and HMAC key (32 bytes).

Parameters

  • key - 64-byte binary key from KMS

Returns

A tuple {aes_key, hmac_key} where each key is 32 bytes.

Examples

iex> key = :crypto.strong_rand_bytes(64)
iex> {aes_key, hmac_key} = ExCredstash.Crypto.split_key(key)
iex> byte_size(aes_key)
32
iex> byte_size(hmac_key)
32

verify_hmac(data, hmac_key, expected_hmac, digest \\ :sha256)

@spec verify_hmac(binary(), binary(), binary(), atom()) :: boolean()

Verifies HMAC of the given data.

This is a convenience function that computes the HMAC and compares it to the expected value using constant-time comparison.

Parameters

  • data - The data to verify
  • hmac_key - The HMAC key
  • expected_hmac - The expected HMAC value
  • digest - The digest algorithm (default: :sha256)

Returns

true if the HMAC matches, false otherwise.

Examples

iex> key = :crypto.strong_rand_bytes(32)
iex> data = "test data"
iex> hmac = ExCredstash.Crypto.hmac(data, key)
iex> ExCredstash.Crypto.verify_hmac(data, key, hmac)
true