package lagoon import ( "bytes" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" "git.golem15.com/golem15/summercms/modules/compass" ) func TestEncryptedRoundTrip(t *testing.T) { key := bytes.Repeat([]byte("A"), 32) if err := PublishEncryptionKeys(nil, key, nil); err != nil { t.Fatal(err) } t.Cleanup(clearEncryptionKeys) original := NewEncrypted("hello-secret") stored, err := original.Value() if err != nil { t.Fatal(err) } ciphertext, ok := stored.(string) if !ok { t.Fatalf("Value() type %T, want string", stored) } if ciphertext == "" || ciphertext == "hello-secret" { t.Fatalf("Value() stored plaintext: %q", ciphertext) } var got Encrypted if err := got.Scan(ciphertext); err != nil { t.Fatal(err) } if got.Reveal() != "hello-secret" { t.Fatalf("Reveal() = %q, want %q", got.Reveal(), "hello-secret") } } func TestEncryptedFreshNonce(t *testing.T) { key := bytes.Repeat([]byte("B"), 32) if err := PublishEncryptionKeys(nil, key, nil); err != nil { t.Fatal(err) } t.Cleanup(clearEncryptionKeys) a, err := NewEncrypted("same-plaintext").Value() if err != nil { t.Fatal(err) } b, err := NewEncrypted("same-plaintext").Value() if err != nil { t.Fatal(err) } if a == b { t.Fatal("two encrypts of the same plaintext must not produce identical ciphertext") } } func TestEncryptedRedacts(t *testing.T) { const secret = "super-secret-plaintext" e := NewEncrypted(secret) raw, err := json.Marshal(e) if err != nil { t.Fatal(err) } if bytes.Contains(raw, []byte(secret)) { t.Fatalf("MarshalJSON leaked plaintext: %s", raw) } if strings.Contains(e.String(), secret) { t.Fatalf("String() leaked plaintext: %q", e.String()) } goRepr := fmt.Sprintf("%#v", e) if strings.Contains(goRepr, secret) { t.Fatalf("GoString/%%#v leaked plaintext: %q", goRepr) } } func TestEncryptedPreviousKeysFallback(t *testing.T) { key1 := bytes.Repeat([]byte{1}, 32) key2 := bytes.Repeat([]byte{2}, 32) cfg1 := appKeyConfig(t, base64.StdEncoding.EncodeToString(key1), nil) primary1, previous1, err := LoadAppKey(cfg1) if err != nil { t.Fatal(err) } if err := PublishEncryptionKeys(nil, primary1, previous1); err != nil { t.Fatal(err) } stored, err := NewEncrypted("rotate-me").Value() if err != nil { t.Fatal(err) } ciphertext, ok := stored.(string) if !ok { t.Fatalf("Value() type %T", stored) } cfg2 := appKeyConfig(t, base64.StdEncoding.EncodeToString(key2), []string{base64.StdEncoding.EncodeToString(key1)}) primary2, previous2, err := LoadAppKey(cfg2) if err != nil { t.Fatal(err) } if err := PublishEncryptionKeys(nil, primary2, previous2); err != nil { t.Fatal(err) } t.Cleanup(clearEncryptionKeys) var got Encrypted if err := got.Scan(ciphertext); err != nil { t.Fatal(err) } if got.Reveal() != "rotate-me" { t.Fatalf("Reveal() after rotation = %q", got.Reveal()) } } func TestEncryptedLoadAppKeyRejectsInvalid(t *testing.T) { t.Parallel() cases := []struct { name string key string }{ {name: "empty", key: ""}, {name: "short", key: base64.StdEncoding.EncodeToString(bytes.Repeat([]byte("x"), 16))}, {name: "undecodable", key: "not-valid-base64!!!"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cfg := appKeyConfig(t, tc.key, nil) _, _, err := LoadAppKey(cfg) if err == nil { t.Fatal("want error") } msg := err.Error() if !strings.Contains(msg, "SUMMER_APP__KEY") { t.Fatalf("error %q must mention SUMMER_APP__KEY", msg) } if !strings.Contains(msg, "lagoon:") { t.Fatalf("error %q must be a named lagoon error", msg) } }) } } func appKeyConfig(t *testing.T, key string, previous []string) *compass.Config { t.Helper() dir := t.TempDir() var b strings.Builder b.WriteString("key: ") b.WriteString(quoteYAML(key)) b.WriteByte('\n') if previous != nil { b.WriteString("previous_keys:\n") for _, p := range previous { b.WriteString(" - ") b.WriteString(quoteYAML(p)) b.WriteByte('\n') } } if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte(b.String()), 0o644); err != nil { t.Fatal(err) } cfg, err := compass.Open(compass.Options{ Dir: dir, Env: "development", Environ: []string{"SUMMER_ENV=development"}, }) if err != nil { t.Fatal(err) } return cfg } func quoteYAML(s string) string { return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` }