162 lines
4.3 KiB
Go
162 lines
4.3 KiB
Go
package credential
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestEncryptDecrypt(t *testing.T) {
|
|
root := t.TempDir()
|
|
encrypted, err := Encrypt(root, "secret-value")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(encrypted, encryptedPrefix) || strings.Contains(encrypted, "secret-value") {
|
|
t.Fatal("credential was not encrypted")
|
|
}
|
|
decrypted, err := Decrypt(root, encrypted)
|
|
if err != nil || decrypted != "secret-value" {
|
|
t.Fatal("credential could not be decrypted")
|
|
}
|
|
info, err := os.Stat(filepath.Join(root, "data", "credential.key"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if runtime.GOOS != "windows" && info.Mode().Perm()&0077 != 0 {
|
|
t.Fatalf("credential key permissions are too broad: %o", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestDecryptRejectsTampering(t *testing.T) {
|
|
root := t.TempDir()
|
|
encrypted, err := Encrypt(root, "secret-value")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
replacement := byte('A')
|
|
if encrypted[len(encrypted)-1] == replacement {
|
|
replacement = 'B'
|
|
}
|
|
encrypted = encrypted[:len(encrypted)-1] + string(replacement)
|
|
if _, err := Decrypt(root, encrypted); err == nil {
|
|
t.Fatal("tampered credential was accepted")
|
|
}
|
|
}
|
|
|
|
func TestDecryptRejectsPlaintext(t *testing.T) {
|
|
if _, err := Decrypt(t.TempDir(), "legacy-plaintext-secret"); err == nil {
|
|
t.Fatal("plaintext credential was accepted by strict decryption")
|
|
}
|
|
}
|
|
|
|
func TestEncryptKeepsEmptyAndProtectsPrefixValues(t *testing.T) {
|
|
root := t.TempDir()
|
|
if value, err := Encrypt(root, ""); err != nil || value != "" {
|
|
t.Fatal("empty credential changed")
|
|
}
|
|
plaintext := "enc:v1:third-party-secret"
|
|
encrypted, err := Encrypt(root, plaintext)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if encrypted == plaintext || !strings.HasPrefix(encrypted, encryptedPrefix) {
|
|
t.Fatal("prefix credential was not encrypted")
|
|
}
|
|
decrypted, err := Decrypt(root, encrypted)
|
|
if err != nil || decrypted != plaintext {
|
|
t.Fatal("prefix credential did not round trip")
|
|
}
|
|
}
|
|
|
|
func TestCredentialKeyTemporaryNamesAreIndependentRandomValues(t *testing.T) {
|
|
seen := make(map[string]struct{})
|
|
for i := 0; i < 32; i++ {
|
|
name, err := newCredentialKeyTemporaryName()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(name, ".credential.key-") {
|
|
t.Fatalf("unexpected temporary name: %s", name)
|
|
}
|
|
if _, ok := seen[name]; ok {
|
|
t.Fatalf("duplicate temporary name: %s", name)
|
|
}
|
|
seen[name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
func TestReadKeyRejectsDirectorySymlink(t *testing.T) {
|
|
root := t.TempDir()
|
|
target := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(target, "credential.key"), make([]byte, 32), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(target, filepath.Join(root, "data")); err != nil {
|
|
t.Skipf("symlink is unavailable: %v", err)
|
|
}
|
|
if _, err := readKey(root); err == nil {
|
|
t.Fatal("credential directory symlink was accepted")
|
|
}
|
|
}
|
|
|
|
func TestReadKeyRejectsKeySymlink(t *testing.T) {
|
|
root := t.TempDir()
|
|
configDir := filepath.Join(root, "data")
|
|
if err := os.MkdirAll(configDir, 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
target := filepath.Join(root, "outside.key")
|
|
if err := os.WriteFile(target, make([]byte, 32), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(target, filepath.Join(configDir, "credential.key")); err != nil {
|
|
t.Skipf("symlink is unavailable: %v", err)
|
|
}
|
|
if _, err := readKey(root); err == nil {
|
|
t.Fatal("credential key symlink was accepted")
|
|
}
|
|
}
|
|
|
|
func TestConcurrentEncryptUsesSingleCompleteKey(t *testing.T) {
|
|
root := t.TempDir()
|
|
const workers = 16
|
|
values := make(chan string, workers)
|
|
errors := make(chan error, workers)
|
|
var group sync.WaitGroup
|
|
for i := 0; i < workers; i++ {
|
|
group.Add(1)
|
|
go func() {
|
|
defer group.Done()
|
|
value, err := Encrypt(root, "shared-secret")
|
|
if err != nil {
|
|
errors <- err
|
|
return
|
|
}
|
|
values <- value
|
|
}()
|
|
}
|
|
group.Wait()
|
|
close(values)
|
|
close(errors)
|
|
for err := range errors {
|
|
t.Fatal(err)
|
|
}
|
|
for value := range values {
|
|
plaintext, err := Decrypt(root, value)
|
|
if err != nil || plaintext != "shared-secret" {
|
|
t.Fatalf("concurrent credential could not be decrypted: %v", err)
|
|
}
|
|
}
|
|
entries, err := os.ReadDir(filepath.Join(root, "data"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(entries) != 1 || entries[0].Name() != "credential.key" {
|
|
t.Fatalf("unexpected credential directory contents: %#v", entries)
|
|
}
|
|
}
|