Files
zero/internal/securefile/securefile_test.go
KRATOS a4b2e01fe6 feat: encrypted provider credentials + provider-aware /model (#329)
Encrypted credential storage
- internal/securefile: AES-256-GCM at-rest crypter (fail-closed, atomic, locked).
- internal/credstore: provider API-key store, keyring-first with an encrypted-file
  fallback; plaintext only via an explicit ZERO_CRED_STORAGE=file opt-out.
- Provider API keys no longer live in config.json: captured straight into the store
  from the wizard/CLI setup, and any existing inline plaintext key is migrated to the
  store on launch (fail-soft; stripped only after the store write succeeds).
- buildProvider and every model/provider switch reload the credential (stored key,
  env var, or OAuth bearer), so a stored-key provider always authenticates.
- `zero auth logout <provider>` now also removes the stored API key and its marker.

/model and /provider
- /model lists every authenticated provider (filtered to those with a usable
  credential), grouped per provider under section headers.
- Each provider's models are live-discovered (its real served models) rather than a
  static catalog; local providers show only the models actually pulled.
- Selecting a model from another provider switches provider and model together.
- /provider offers keep / replace / remove when a provider already has a stored key.

Gated throughout: go build, go vet ./..., go test ./... -race, lint.
2026-06-27 23:25:08 +05:30

83 lines
2.2 KiB
Go

package securefile
import (
"os"
"path/filepath"
"runtime"
"testing"
)
func TestSealOpenRoundTrip(t *testing.T) {
secret := filepath.Join(t.TempDir(), "k.secret")
c := NewCrypter(secret)
plain := []byte(`{"openai":{"type":"api","key":"sk-test-123"}}`)
blob, err := c.Seal(plain)
if err != nil {
t.Fatalf("seal: %v", err)
}
if string(blob) == string(plain) {
t.Fatal("blob must not equal plaintext")
}
// The secret file is created 0600.
info, err := os.Stat(secret)
if err != nil {
t.Fatalf("secret not created: %v", err)
}
// Windows doesn't honor Unix permission bits (Chmod only toggles read-only), so
// the 0600 guarantee only holds on Unix.
if runtime.GOOS != "windows" {
if perm := info.Mode().Perm(); perm != 0o600 {
t.Fatalf("secret perm = %o, want 600", perm)
}
}
got, err := c.Open(blob)
if err != nil {
t.Fatalf("open: %v", err)
}
if string(got) != string(plain) {
t.Fatalf("round-trip = %q, want %q", got, plain)
}
}
func TestOpenTamperedFailsClosed(t *testing.T) {
c := NewCrypter(filepath.Join(t.TempDir(), "k.secret"))
blob, err := c.Seal([]byte("secret-data"))
if err != nil {
t.Fatal(err)
}
blob[len(blob)-1] ^= 0xff // flip a ciphertext bit
if _, err := c.Open(blob); err == nil {
t.Fatal("expected tampered blob to fail authentication")
}
}
func TestOpenMissingSecretFailsClosed(t *testing.T) {
c := NewCrypter(filepath.Join(t.TempDir(), "absent.secret"))
if _, err := c.Open([]byte("0123456789012")); err == nil {
t.Fatal("expected open with no secret to fail, not mint a new key")
}
}
func TestOpenShortBlobFails(t *testing.T) {
c := NewCrypter(filepath.Join(t.TempDir(), "k.secret"))
if _, err := c.Seal([]byte("x")); err != nil { // create the secret
t.Fatal(err)
}
if _, err := c.Open([]byte("short")); err == nil {
t.Fatal("expected too-short blob to fail")
}
}
func TestWrongSecretCannotDecrypt(t *testing.T) {
dir := t.TempDir()
a := NewCrypter(filepath.Join(dir, "a.secret"))
blob, err := a.Seal([]byte("data"))
if err != nil {
t.Fatal(err)
}
b := NewCrypter(filepath.Join(dir, "b.secret")) // different secret
if _, err := b.Open(blob); err == nil {
t.Fatal("expected decryption under a different secret to fail")
}
}