278 lines
7.0 KiB
Go
278 lines
7.0 KiB
Go
package secrets
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func testKey(seed byte) []byte {
|
|
key := make([]byte, 32)
|
|
for i := range key {
|
|
key[i] = seed
|
|
}
|
|
return key
|
|
}
|
|
|
|
func readAllSecretStoreBytes(t *testing.T, root string) []byte {
|
|
t.Helper()
|
|
|
|
var data []byte
|
|
if err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
fileData, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
data = append(data, fileData...)
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatalf("read secret store bytes: %v", err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func TestStoreRoundTripsSecretWithoutPlaintextOnDisk(t *testing.T) {
|
|
root := t.TempDir()
|
|
store, err := NewStore(root, testKey(0x11))
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
|
|
const id = "server.password"
|
|
const value = "s3cr3t-value"
|
|
if err := store.Write(id, value); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
|
|
got, err := store.Read(id)
|
|
if err != nil {
|
|
t.Fatalf("Read: %v", err)
|
|
}
|
|
if got != value {
|
|
t.Fatalf("Read = %q, want %q", got, value)
|
|
}
|
|
|
|
raw := readAllSecretStoreBytes(t, root)
|
|
if bytes.Contains(raw, []byte(value)) {
|
|
t.Fatal("secret value is stored as plaintext")
|
|
}
|
|
if bytes.Contains(raw, []byte(id)) {
|
|
t.Fatal("secret id is stored as plaintext")
|
|
}
|
|
}
|
|
|
|
func TestStoreRejectsWrongKey(t *testing.T) {
|
|
root := t.TempDir()
|
|
store, err := NewStore(root, testKey(0x11))
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
if err := store.Write("server.password", "s3cr3t-value"); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
|
|
wrongKeyStore, err := NewStore(root, testKey(0x22))
|
|
if err != nil {
|
|
t.Fatalf("NewStore wrong key: %v", err)
|
|
}
|
|
if _, err := wrongKeyStore.Read("server.password"); err == nil {
|
|
t.Fatal("Read with wrong key succeeded")
|
|
}
|
|
}
|
|
|
|
func TestStoreRejectsUnsafeIDs(t *testing.T) {
|
|
store, err := NewStore(t.TempDir(), testKey(0x11))
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
|
|
for _, id := range []string{"", ".", "..", "../secret", `folder\secret`, strings.Repeat("a", 257)} {
|
|
t.Run(id, func(t *testing.T) {
|
|
if err := store.Write(id, "value"); err == nil {
|
|
t.Fatalf("Write(%q): expected error", id)
|
|
}
|
|
if _, err := store.Read(id); err == nil {
|
|
t.Fatalf("Read(%q): expected error", id)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStoreDeletesSecretRecord(t *testing.T) {
|
|
store, err := NewStore(t.TempDir(), testKey(0x11))
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
if err := store.Write("server.password", "s3cr3t-value"); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
|
|
if err := store.Delete("server.password"); err != nil {
|
|
t.Fatalf("Delete: %v", err)
|
|
}
|
|
if _, err := store.Read("server.password"); err == nil {
|
|
t.Fatal("Read after Delete succeeded")
|
|
}
|
|
list, err := store.ListRecords()
|
|
if err != nil {
|
|
t.Fatalf("ListRecords: %v", err)
|
|
}
|
|
if len(list) != 0 {
|
|
t.Fatalf("ListRecords after Delete = %+v, want empty", list)
|
|
}
|
|
}
|
|
|
|
func TestStoreListsScopedRecordsWithoutPlaintextOnDisk(t *testing.T) {
|
|
root := t.TempDir()
|
|
store, err := NewStore(root, testKey(0x11))
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
|
|
records := []SecretRecord{
|
|
{
|
|
ID: "global.server-token",
|
|
Title: "Server Token",
|
|
Value: "global-secret-value",
|
|
Scope: SecretScope{Kind: ScopeGlobal},
|
|
Username: "root",
|
|
UpdatedAt: "caller value must be replaced",
|
|
},
|
|
{
|
|
ID: "client-a.database",
|
|
Title: "Client A Database",
|
|
Value: "workspace-secret-value",
|
|
Scope: SecretScope{
|
|
Kind: ScopeWorkspace,
|
|
WorkspaceRootPath: "ClientA",
|
|
},
|
|
Username: "app",
|
|
},
|
|
}
|
|
for _, record := range records {
|
|
if err := store.WriteRecord(record); err != nil {
|
|
t.Fatalf("WriteRecord(%s): %v", record.ID, err)
|
|
}
|
|
}
|
|
|
|
list, err := store.ListRecords()
|
|
if err != nil {
|
|
t.Fatalf("ListRecords: %v", err)
|
|
}
|
|
if len(list) != 2 {
|
|
t.Fatalf("ListRecords len = %d, want 2: %+v", len(list), list)
|
|
}
|
|
if list[0].Value != "" || list[1].Value != "" {
|
|
t.Fatalf("ListRecords leaked secret values: %+v", list)
|
|
}
|
|
|
|
workspaceRecord, err := store.ReadRecord("client-a.database")
|
|
if err != nil {
|
|
t.Fatalf("ReadRecord: %v", err)
|
|
}
|
|
if workspaceRecord.Value != "workspace-secret-value" {
|
|
t.Fatalf("workspace secret value = %q", workspaceRecord.Value)
|
|
}
|
|
if workspaceRecord.Scope.Kind != ScopeWorkspace || workspaceRecord.Scope.WorkspaceRootPath != "ClientA" {
|
|
t.Fatalf("workspace scope = %+v", workspaceRecord.Scope)
|
|
}
|
|
if workspaceRecord.UpdatedAt == "" || workspaceRecord.UpdatedAt == "caller value must be replaced" {
|
|
t.Fatalf("UpdatedAt was not set by store: %+v", workspaceRecord)
|
|
}
|
|
|
|
raw := readAllSecretStoreBytes(t, root)
|
|
for _, plaintext := range []string{
|
|
"global-secret-value",
|
|
"workspace-secret-value",
|
|
"global.server-token",
|
|
"client-a.database",
|
|
"Client A Database",
|
|
"ClientA",
|
|
} {
|
|
if bytes.Contains(raw, []byte(plaintext)) {
|
|
t.Fatalf("secret store contains plaintext %q", plaintext)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVaultSessionUnlocksWithMasterPasswordOnce(t *testing.T) {
|
|
root := t.TempDir()
|
|
session := NewVaultSession(root)
|
|
|
|
if session.Unlocked() {
|
|
t.Fatal("new session is unlocked")
|
|
}
|
|
if _, err := session.Store(); err == nil {
|
|
t.Fatal("Store before unlock succeeded")
|
|
}
|
|
|
|
store, err := session.Unlock("correct horse battery staple")
|
|
if err != nil {
|
|
t.Fatalf("Unlock first time: %v", err)
|
|
}
|
|
if !session.Unlocked() {
|
|
t.Fatal("session did not stay unlocked")
|
|
}
|
|
if err := store.Write("server.password", "s3cr3t-value"); err != nil {
|
|
t.Fatalf("Write: %v", err)
|
|
}
|
|
|
|
sameStore, err := session.Store()
|
|
if err != nil {
|
|
t.Fatalf("Store after unlock: %v", err)
|
|
}
|
|
if got, err := sameStore.Read("server.password"); err != nil || got != "s3cr3t-value" {
|
|
t.Fatalf("Read after unlock = %q, %v", got, err)
|
|
}
|
|
|
|
nextSession := NewVaultSession(root)
|
|
if _, err := nextSession.Unlock("wrong password"); err == nil {
|
|
t.Fatal("Unlock with wrong password succeeded")
|
|
}
|
|
if _, err := nextSession.Unlock("correct horse battery staple"); err != nil {
|
|
t.Fatalf("Unlock with correct password: %v", err)
|
|
}
|
|
|
|
raw := readAllSecretStoreBytes(t, root)
|
|
for _, plaintext := range []string{"correct horse battery staple", "s3cr3t-value"} {
|
|
if bytes.Contains(raw, []byte(plaintext)) {
|
|
t.Fatalf("vault session storage contains plaintext %q", plaintext)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestVaultSessionReportsInitializationAndRejectsWeakInitialPassword(t *testing.T) {
|
|
root := t.TempDir()
|
|
session := NewVaultSession(root)
|
|
|
|
initialized, err := session.Initialized()
|
|
if err != nil {
|
|
t.Fatalf("Initialized: %v", err)
|
|
}
|
|
if initialized {
|
|
t.Fatal("new secret session is initialized")
|
|
}
|
|
if _, err := session.Unlock("123123"); err == nil || !strings.Contains(err.Error(), "at least 8 characters") {
|
|
t.Fatalf("weak initial Unlock err = %v, want minimum length error", err)
|
|
}
|
|
|
|
if _, err := session.Unlock("strong password"); err != nil {
|
|
t.Fatalf("Unlock strong initial password: %v", err)
|
|
}
|
|
initialized, err = session.Initialized()
|
|
if err != nil {
|
|
t.Fatalf("Initialized after unlock: %v", err)
|
|
}
|
|
if !initialized {
|
|
t.Fatal("secret session was not initialized")
|
|
}
|
|
}
|