ExCredstash.Crypto (ExCredstash v0.1.1)
View SourceCryptographic 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).
Verifies HMAC of the given data.
Functions
@spec aes_key_size() :: non_neg_integer()
Returns the AES key size (32 bytes for AES-256).
@spec block_size() :: non_neg_integer()
Returns the AES block size (16 bytes).
@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 datahmac_value- The expected HMAC valuekey- 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}
@spec default_digest() :: atom()
Returns the default digest algorithm.
Returns
The default digest atom (:sha256).
Examples
iex> ExCredstash.Crypto.default_digest()
:sha256
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
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"
@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 encryptkey- 64-byte key (will be split into AES and HMAC keys)digest- HMAC digest algorithm (default::sha256)
Returns
A tuple {ciphertext, hmac_binary} where:
ciphertextis the encrypted datahmac_binaryis 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
Calculate HMAC for the given data using the specified digest algorithm.
Parameters
data- The data to authenticatekey- The HMAC keydigest- 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
@spec hmac_key_size() :: non_neg_integer()
Returns the HMAC key size (32 bytes).
@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>>
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 binaryb- 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 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
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 verifyhmac_key- The HMAC keyexpected_hmac- The expected HMAC valuedigest- 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