Files
vault-tools/shared/keyring_test.go
T
unkin-agent 42048ae6ee
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Encrypt cached tokens with a kernel keyring key
2026-10-23 23:07:35 +11:00

133 lines
3.5 KiB
Go

package shared
import (
"bytes"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"git.unkin.net/unkin/golib/keyring"
"golang.org/x/sys/unix"
)
// TestMain seals tokens with a run-unique key, skipping outside CI where
// keyctl is unimplemented.
func TestMain(m *testing.M) {
KeyringDesc = fmt.Sprintf("vault-tools-test:shared:%d", os.Getpid())
k, err := keyring.Open(KeyringDesc)
if errors.Is(err, unix.ENOSYS) && os.Getenv("CI") == "" {
fmt.Println("skipping: kernel keyring unavailable:", err)
os.Exit(0)
}
if err != nil {
fmt.Println(err)
os.Exit(1)
}
code := m.Run()
_ = k.Forget()
os.Exit(code)
}
func writeRaw(t *testing.T, context string, data []byte) string {
t.Helper()
path, err := TokenPath(context)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
return path
}
func TestSaveTokenSealsAtRest(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
if err := SaveToken(&Token{Context: "sydney", Token: "hvs.secret"}); err != nil {
t.Fatal(err)
}
path, _ := TokenPath("sydney")
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(data, []byte("hvs.secret")) {
t.Fatal("token stored in plaintext")
}
entries, _ := os.ReadDir(filepath.Dir(path))
if len(entries) != 1 {
t.Errorf("cache dir has %d entries, want only the token file", len(entries))
}
}
func TestLoadTokenLockedAfterKeyLoss(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
if err := SaveToken(&Token{Context: "sydney", Token: "s.x"}); err != nil {
t.Fatal(err)
}
k, err := keyring.Open(KeyringDesc)
if err != nil {
t.Fatal(err)
}
if err := k.Forget(); err != nil {
t.Fatal(err)
}
_, err = LoadToken("sydney")
if !errors.Is(err, ErrLocked) || !strings.Contains(err.Error(), "vcm login sydney") {
t.Fatalf("LoadToken = %v, want ErrLocked with login hint", err)
}
if path, _ := TokenPath("sydney"); !fileExists(path) {
t.Error("locked token file was deleted")
}
}
func TestLoadTokenLegacyPlaintextIsLocked(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
if err := os.MkdirAll(CacheDir(), 0o700); err != nil {
t.Fatal(err)
}
path := writeRaw(t, "sydney", []byte(`{"context":"sydney","token":"s.x"}`+"\n"))
if _, err := LoadToken("sydney"); !errors.Is(err, ErrLocked) {
t.Fatalf("LoadToken = %v, want ErrLocked", err)
}
if !fileExists(path) {
t.Error("legacy token file was deleted")
}
}
func TestLoadTokenCorruptNamesFile(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
if err := SaveToken(&Token{Context: "sydney", Token: "s.x"}); err != nil {
t.Fatal(err)
}
path, _ := TokenPath("sydney")
data, _ := os.ReadFile(path)
data[len(data)-1] ^= 0xff
writeRaw(t, "sydney", data)
_, err := LoadToken("sydney")
if !errors.Is(err, keyring.ErrCorrupt) || !strings.Contains(err.Error(), path) {
t.Fatalf("LoadToken = %v, want ErrCorrupt naming %s", err, path)
}
}
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func TestSaveTokenRenameFailureLeavesNoTemp(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
path, _ := TokenPath("sydney")
if err := os.MkdirAll(filepath.Join(path, "blocker"), 0o700); err != nil {
t.Fatal(err)
}
if err := SaveToken(&Token{Context: "sydney", Token: "s.x"}); err == nil {
t.Fatal("expected rename onto a non-empty directory to fail")
}
if entries, _ := os.ReadDir(CacheDir()); len(entries) != 1 {
t.Errorf("cache dir has %d entries, want temp file cleaned up", len(entries))
}
}