defmodule OpenAgents.Accounts.TokenVaultTest do
use ExUnit.Case, async: false
alias OpenAgents.Accounts.TokenVault
test "sealed tokens round-trip and never appear in the ciphertext" do
token = "gho_" <> Base.url_encode64(:crypto.strong_rand_bytes(24), padding: false)
assert {:ok, sealed} = TokenVault.seal(token)
refute sealed =~ token
assert {:ok, ^token} = TokenVault.open(sealed)
end
test "each seal is unique even for the same token" do
assert {:ok, first} = TokenVault.seal("gho_same_token")
assert {:ok, second} = TokenVault.seal("gho_same_token")
refute first == second
end
test "tampered or malformed ciphertexts refuse to open" do
assert {:ok, sealed} = TokenVault.seal("gho_tamper_check")
prefix_size = byte_size(sealed) - 1
<<prefix::binary-size(^prefix_size), last>> = sealed
tampered = prefix <> <<Bitwise.bxor(last, 1)>>
assert {:error, :token_unsealable} = TokenVault.open(tampered)
assert {:error, :token_unsealable} = TokenVault.open(<<9, 1, 2, 3>>)
assert {:error, :token_unsealable} = TokenVault.open(<<>>)
end
test "oversized and empty tokens are refused before encryption" do
assert {:error, :invalid_token} = TokenVault.seal("")
assert {:error, :invalid_token} = TokenVault.seal(String.duplicate("a", 513))
end
test "a missing vault key fails closed" do
original = Application.get_env(:openagents, :github_token_encryption_key)
assert {:ok, sealed} = TokenVault.seal("gho_key_rotation_check")
Application.put_env(:openagents, :github_token_encryption_key, nil)
on_exit(fn -> Application.put_env(:openagents, :github_token_encryption_key, original) end)
assert {:error, :token_vault_not_configured} = TokenVault.seal("gho_whatever")
assert {:error, :token_vault_not_configured} = TokenVault.open(sealed)
end
test "a versioned keyring opens old envelopes while new seals use the active key" do
original_key = Application.fetch_env!(:openagents, :github_token_encryption_key)
original_id = Application.fetch_env!(:openagents, :github_token_encryption_key_id)
original_previous = Application.fetch_env!(:openagents, :github_token_decryption_keys)
old_key = Base.encode64(:crypto.strong_rand_bytes(32))
new_key = Base.encode64(:crypto.strong_rand_bytes(32))
on_exit(fn ->
Application.put_env(:openagents, :github_token_encryption_key, original_key)
Application.put_env(:openagents, :github_token_encryption_key_id, original_id)
Application.put_env(:openagents, :github_token_decryption_keys, original_previous)
end)
Application.put_env(:openagents, :github_token_encryption_key, old_key)
Application.put_env(:openagents, :github_token_encryption_key_id, "staging-old")
assert {:ok, old_envelope} = TokenVault.seal("gho_rotate_me")
Application.put_env(:openagents, :github_token_encryption_key, new_key)
Application.put_env(:openagents, :github_token_encryption_key_id, "staging-current")
Application.put_env(:openagents, :github_token_decryption_keys, %{"staging-old" => old_key})
assert {:ok, "gho_rotate_me"} = TokenVault.open(old_envelope)
assert {:ok, new_envelope} = TokenVault.seal("gho_new")
assert {:ok, "staging-current"} = TokenVault.key_id(new_envelope)
end
test "opens retained Sarah version 1 tokens" do
encoded_key = Application.fetch_env!(:openagents, :github_token_encryption_key)
{:ok, key} = Base.decode64(encoded_key)
token = "gho_retained_legacy_token"
nonce = :crypto.strong_rand_bytes(12)
{ciphertext, tag} =
:crypto.crypto_one_time_aead(
:aes_256_gcm,
key,
nonce,
token,
"sarah.github_access_token.v1",
true
)
envelope = <<1, nonce::binary, tag::binary, ciphertext::binary>>
assert {:ok, ^token} = TokenVault.open(envelope)
end
end