feat: share env files as encrypted links #54
No files matched your search
@@ -317,7 +317,7 @@ Flags:
|
|||||||
if shareArgs != nil {
|
if shareArgs != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return shareFiles(ctx, providers, st, cfg.Share.Key, shareArgs, os.Stdout)
|
return shareFiles(ctx, providers, st, cfg.Share.Key, shareRules(cfg), shareArgs, os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Links opened elsewhere (lazysecrets launch) can come here.
|
// Links opened elsewhere (lazysecrets launch) can come here.
|
||||||
@@ -391,6 +391,7 @@ Flags:
|
|||||||
Changelog: lazysecrets.Changelog,
|
Changelog: lazysecrets.Changelog,
|
||||||
Dir: placeDir,
|
Dir: placeDir,
|
||||||
ShareKey: cfg.Share.Key,
|
ShareKey: cfg.Share.Key,
|
||||||
|
ShareRules: shareRules(cfg),
|
||||||
Open: open,
|
Open: open,
|
||||||
Styles: &theme,
|
Styles: &theme,
|
||||||
DefaultFg: defaultFg,
|
DefaultFg: defaultFg,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/config"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
||||||
@@ -19,7 +20,7 @@ import (
|
|||||||
// the --key secret's value, else the repo's or the config's share.key:
|
// the --key secret's value, else the repo's or the config's share.key:
|
||||||
//
|
//
|
||||||
// lazysecrets share .env apps/api/.env
|
// lazysecrets share .env apps/api/.env
|
||||||
func shareFiles(ctx context.Context, providers []provider.Provider, st *store.Store, general string, args []string, out io.Writer) error {
|
func shareFiles(ctx context.Context, providers []provider.Provider, st *store.Store, general string, rules share.Rules, args []string, out io.Writer) error {
|
||||||
fs := flag.NewFlagSet("share", flag.ContinueOnError)
|
fs := flag.NewFlagSet("share", flag.ContinueOnError)
|
||||||
keyLink := fs.String("key", "", "the secret to encrypt with: its lazysecrets:// link, Key Vault URL or reference (default: share.key)")
|
keyLink := fs.String("key", "", "the secret to encrypt with: its lazysecrets:// link, Key Vault URL or reference (default: share.key)")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
@@ -78,10 +79,15 @@ func shareFiles(ctx context.Context, providers []provider.Provider, st *store.St
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("reading the key %s: %w", key, err)
|
return fmt.Errorf("reading the key %s: %w", key, err)
|
||||||
}
|
}
|
||||||
link, err := share.Seal(share.Header{Key: share.Pin(key, version), Repo: id}, []byte(v.Reveal()), files)
|
link, err := share.Seal(share.Header{Key: share.Pin(key, version), Repo: id}, []byte(v.Reveal()), files, rules)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = fmt.Fprintln(out, link)
|
_, err = fmt.Fprintln(out, link)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shareRules are the config's share.keyRules.
|
||||||
|
func shareRules(cfg config.Config) share.Rules {
|
||||||
|
return share.Rules{MinLength: cfg.Share.KeyRules.MinLength, MinClasses: cfg.Share.KeyRules.MinClasses}
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@ import (
|
|||||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// demoRules take the demo key, made up to be remembered.
|
||||||
|
var demoRules = share.Rules{MinLength: 16, MinClasses: 1}
|
||||||
|
|
||||||
func TestShareFiles(t *testing.T) {
|
func TestShareFiles(t *testing.T) {
|
||||||
root, _ := filepath.EvalSymlinks(t.TempDir())
|
root, _ := filepath.EvalSymlinks(t.TempDir())
|
||||||
for _, args := range [][]string{{"init", "-q"}, {"remote", "add", "origin", "git@git.example.com:team/app.git"}} {
|
for _, args := range [][]string{{"init", "-q"}, {"remote", "add", "origin", "git@git.example.com:team/app.git"}} {
|
||||||
@@ -31,7 +34,7 @@ func TestShareFiles(t *testing.T) {
|
|||||||
key := "lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password"
|
key := "lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password"
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
if err := shareFiles(ctx, providers, st, key, []string{filepath.Join(root, ".env")}, &out); err != nil {
|
if err := shareFiles(ctx, providers, st, key, demoRules, []string{filepath.Join(root, ".env")}, &out); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
r, err := ref.Parse(strings.TrimSpace(out.String()))
|
r, err := ref.Parse(strings.TrimSpace(out.String()))
|
||||||
@@ -57,7 +60,7 @@ func TestShareFiles(t *testing.T) {
|
|||||||
{key, nil, "usage"},
|
{key, nil, "usage"},
|
||||||
{"", []string{"--key", "lazysecrets://azure/kv-app-prod.vault.azure.net", filepath.Join(root, ".env")}, "isn't a secret"},
|
{"", []string{"--key", "lazysecrets://azure/kv-app-prod.vault.azure.net", filepath.Join(root, ".env")}, "isn't a secret"},
|
||||||
} {
|
} {
|
||||||
if err := shareFiles(ctx, providers, st, c.general, c.args, &out); err == nil || !strings.Contains(err.Error(), c.want) {
|
if err := shareFiles(ctx, providers, st, c.general, demoRules, c.args, &out); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||||
t.Errorf("%v: %v, want %q", c.args, err, c.want)
|
t.Errorf("%v: %v, want %q", c.args, err, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ providers:
|
|||||||
exclude: ["**/testdata/**"]
|
exclude: ["**/testdata/**"]
|
||||||
share: # sharing env files as encrypted links
|
share: # sharing env files as encrypted links
|
||||||
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link; for repos naming none
|
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link; for repos naming none
|
||||||
|
keyRules: # what a key's value must be
|
||||||
|
minLength: 32 # characters, 16 at least
|
||||||
|
minClasses: 2 # kinds: lowercase, uppercase, digits, symbols
|
||||||
```
|
```
|
||||||
|
|
||||||
More, each on its own page: [repos](repos.md) (`providers.repos`),
|
More, each on its own page: [repos](repos.md) (`providers.repos`),
|
||||||
|
|||||||
+14
-2
@@ -42,8 +42,7 @@ lazysecrets://share/AQ…
|
|||||||
The files are encrypted with a secret's value, so whoever can read that
|
The files are encrypted with a secret's value, so whoever can read that
|
||||||
secret (in Key Vault, Databricks or a repo's SOPS file) can open the link,
|
secret (in Key Vault, Databricks or a repo's SOPS file) can open the link,
|
||||||
and nobody else: who may open it is up to the vault's access, as for
|
and nobody else: who may open it is up to the vault's access, as for
|
||||||
everything else. Make a secret for it once, random and at least 16
|
everything else. Make a secret for it once, random:
|
||||||
characters:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
az keyvault secret set --vault-name kv-team --name env-share --value "$(openssl rand -base64 32)"
|
az keyvault secret set --vault-name kv-team --name env-share --value "$(openssl rand -base64 32)"
|
||||||
@@ -63,6 +62,19 @@ With neither, sharing asks which secret, in [Go to](keys.md#go-to): it
|
|||||||
lists only secrets, and `enter` picks one (pasting a reference picks it
|
lists only secrets, and `enter` picks one (pasting a reference picks it
|
||||||
too).
|
too).
|
||||||
|
|
||||||
|
Anyone with a link can try to guess the key, so sharing refuses one that
|
||||||
|
isn't long and varied enough, saying how (never showing it). By default
|
||||||
|
that's 32 characters of two kinds; your config can ask for more, or less:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
share:
|
||||||
|
keyRules:
|
||||||
|
minLength: 32 # characters; 16 at least
|
||||||
|
minClasses: 2 # kinds of character: lowercase, uppercase, digits, symbols
|
||||||
|
```
|
||||||
|
|
||||||
|
Opening a link doesn't check them.
|
||||||
|
|
||||||
On the command line, `lazysecrets share [--key <secret>] <env file>...`
|
On the command line, `lazysecrets share [--key <secret>] <env file>...`
|
||||||
prints the link.
|
prints the link.
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,16 @@ type Share struct {
|
|||||||
// lazysecrets:// link, Key Vault URL or reference), for repos whose
|
// lazysecrets:// link, Key Vault URL or reference), for repos whose
|
||||||
// .lazysecrets.yaml doesn't name one.
|
// .lazysecrets.yaml doesn't name one.
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
|
// KeyRules are what a key's value must be to share with it.
|
||||||
|
KeyRules KeyRules `yaml:"keyRules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyRules default to 32 characters of 2 kinds (share.DefaultRules).
|
||||||
|
type KeyRules struct {
|
||||||
|
MinLength int `yaml:"minLength"` // at least 16
|
||||||
|
// MinClasses is how many kinds of character: lowercase, uppercase,
|
||||||
|
// digits, symbols.
|
||||||
|
MinClasses int `yaml:"minClasses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
@@ -236,6 +246,12 @@ func (c Config) validate(path string) error {
|
|||||||
return fmt.Errorf("%s: share.key: %q is not a secret's lazysecrets:// link, Key Vault URL or reference", path, k)
|
return fmt.Errorf("%s: share.key: %q is not a secret's lazysecrets:// link, Key Vault URL or reference", path, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if n := c.Share.KeyRules.MinLength; n != 0 && n < 16 {
|
||||||
|
return fmt.Errorf("%s: share.keyRules.minLength: %d is under 16", path, n)
|
||||||
|
}
|
||||||
|
if n := c.Share.KeyRules.MinClasses; n < 0 || n > 4 {
|
||||||
|
return fmt.Errorf("%s: share.keyRules.minClasses: %d is not between 1 and 4", path, n)
|
||||||
|
}
|
||||||
for i, v := range c.Providers.Azure.Vaults {
|
for i, v := range c.Providers.Azure.Vaults {
|
||||||
if !strings.HasPrefix(v, "https://") {
|
if !strings.HasPrefix(v, "https://") {
|
||||||
return fmt.Errorf("%s: providers.azure.vaults[%d]: %q is not an https:// vault URL", path, i, v)
|
return fmt.Errorf("%s: providers.azure.vaults[%d]: %q is not an https:// vault URL", path, i, v)
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ func TestLoadRejectsMistakes(t *testing.T) {
|
|||||||
"view": "sourcesView: tree\n",
|
"view": "sourcesView: tree\n",
|
||||||
"releases": "releases: [git.example.com/team/lazysecrets]\n",
|
"releases": "releases: [git.example.com/team/lazysecrets]\n",
|
||||||
"http": "releases: [http://git.example.com/team/lazysecrets]\n",
|
"http": "releases: [http://git.example.com/team/lazysecrets]\n",
|
||||||
|
"share key": "share:\n key: https://kv.vault.azure.net/\n",
|
||||||
|
"short keys": "share:\n keyRules:\n minLength: 8\n",
|
||||||
|
"classes": "share:\n keyRules:\n minClasses: 5\n",
|
||||||
} {
|
} {
|
||||||
if _, err := Load(write(t, content)); err == nil {
|
if _, err := Load(write(t, content)); err == nil {
|
||||||
t.Errorf("%s: no error", name)
|
t.Errorf("%s: no error", name)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package share
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rules are what a key's value must be to share with it: random enough
|
||||||
|
// that guessing it is out of reach, since anyone with a link can try.
|
||||||
|
type Rules struct {
|
||||||
|
MinLength int // characters
|
||||||
|
MinClasses int // kinds of character: lowercase, uppercase, digits, symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultRules take a random base64 or hex value (openssl rand -base64
|
||||||
|
// 32, or -hex 32) and refuse passwords made up to be remembered.
|
||||||
|
var DefaultRules = Rules{MinLength: 32, MinClasses: 2}
|
||||||
|
|
||||||
|
// Check reports what key lacks, without showing it.
|
||||||
|
func (r Rules) Check(key []byte) error {
|
||||||
|
r = r.withDefaults()
|
||||||
|
s := string(key)
|
||||||
|
var lower, upper, digit, symbol bool
|
||||||
|
for _, c := range s {
|
||||||
|
switch {
|
||||||
|
case unicode.IsLower(c):
|
||||||
|
lower = true
|
||||||
|
case unicode.IsUpper(c):
|
||||||
|
upper = true
|
||||||
|
case unicode.IsDigit(c):
|
||||||
|
digit = true
|
||||||
|
default:
|
||||||
|
symbol = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kinds := 0
|
||||||
|
for _, has := range []bool{lower, upper, digit, symbol} {
|
||||||
|
if has {
|
||||||
|
kinds++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n := len([]rune(s))
|
||||||
|
if n >= r.MinLength && kinds >= r.MinClasses {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("the key's value is %d characters of %d kinds, and share.keyRules wants at least %d characters of %d kinds (lowercase, uppercase, digits, symbols): use a random one, like openssl rand -base64 32 makes",
|
||||||
|
n, kinds, r.MinLength, r.MinClasses)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Rules) withDefaults() Rules {
|
||||||
|
if r.MinLength == 0 {
|
||||||
|
r.MinLength = DefaultRules.MinLength
|
||||||
|
}
|
||||||
|
if r.MinClasses == 0 {
|
||||||
|
r.MinClasses = DefaultRules.MinClasses
|
||||||
|
}
|
||||||
|
r.MinLength = max(r.MinLength, MinKey)
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ const (
|
|||||||
// MaxLink keeps a link short enough for chat and tickets (Teams takes
|
// MaxLink keeps a link short enough for chat and tickets (Teams takes
|
||||||
// about 28 KB) and for passing it to a terminal window.
|
// about 28 KB) and for passing it to a terminal window.
|
||||||
MaxLink = 16 << 10
|
MaxLink = 16 << 10
|
||||||
// MinKey is the shortest secret value accepted as a key.
|
// MinKey is the shortest key the rules can ask for.
|
||||||
MinKey = 16
|
MinKey = 16
|
||||||
// maxFiles caps what opening a link unpacks.
|
// maxFiles caps what opening a link unpacks.
|
||||||
maxFiles = 1 << 20
|
maxFiles = 1 << 20
|
||||||
@@ -85,13 +85,14 @@ type file struct {
|
|||||||
Data string `json:"data"`
|
Data string `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seal encrypts files with key, the value of h.Key, into a share link.
|
// Seal encrypts files with key, the value of h.Key, into a share link,
|
||||||
func Seal(h Header, key []byte, files []File) (ref.Ref, error) {
|
// if key keeps to rules.
|
||||||
|
func Seal(h Header, key []byte, files []File, rules Rules) (ref.Ref, error) {
|
||||||
if _, err := ParseKey(h.Key.String()); err != nil {
|
if _, err := ParseKey(h.Key.String()); err != nil {
|
||||||
return ref.Ref{}, fmt.Errorf("the key: %w", err)
|
return ref.Ref{}, fmt.Errorf("the key: %w", err)
|
||||||
}
|
}
|
||||||
if len(key) < MinKey {
|
if err := rules.Check(key); err != nil {
|
||||||
return ref.Ref{}, fmt.Errorf("the key's value is %d characters: use a random one of at least %d", len(key), MinKey)
|
return ref.Ref{}, err
|
||||||
}
|
}
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return ref.Ref{}, errors.New("no files to share")
|
return ref.Ref{}, errors.New("no files to share")
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestSealOpen(t *testing.T) {
|
|||||||
{Path: ".env", Data: []byte("DB_PASSWORD=hunter2\n")},
|
{Path: ".env", Data: []byte("DB_PASSWORD=hunter2\n")},
|
||||||
{Path: "apps/api/.env.local", Data: []byte("TOKEN=\"a b\" # quoted\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)
|
link, err := Seal(Header{Key: key, Repo: "git.example.com/team/app"}, pass, files, Rules{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@ func TestSealRefuses(t *testing.T) {
|
|||||||
files []File
|
files []File
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
"short key": {Header{Key: key}, []byte("short"), ok, "at least 16"},
|
"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"},
|
"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"},
|
"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"},
|
"not env": {Header{Key: key}, pass, []File{{Path: "main.go", Data: []byte("x")}}, "isn't an env file"},
|
||||||
@@ -70,7 +70,7 @@ func TestSealRefuses(t *testing.T) {
|
|||||||
"binary": {Header{Key: key}, pass, []File{{Path: ".env", Data: []byte{0xff, 0xfe}}}, "isn't text"},
|
"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"},
|
"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); err == nil || !strings.Contains(err.Error(), c.want) {
|
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)
|
t.Errorf("%s: %v, want %q", name, err, c.want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,8 +89,31 @@ func random(n int) string {
|
|||||||
return b.String()
|
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) {
|
func TestReadGarbled(t *testing.T) {
|
||||||
link, err := Seal(Header{Key: key}, pass, []File{{Path: ".env", Data: []byte("A=1")}})
|
link, err := Seal(Header{Key: key}, pass, []File{{Path: ".env", Data: []byte("A=1")}}, Rules{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"git.kebcloud.no/keb/lazysecrets/internal/commands"
|
"git.kebcloud.no/keb/lazysecrets/internal/commands"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/keys"
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/keys"
|
||||||
@@ -84,6 +85,8 @@ type Options struct {
|
|||||||
// ShareKey is the config's share.key: the secret that encrypts shared
|
// ShareKey is the config's share.key: the secret that encrypts shared
|
||||||
// env files, for repos whose .lazysecrets.yaml names none.
|
// env files, for repos whose .lazysecrets.yaml names none.
|
||||||
ShareKey string
|
ShareKey string
|
||||||
|
// ShareRules are what a key's value must be to share with it.
|
||||||
|
ShareRules share.Rules
|
||||||
// Dir is the folder lazysecrets runs in: it goes back to where it was
|
// Dir is the folder lazysecrets runs in: it goes back to where it was
|
||||||
// left there, unless it was started on a link. "" doesn't.
|
// left there, unless it was started on a link. "" doesn't.
|
||||||
Dir string
|
Dir string
|
||||||
@@ -274,6 +277,7 @@ type Model struct {
|
|||||||
open *ref.Ref // reference to open at startup
|
open *ref.Ref // reference to open at startup
|
||||||
|
|
||||||
shareKey string // the config's share.key
|
shareKey string // the config's share.key
|
||||||
|
shareRules share.Rules // and share.keyRules
|
||||||
|
|
||||||
// A lazysecrets:// link found on the clipboard, offered in the footer.
|
// A lazysecrets:// link found on the clipboard, offered in the footer.
|
||||||
dismissedLink string // the last link offered: not offered again
|
dismissedLink string // the last link offered: not offered again
|
||||||
@@ -460,7 +464,7 @@ func New(providers []provider.Provider, opts Options) Model {
|
|||||||
m.spinning = true
|
m.spinning = true
|
||||||
|
|
||||||
m.cachePath, m.placeDir, m.sourcesAt = opts.CacheFile, opts.Dir, map[string]time.Time{}
|
m.cachePath, m.placeDir, m.sourcesAt = opts.CacheFile, opts.Dir, map[string]time.Time{}
|
||||||
m.shareKey = opts.ShareKey
|
m.shareKey, m.shareRules = opts.ShareKey, opts.ShareRules
|
||||||
m.configFiles, m.statePath, m.changelog = opts.ConfigFiles, opts.StatePath, changelogFields(opts.Changelog)
|
m.configFiles, m.statePath, m.changelog = opts.ConfigFiles, opts.StatePath, changelogFields(opts.Changelog)
|
||||||
if m.cachePath != "" {
|
if m.cachePath != "" {
|
||||||
m.loadCacheFile()
|
m.loadCacheFile()
|
||||||
|
|||||||
@@ -158,14 +158,14 @@ func (m *Model) pickPasted(text string) tea.Cmd {
|
|||||||
|
|
||||||
// seal reads the key and encrypts the files with it.
|
// seal reads the key and encrypts the files with it.
|
||||||
func (m *Model) seal(s sharing) tea.Cmd {
|
func (m *Model) seal(s sharing) tea.Cmd {
|
||||||
read := m.keyReader()
|
read, rules := m.keyReader(), m.shareRules
|
||||||
m.flash = flash{text: "encrypting with " + s.key.Name + "…"}
|
m.flash = flash{text: "encrypting with " + s.key.Name + "…"}
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
v, version, err := read(s.key)
|
v, version, err := read(s.key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sealedMsg{err: fmt.Errorf("reading the key %s: %w", s.key.Name, err)}
|
return sealedMsg{err: fmt.Errorf("reading the key %s: %w", s.key.Name, err)}
|
||||||
}
|
}
|
||||||
link, err := share.Seal(share.Header{Key: share.Pin(s.key, version), Repo: s.repo.Detail}, []byte(v.Reveal()), s.files)
|
link, err := share.Seal(share.Header{Key: share.Pin(s.key, version), Repo: s.repo.Detail}, []byte(v.Reveal()), s.files, rules)
|
||||||
return sealedMsg{link: link, n: len(s.files), err: err}
|
return sealedMsg{link: link, n: len(s.files), err: err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ import (
|
|||||||
|
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/share"
|
||||||
)
|
)
|
||||||
|
|
||||||
// scryptWait is how long the driver waits for encrypting and decrypting,
|
// scryptWait is how long the driver waits for encrypting and decrypting,
|
||||||
// which take about a second on purpose.
|
// which take about a second on purpose.
|
||||||
const scryptWait = 20 * time.Second
|
const scryptWait = 20 * time.Second
|
||||||
|
|
||||||
|
// demoRules take the demo key, made up to be remembered.
|
||||||
|
var demoRules = share.Rules{MinLength: 16, MinClasses: 1}
|
||||||
|
|
||||||
// cloneWith makes a clone of git.example.com/team/app holding files.
|
// cloneWith makes a clone of git.example.com/team/app holding files.
|
||||||
func cloneWith(t *testing.T, files map[string]string) string {
|
func cloneWith(t *testing.T, files map[string]string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
@@ -35,7 +39,7 @@ func cloneWith(t *testing.T, files map[string]string) string {
|
|||||||
func shareFrom(t *testing.T, root string, paste bool) string {
|
func shareFrom(t *testing.T, root string, paste bool) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
clip := &fakeClipboard{}
|
clip := &fakeClipboard{}
|
||||||
d := newDriverOpts(t, Options{Clipboard: clip}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
|
d := newDriverOpts(t, Options{Clipboard: clip, ShareRules: demoRules}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
|
||||||
d.wait = scryptWait
|
d.wait = scryptWait
|
||||||
if src, ok := d.m.selectedSource(); !ok || src.ID != root {
|
if src, ok := d.m.selectedSource(); !ok || src.ID != root {
|
||||||
t.Fatalf("selected %v, want the repo", src.ID)
|
t.Fatalf("selected %v, want the repo", src.ID)
|
||||||
|
|||||||
Reference in new issue
Block a user