Sharing refuses a key shorter than minLength characters (default 32, never under 16) or of fewer than minClasses kinds of lowercase, uppercase, digits and symbols (default 2, so random hex passes), saying so without showing it. Opening doesn't check. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
237 lines
8.0 KiB
Go
237 lines
8.0 KiB
Go
package share
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
|
)
|
|
|
|
func init() { workFactor = 10 } // fast; opening accepts any up to maxWorkFactor
|
|
|
|
var (
|
|
key = ref.Ref{Kind: provider.KindAzure, Host: "kv-team.vault.azure.net", Type: provider.TypeSecret, Name: "env-share", Version: "91d3e0"}
|
|
pass = []byte("0123456789abcdef0123456789abcdef")
|
|
)
|
|
|
|
func TestSealOpen(t *testing.T) {
|
|
files := []File{
|
|
{Path: ".env", Data: []byte("DB_PASSWORD=hunter2\n")},
|
|
{Path: "apps/api/.env.local", Data: []byte("TOKEN=\"a b\" # quoted\n")},
|
|
}
|
|
link, err := Seal(Header{Key: key, Repo: "git.example.com/team/app"}, pass, files, Rules{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(link.String(), "hunter2") || strings.Contains(link.String(), "apps") {
|
|
t.Errorf("link shows what it shares: %s", link)
|
|
}
|
|
// It goes through ref.Parse like any link.
|
|
r, err := ref.Parse(link.String())
|
|
if err != nil || !r.IsShare() {
|
|
t.Fatalf("parse: %v, %+v", err, r)
|
|
}
|
|
s, err := Read(r)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Key != key || s.Repo != "git.example.com/team/app" || s.Files != 2 {
|
|
t.Errorf("header %+v", s.Header)
|
|
}
|
|
got, err := s.Open(pass)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 || got[1].Path != files[1].Path || string(got[1].Data) != string(files[1].Data) {
|
|
t.Errorf("opened %+v", got)
|
|
}
|
|
if _, err := s.Open([]byte("another key, long enough to use")); err == nil || !strings.Contains(err.Error(), "doesn't decrypt") {
|
|
t.Errorf("wrong key: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSealRefuses(t *testing.T) {
|
|
ok := []File{{Path: ".env", Data: []byte("A=1")}}
|
|
for name, c := range map[string]struct {
|
|
h Header
|
|
pass []byte
|
|
files []File
|
|
want string
|
|
}{
|
|
"short key": {Header{Key: key}, []byte("short"), ok, "keyRules wants"},
|
|
"vault as key": {Header{Key: ref.Ref{Kind: provider.KindAzure, Host: "kv.vault.azure.net"}}, pass, ok, "isn't a secret"},
|
|
"no files": {Header{Key: key}, pass, nil, "no files"},
|
|
"not env": {Header{Key: key}, pass, []File{{Path: "main.go", Data: []byte("x")}}, "isn't an env file"},
|
|
"escapes": {Header{Key: key}, pass, []File{{Path: "../.env", Data: []byte("x")}}, "inside a repo"},
|
|
"binary": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte{0xff, 0xfe}}}, "isn't text"},
|
|
"too big": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte(random(2 * MaxLink))}}, "more than the 16 KB"},
|
|
} {
|
|
if _, err := Seal(c.h, c.pass, c.files, Rules{}); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: %v, want %q", name, err, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// random is text that doesn't compress.
|
|
func random(n int) string {
|
|
var b strings.Builder
|
|
x := uint32(1)
|
|
for b.Len() < n {
|
|
x ^= x << 13
|
|
x ^= x >> 17
|
|
x ^= x << 5
|
|
b.WriteByte("abcdefghijklmnopqrstuvwxyz0123456789"[x%36])
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func TestRules(t *testing.T) {
|
|
for _, c := range []struct {
|
|
r Rules
|
|
key string
|
|
pass bool
|
|
}{
|
|
{Rules{}, "q2Hd8bT4Lw1sZp9Kf6Vx3Nc7Rm0Ya5Ue", true}, // base64-like, 32
|
|
{Rules{}, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", true}, // hex
|
|
{Rules{}, "correct-horse-battery-staple", false}, // 28: short
|
|
{Rules{}, strings.Repeat("a", 40), false}, // one kind
|
|
{Rules{MinLength: 16, MinClasses: 1}, strings.Repeat("a", 16), true},
|
|
{Rules{MinLength: 4}, "abc123", false}, // never under MinKey
|
|
{Rules{MinClasses: 4}, "q2Hd8bT4Lw1sZp9Kf6Vx3Nc7Rm0Ya5Ue", false},
|
|
{Rules{MinClasses: 4}, "q2Hd8bT4Lw1sZp9Kf6Vx3Nc7Rm0Ya5U+", true},
|
|
} {
|
|
if err := c.r.Check([]byte(c.key)); (err == nil) != c.pass {
|
|
t.Errorf("%+v %q: %v", c.r, c.key, err)
|
|
} else if err != nil && strings.Contains(err.Error(), c.key) {
|
|
t.Errorf("the error shows the key: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadGarbled(t *testing.T) {
|
|
link, err := Seal(Header{Key: key}, pass, []File{{Path: ".env", Data: []byte("A=1")}}, Rules{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cut := ref.Ref{Share: link.Share[:len(link.Share)/2]}
|
|
s, err := Read(cut)
|
|
if err == nil {
|
|
_, err = s.Open(pass)
|
|
}
|
|
if err == nil {
|
|
t.Error("a link cut in half opened")
|
|
}
|
|
if _, err := Read(ref.Ref{Share: "AgAAAAAAAAAAAAAAAAAAAA"}); err == nil || !strings.Contains(err.Error(), "newer lazysecrets") {
|
|
t.Errorf("version 2: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckPath(t *testing.T) {
|
|
for _, p := range []string{".env", "apps/api/.env.local", "prod.env"} {
|
|
if err := CheckPath(p); err != nil {
|
|
t.Errorf("%s: %v", p, err)
|
|
}
|
|
}
|
|
for _, p := range []string{"", "/etc/.env", "../.env", "a/../.env", ".git/.env", "a/.git/x.env", `a\.env`, ".env.example", "node_modules/x/.env", "Makefile"} {
|
|
if err := CheckPath(p); err == nil {
|
|
t.Errorf("%q accepted", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPlanWrite(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if out, err := exec.Command("git", "-C", dir, "init", "-q").CombinedOutput(); err != nil {
|
|
t.Skip("git:", string(out))
|
|
}
|
|
os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(".env\n"), 0o644)
|
|
os.WriteFile(filepath.Join(dir, ".env"), []byte("OLD=1\n"), 0o644)
|
|
targets, err := Plan(dir, []File{
|
|
{Path: ".env", Data: []byte("NEW=1\n")},
|
|
{Path: "apps/api/.env.local", Data: []byte("B=2\n")},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !targets[0].Exists || targets[0].Exposed {
|
|
t.Errorf(".env: %+v", targets[0])
|
|
}
|
|
if targets[1].Exists || !targets[1].Exposed {
|
|
t.Errorf("apps/api/.env.local: %+v", targets[1])
|
|
}
|
|
for _, tg := range targets {
|
|
if err := tg.Write(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if b, _ := os.ReadFile(filepath.Join(dir, ".env")); string(b) != "NEW=1\n" {
|
|
t.Errorf(".env is %q", b)
|
|
}
|
|
info, err := os.Stat(filepath.Join(dir, "apps", "api", ".env.local"))
|
|
if err != nil || info.Mode().Perm() != 0o600 {
|
|
t.Errorf("new file: %v, %v", info.Mode(), err)
|
|
}
|
|
}
|
|
|
|
func TestPlanSymlinks(t *testing.T) {
|
|
dir, outside := t.TempDir(), t.TempDir()
|
|
if err := os.Symlink(outside, filepath.Join(dir, "apps")); err != nil {
|
|
t.Skip(err)
|
|
}
|
|
if _, err := Plan(dir, []File{{Path: "apps/.env"}}); err == nil || !strings.Contains(err.Error(), "out of the folder") {
|
|
t.Errorf("through a linked folder: %v", err)
|
|
}
|
|
os.Symlink(filepath.Join(outside, "x"), filepath.Join(dir, ".env"))
|
|
if _, err := Plan(dir, []File{{Path: ".env"}}); err == nil || !strings.Contains(err.Error(), "symlink") {
|
|
t.Errorf("onto a symlink: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestParseKey(t *testing.T) {
|
|
for _, s := range []string{
|
|
"lazysecrets://azure/kv-team.vault.azure.net/secrets/env-share",
|
|
"https://kv-team.vault.azure.net/secrets/env-share",
|
|
"@Microsoft.KeyVault(VaultName=kv-team;SecretName=env-share)",
|
|
"lazysecrets://databricks/adb-1.1.azuredatabricks.net/team/env-share",
|
|
} {
|
|
if k, err := ParseKey(s); err != nil || k.Name != "env-share" {
|
|
t.Errorf("%s: %+v, %v", s, k, err)
|
|
}
|
|
}
|
|
for s, want := range map[string]string{
|
|
"{{secrets/team/env-share}}": "names no workspace",
|
|
"https://kv-team.vault.azure.net/keys/sops": "isn't a secret",
|
|
"https://kv-team.vault.azure.net/": "isn't a secret",
|
|
"lazysecrets://azure/subscriptions/0000-1111": "isn't a secret",
|
|
} {
|
|
if _, err := ParseKey(s); err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("%s: %v, want %q", s, err, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDefaultKey(t *testing.T) {
|
|
dir := t.TempDir()
|
|
general := "lazysecrets://azure/kv-team.vault.azure.net/secrets/env-share"
|
|
k, ok, err := DefaultKey(dir, general)
|
|
if err != nil || !ok || k.Name != "env-share" {
|
|
t.Errorf("general: %+v %v %v", k, ok, err)
|
|
}
|
|
os.WriteFile(filepath.Join(dir, ".lazysecrets.yaml"), []byte("share:\n key: lazysecrets://azure/kv-app.vault.azure.net/secrets/app-share\n"), 0o644)
|
|
if k, _, _ := DefaultKey(dir, general); k.Name != "app-share" {
|
|
t.Errorf("the repo's should win: %+v", k)
|
|
}
|
|
if _, ok, _ := DefaultKey(t.TempDir(), ""); ok {
|
|
t.Error("a key with none set")
|
|
}
|
|
unpinned := key
|
|
unpinned.Version = ""
|
|
if p := Pin(unpinned, "abc"); p.Version != "abc" {
|
|
t.Errorf("pin: %+v", p)
|
|
}
|
|
}
|