test/openagents/accounts/token_vault_test.exs

58e6347eeb72 · 3 KB

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