feat: share.keyRules, how long and varied a share key must be
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s

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>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-13 12:16:12 +02:00
1 parent b46991c7c2
commit 0bfa2a3141
13 files changed
+156 -21

No files matched your search

+6 -2
View File
@@ -24,6 +24,7 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/commands"
"git.kebcloud.no/keb/lazysecrets/internal/provider"
"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/ui/icons"
"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
// env files, for repos whose .lazysecrets.yaml names none.
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
// left there, unless it was started on a link. "" doesn't.
Dir string
@@ -273,7 +276,8 @@ type Model struct {
linked []provider.Container // vaults opened from links, this session
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.
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.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)
if m.cachePath != "" {
m.loadCacheFile()
+2 -2
View File
@@ -158,14 +158,14 @@ func (m *Model) pickPasted(text string) tea.Cmd {
// seal reads the key and encrypts the files with it.
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 + "…"}
return func() tea.Msg {
v, version, err := read(s.key)
if err != nil {
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}
}
}
+5 -1
View File
@@ -12,12 +12,16 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
"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,
// which take about a second on purpose.
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.
func cloneWith(t *testing.T, files map[string]string) string {
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 {
t.Helper()
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
if src, ok := d.m.selectedSource(); !ok || src.ID != root {
t.Fatalf("selected %v, want the repo", src.ID)