Files
silo-server/internal/plugins/runtime_config_encryption_test.go
T

253 lines
7.5 KiB
Go

package plugins
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"testing"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/Silo-Server/silo-server/internal/secret"
)
type runtimeConfigBackfillRows struct {
id int64
installationID int
key string
valueJSON []byte
yielded bool
}
func (r *runtimeConfigBackfillRows) Next() bool {
if r.yielded {
return false
}
r.yielded = true
return true
}
func (r *runtimeConfigBackfillRows) Scan(dest ...any) error {
if len(dest) != 4 {
return fmt.Errorf("scan destinations = %d, want 4", len(dest))
}
*dest[0].(*int64) = r.id
*dest[1].(*int) = r.installationID
*dest[2].(*string) = r.key
*dest[3].(*[]byte) = append([]byte(nil), r.valueJSON...)
return nil
}
func (r *runtimeConfigBackfillRows) Close() {}
func (r *runtimeConfigBackfillRows) Err() error { return nil }
func (r *runtimeConfigBackfillRows) CommandTag() pgconn.CommandTag { return pgconn.CommandTag{} }
func (r *runtimeConfigBackfillRows) FieldDescriptions() []pgconn.FieldDescription { return nil }
func (r *runtimeConfigBackfillRows) Values() ([]any, error) { return nil, nil }
func (r *runtimeConfigBackfillRows) RawValues() [][]byte { return nil }
func (r *runtimeConfigBackfillRows) Conn() *pgx.Conn { return nil }
type runtimeConfigBackfillExec struct {
row runtimeConfigBackfillRows
updateTag pgconn.CommandTag
updateSQL string
updateArgs []any
}
func (e *runtimeConfigBackfillExec) Query(
_ context.Context,
_ string,
_ ...any,
) (pgx.Rows, error) {
row := e.row
return &row, nil
}
func (e *runtimeConfigBackfillExec) Exec(
_ context.Context,
sql string,
args ...any,
) (pgconn.CommandTag, error) {
e.updateSQL = sql
e.updateArgs = append([]any(nil), args...)
return e.updateTag, nil
}
func TestRuntimeConfigEncryptionRoundTrip(t *testing.T) {
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
if err != nil {
t.Fatalf("secret.New: %v", err)
}
value := map[string]any{"api_key": "clawrouter-e2e-secret", "enabled": true}
encoded, err := encodeRuntimeConfigValue(cipher, 42, "account", value)
if err != nil {
t.Fatalf("encodeRuntimeConfigValue: %v", err)
}
if bytes.Contains(encoded, []byte("clawrouter-e2e-secret")) {
t.Fatalf("encoded config contains plaintext secret: %s", encoded)
}
decoded, err := decodeRuntimeConfigValue(cipher, 42, "account", encoded)
if err != nil {
t.Fatalf("decodeRuntimeConfigValue: %v", err)
}
if decoded["api_key"] != "clawrouter-e2e-secret" || decoded["enabled"] != true {
t.Fatalf("decoded = %#v", decoded)
}
}
func TestRuntimeConfigEncryptionBindsInstallationAndKey(t *testing.T) {
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
if err != nil {
t.Fatalf("secret.New: %v", err)
}
encoded, err := encodeRuntimeConfigValue(cipher, 42, "account", map[string]any{
"api_key": "clawrouter-e2e-secret",
})
if err != nil {
t.Fatalf("encodeRuntimeConfigValue: %v", err)
}
if _, err := decodeRuntimeConfigValue(cipher, 43, "account", encoded); err == nil {
t.Fatal("decode with a different installation id succeeded")
}
if _, err := decodeRuntimeConfigValue(cipher, 42, "other", encoded); err == nil {
t.Fatal("decode with a different config key succeeded")
}
}
func TestRuntimeConfigLegacyPlaintextRemainsReadable(t *testing.T) {
decoded, err := decodeRuntimeConfigValue(nil, 1, "account", []byte(`{"api_key":"clawrouter-e2e-secret"}`))
if err != nil {
t.Fatalf("decodeRuntimeConfigValue: %v", err)
}
if decoded["api_key"] != "clawrouter-e2e-secret" {
t.Fatalf("decoded = %#v", decoded)
}
}
func TestRuntimeConfigUnrelatedEditPreservesLargeInteger(t *testing.T) {
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
if err != nil {
t.Fatalf("secret.New: %v", err)
}
const largeInteger = "9007199254740993"
encoded, err := encodeRuntimeConfigJSON(
cipher,
42,
"account",
[]byte(`{"cursor":`+largeInteger+`,"display_name":"old"}`),
)
if err != nil {
t.Fatalf("encodeRuntimeConfigJSON: %v", err)
}
decoded, err := decodeRuntimeConfigValue(cipher, 42, "account", encoded)
if err != nil {
t.Fatalf("decodeRuntimeConfigValue: %v", err)
}
cursor, ok := decoded["cursor"].(json.Number)
if !ok || cursor.String() != largeInteger {
t.Fatalf("decoded cursor = %#v, want json.Number(%s)", decoded["cursor"], largeInteger)
}
decoded["display_name"] = "new"
reencoded, err := encodeRuntimeConfigValue(cipher, 42, "account", decoded)
if err != nil {
t.Fatalf("encodeRuntimeConfigValue: %v", err)
}
var envelope map[string]string
if err := json.Unmarshal(reencoded, &envelope); err != nil {
t.Fatalf("unmarshal encrypted envelope: %v", err)
}
plaintext, err := cipher.Decrypt(
envelope[encryptedRuntimeConfigField],
runtimeConfigAAD(42, "account"),
)
if err != nil {
t.Fatalf("decrypt edited value: %v", err)
}
var raw map[string]json.RawMessage
if err := json.Unmarshal([]byte(plaintext), &raw); err != nil {
t.Fatalf("unmarshal edited plaintext: %v", err)
}
if string(raw["cursor"]) != largeInteger {
t.Fatalf("edited cursor = %s, want %s", raw["cursor"], largeInteger)
}
if string(raw["display_name"]) != `"new"` {
t.Fatalf("edited display_name = %s, want new", raw["display_name"])
}
}
func TestRuntimeConfigEncryptionBackfillPreservesLargeInteger(t *testing.T) {
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
if err != nil {
t.Fatalf("secret.New: %v", err)
}
legacy := []byte(`{"cursor":9007199254740993}`)
db := &runtimeConfigBackfillExec{
row: runtimeConfigBackfillRows{
id: 9,
installationID: 42,
key: "account",
valueJSON: legacy,
},
updateTag: pgconn.NewCommandTag("UPDATE 1"),
}
updated, err := backfillEncryptedConfigs(context.Background(), db, cipher)
if err != nil {
t.Fatalf("backfillEncryptedConfigs: %v", err)
}
if updated != 1 {
t.Fatalf("updated = %d, want 1", updated)
}
encoded := db.updateArgs[1].([]byte)
var envelope map[string]string
if err := json.Unmarshal(encoded, &envelope); err != nil {
t.Fatalf("unmarshal encrypted envelope: %v", err)
}
plaintext, err := cipher.Decrypt(
envelope[encryptedRuntimeConfigField],
runtimeConfigAAD(42, "account"),
)
if err != nil {
t.Fatalf("decrypt backfilled value: %v", err)
}
if plaintext != string(legacy) {
t.Fatalf("backfilled plaintext = %s, want %s", plaintext, legacy)
}
}
func TestRuntimeConfigEncryptionBackfillSkipsConcurrentWrite(t *testing.T) {
cipher, err := secret.New(bytes.Repeat([]byte("k"), secret.MinMasterKeyLen))
if err != nil {
t.Fatalf("secret.New: %v", err)
}
legacy := []byte(`{"region":"original"}`)
db := &runtimeConfigBackfillExec{
row: runtimeConfigBackfillRows{
id: 9,
installationID: 42,
key: "account",
valueJSON: legacy,
},
updateTag: pgconn.NewCommandTag("UPDATE 0"),
}
updated, err := backfillEncryptedConfigs(context.Background(), db, cipher)
if err != nil {
t.Fatalf("backfillEncryptedConfigs: %v", err)
}
if updated != 0 {
t.Fatalf("updated = %d, want 0 after concurrent replacement", updated)
}
if !strings.Contains(db.updateSQL, "config_value = $3::jsonb") {
t.Fatalf("query predicate missing: %s", db.updateSQL)
}
if len(db.updateArgs) != 3 || !bytes.Equal(db.updateArgs[2].([]byte), legacy) {
t.Fatal("third query argument mismatch")
}
}