feat: share env files as encrypted links #54

Merged
keb merged 25 commits from share-files into main 2026-10-13 14:55:22 +02:00
Owner

Share .env files as an encrypted lazysecrets://share/… link. The files are encrypted with a secret's value, so anyone who can read that secret can open the link.

Sharing: S → f on a repo (all its plain env files), an env file, or a key in one (its file).

  • The key is share.key from the repo's .lazysecrets.yaml, else from the config. It can be a lazysecrets:// link, a Key Vault URL or an @Microsoft.KeyVault(...) reference.
  • Without share.key, a picker built on Go to opens, titled "Share .env", with "Pick a secret to encrypt with" in its own border. It lists only secrets, leaving out values from plain .env files. Pasting a reference picks it. Picking shares at once, and the picker stays open until the link is copied. If the secret can't be used, the reason shows in red in the picker: it can't be read, or it doesn't meet share.keyRules (which rule failed is named). esc cancels.
  • share.keyRules (defaults minLength: 32, minClasses: 2; never under 16) applies when sharing, not when opening.
  • On Azure, the link pins the key's version, so rotating the key doesn't break old links, and disabling a version revokes them.
  • The command line: lazysecrets share [--key <secret>] <env files>.

Opening works by paste, ctrl+o, the clipboard offer, the link handler or lazysecrets <link>.

  1. It first says which repo the link is from, how many files it holds and which key it uses, and decrypts only after o.
  2. It saves into the repo's clone, found by its remote, showing which files are new, which would replace one there, and which git doesn't ignore.
  3. Without a clone, it asks for a folder and saves straight away. The prompt stays open, with the reason in red, until the files are written.
  4. If decryption fails, a popup says why: the first line of the reason, r for the whole message, g to go to the key's secret. A key that has changed is named by where it lives: a Databricks or SOPS value changed in place, an Azure current version that isn't the one the link was made with, or a pinned version, meaning the link may be damaged.

Format (internal/share): a version byte, then a JSON header with the key, the repo and the file count, then an age file (scrypt 2^15, with the key's value as passphrase) holding the files as deflated JSON, all as unpadded base64url. Links are capped at 16 KB. A future format gets a new version, and old ones stay readable.

Safety:

  • A link can only write env files, inside the folder: never in .git, never through a symlink.
  • New files are created with mode 0600.
  • Opening accepts an scrypt work factor of at most 2^20, and unpacks at most 1 MB.

Along the way:

  • Opening a link to a secret ends on its details, as enter would.
  • o on an Azure secret opens its portal page, and in the versions view, that version's page.
  • The details pane follows its secret's list when it reloads in the background. Before, it kept what it showed first, including an old portal address.
  • ref.Read is shared by get and sharing, with Azure VersionedValue for pinning.
  • provider.Contents lets the demo repos share their env files. --demo has a share key (kv-shared-prod / env-share), so the whole flow works there.
  • New dependency: filippo.io/age.

Tested end to end in the TUI and the demo, the CLI, and the share package. In a real vault: version pinning, opening into a clone, and revocation by disabling the version.

🤖 Generated with Claude Code

Share `.env` files as an encrypted `lazysecrets://share/…` link. The files are encrypted with a secret's value, so anyone who can read that secret can open the link. **Sharing:** `S` → `f` on a repo (all its plain env files), an env file, or a key in one (its file). - **The key** is `share.key` from the repo's `.lazysecrets.yaml`, else from the config. It can be a `lazysecrets://` link, a Key Vault URL or an `@Microsoft.KeyVault(...)` reference. - **Without `share.key`,** a picker built on Go to opens, titled "Share .env", with "Pick a secret to encrypt with" in its own border. It lists only secrets, leaving out values from plain `.env` files. Pasting a reference picks it. Picking shares at once, and the picker stays open until the link is copied. If the secret can't be used, the reason shows in red in the picker: it can't be read, or it doesn't meet `share.keyRules` (which rule failed is named). `esc` cancels. - **`share.keyRules`** (defaults `minLength: 32`, `minClasses: 2`; never under 16) applies when sharing, not when opening. - **On Azure,** the link pins the key's version, so rotating the key doesn't break old links, and disabling a version revokes them. - **The command line:** `lazysecrets share [--key <secret>] <env files>`. **Opening** works by paste, `ctrl+o`, the clipboard offer, the link handler or `lazysecrets <link>`. 1. It first says which repo the link is from, how many files it holds and which key it uses, and decrypts only after `o`. 2. It saves into the repo's clone, found by its remote, showing which files are new, which would replace one there, and which git doesn't ignore. 3. Without a clone, it asks for a folder and saves straight away. The prompt stays open, with the reason in red, until the files are written. 4. If decryption fails, a popup says why: the first line of the reason, `r` for the whole message, `g` to go to the key's secret. A key that has changed is named by where it lives: a Databricks or SOPS value changed in place, an Azure current version that isn't the one the link was made with, or a pinned version, meaning the link may be damaged. **Format** (`internal/share`): a version byte, then a JSON header with the key, the repo and the file count, then an age file (scrypt 2^15, with the key's value as passphrase) holding the files as deflated JSON, all as unpadded base64url. Links are capped at 16 KB. A future format gets a new version, and old ones stay readable. **Safety:** - A link can only write env files, inside the folder: never in `.git`, never through a symlink. - New files are created with mode 0600. - Opening accepts an scrypt work factor of at most 2^20, and unpacks at most 1 MB. **Along the way:** - Opening a link to a secret ends on its details, as `enter` would. - `o` on an Azure secret opens its portal page, and in the versions view, that version's page. - The details pane follows its secret's list when it reloads in the background. Before, it kept what it showed first, including an old portal address. - `ref.Read` is shared by `get` and sharing, with Azure `VersionedValue` for pinning. - `provider.Contents` lets the demo repos share their env files. `--demo` has a share key (`kv-shared-prod / env-share`), so the whole flow works there. - New dependency: `filippo.io/age`. Tested end to end in the TUI and the demo, the CLI, and the `share` package. In a real vault: version pinning, opening into a clone, and revocation by disabling the version. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
keb added 1 commit 2026-10-13 11:59:50 +02:00
feat: share env files as encrypted links
CI / Checks (pull_request) Failing after 10s
PR title / Conventional title (pull_request) Successful in 0s
4b2f685921
S → f on a repo, an env file or a key in one copies a
lazysecrets://share link holding the env files, encrypted (age) with
a secret's value: whoever can read that secret can open it. The key is
share.key in the repo's .lazysecrets.yaml, else in the config, else
picked when sharing; on Azure the link pins its version. Opening one
says what it holds, decrypts when asked, and saves the files in the
clone of the repo (or a folder chosen), replacing existing ones only
when asked. lazysecrets share prints a link from the command line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:00:34 +02:00
fix: staticcheck and go mod tidy
CI / Checks (pull_request) Successful in 26s
PR title / Conventional title (pull_request) Successful in 1s
4d57c19772
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:09:08 +02:00
feat: pick the share key in Go to, or by reference
CI / Checks (pull_request) Successful in 38s
PR title / Conventional title (pull_request) Successful in 1s
bc691d786a
Sharing without share.key opens Go to as a picker of secrets instead
of asking for a link: it lists only secrets, enter picks one, and a
pasted reference picks it too. The key, there and in share.key or
--key, can be a secret's lazysecrets:// link, Key Vault URL or
@Microsoft.KeyVault(...) reference.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:10:18 +02:00
docs: items can be shared as links, not have them
CI / Checks (pull_request) Successful in 35s
PR title / Conventional title (pull_request) Successful in 2s
68fc76d938
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:13:16 +02:00
feat: the key picker says what sharing does, and skips plain env values
CI / Checks (pull_request) Successful in 37s
PR title / Conventional title (pull_request) Successful in 1s
b46991c7c2
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:16:12 +02:00
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
0bfa2a3141
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>
keb added 1 commit 2026-10-13 12:22:24 +02:00
feat: share popups draw repos and files like everywhere else
CI / Checks (pull_request) Successful in 38s
PR title / Conventional title (pull_request) Successful in 2s
cd265a632e
repoLabel draws a repo, or a file in it, with pathLabel: the repo icon
and colour. The share popups use it for what's shared, where it's
from and where it's saved, and are titled for what's shared
("Share .env", "Share 2 env files"), the key picker too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:24:10 +02:00
feat: the key picker's tabs go under its heading, with help text
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s
8cd0003f8c
The share popup is titled for what's shared; above the search, "Pick
a secret to encrypt with" says the recipient needs read access to it,
with Go to's tabs under it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:36:07 +02:00
feat: picking the key shares, with no second popup
CI / Checks (pull_request) Successful in 40s
PR title / Conventional title (pull_request) Successful in 2s
5c97a8e249
The picker already shows what's shared. The confirmation stays for a
key from share.key, which wasn't picked. The share format's version
is documented as kept readable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:40:19 +02:00
fix: opening a share link reads right for one file or several
CI / Checks (pull_request) Successful in 37s
PR title / Conventional title (pull_request) Successful in 2s
176600be54
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:42:34 +02:00
feat: the key picker has its own border in the share popup
CI / Checks (pull_request) Successful in 40s
PR title / Conventional title (pull_request) Successful in 2s
8d65b60e69
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 12:48:06 +02:00
fix: the key picker leaves share.key to the docs
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s
86fb79a915
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:12:00 +02:00
feat: share env files in --demo
CI / Checks (pull_request) Successful in 42s
PR title / Conventional title (pull_request) Successful in 2s
777c8cb532
Providers whose containers are files give their contents through
provider.Contents, which sharing reads instead of the disk. The demo
repos build their env files from their keys, and the second demo
account has a key to share with (kv-shared-prod / env-share), so S → f
and opening the link work in --demo; the demo repos aren't on disk,
so opening asks where to save.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:17:04 +02:00
fix: a demo repo's SOPS value can be the share key
CI / Checks (pull_request) Successful in 41s
PR title / Conventional title (pull_request) Successful in 2s
166790d825
The demo provider finds repos by their remote, as the repo provider
does, so a key in a demo repo's SOPS file can be read. Opening a
share link uses the repo's clone only when it's on disk (the demo's
aren't), else asks where to save. The demo GitHub client secret is 40
characters, like real ones, so it passes share.keyRules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:24:22 +02:00
feat: a key that can't be used is explained in the picker
CI / Checks (pull_request) Successful in 40s
PR title / Conventional title (pull_request) Successful in 1s
027e748c92
The key picker stays open until the link is made, saying "encrypting
with …" meanwhile. A key that can't be read, or doesn't keep to
share.keyRules, is shown in it in red with which rule it misses
("its value has 28 characters; share.keyRules needs 32
(minLength)"), to pick another.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:29:04 +02:00
fix: a refused share key says what failed, not how to make one
CI / Checks (pull_request) Successful in 42s
PR title / Conventional title (pull_request) Successful in 2s
80332680e5
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:31:35 +02:00
perf: share links use scrypt 2^15, about 40 ms each way
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s
0757898d61
Keys keeping to share.keyRules are random and long, so age's 2^18
(a third of a second to share and to open) bought little.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:35:01 +02:00
feat: a chosen folder saves at once, and the prompt stays until it has
CI / Checks (pull_request) Successful in 40s
PR title / Conventional title (pull_request) Successful in 2s
25dbb38842
Choosing where to save a share link's files saves them right away,
unless one would replace a file there, which still asks. The folder
prompt stays open until they're written: a folder that isn't there,
or a save that fails, is said under it in red, to fix the path. Its
footer says "enter save", and a file git doesn't ignore is named in
the status line after saving.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:42:48 +02:00
refactor: tidy up sharing after the round of tweaks
CI / Checks (pull_request) Successful in 41s
PR title / Conventional title (pull_request) Successful in 2s
61de5e616d
- Go to's picker settings are one picker struct, set while picking.
- Opening a share link plans the save, then asks or saves; no more
  offerWrite doing either by a flag.
- Closing the picker while it encrypts cancels the share, and a
  second pick meanwhile is ignored.
- Favourites and Recent leave out plain env values as keys too.
- share.key in the config is checked with share.ParseKey, so
  {{secrets/…}} is refused there already.
- A key that can't be read reads the same opening as sharing
  ("can't use …"); KeyError moved to share.go, maxFiles is
  maxUnpacked (bytes), and a stray comment is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 13:59:44 +02:00
feat: a share link that can't be decrypted says why in a popup
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s
2a3379b1c1
Where its offer was, instead of the footer: the first line of the
reason, r for the whole message (Key Vault's refusals list what to
check), g to go to the key's secret at the link's version, and x to
dismiss.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 14:06:16 +02:00
feat: opening a link to a secret ends on its details
CI / Checks (pull_request) Successful in 35s
PR title / Conventional title (pull_request) Successful in 1s
7d6a095e42
As enter on it would, instead of on the vault's list with the secret
selected: ctrl+o, a paste, a clicked link and the share failure
popup's g. Going back to where lazysecrets was left, and Go to, stay
as they were.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 14:27:07 +02:00
fix: o on an Azure secret opens its page in the portal
CI / Checks (pull_request) Successful in 38s
PR title / Conventional title (pull_request) Successful in 2s
8757284b0c
Not the vault's list of secrets: the secret's own page (asset/
Microsoft_Azure_KeyVault/Secret/<id>), and in the versions view, the
version's (<id>/<version>), keys' and certificates' versions too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 14:30:24 +02:00
feat: a key that no longer decrypts a share link says why
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 2s
40b90df68c
By where it lives: a Databricks secret or SOPS value has changed
since the link was made, an Azure secret's current version isn't the
link's (unpinned), and a pinned version can't have changed, so the
link may be damaged. The popup wraps the reason instead of cutting
it; r is for what Key Vault adds to check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 14:51:00 +02:00
fix: the details pane follows its secret's list when it reloads
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 1s
db8ce90ee7
A cached list is shown at once and reloaded in the background; the
selected secret's details stayed as first shown, old web page
included, until another secret was selected. When the reloaded list
has it changed, the details pane takes the new listing (a value shown
stays shown) and describes it afresh: its cached details came from
the old one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
keb added 1 commit 2026-10-13 14:54:31 +02:00
Merge branch 'main' into share-files
CI / Checks (pull_request) Successful in 39s
PR title / Conventional title (pull_request) Successful in 1s
869b201695
keb merged commit 4377eb98bf into main 2026-10-13 14:55:22 +02:00
keb deleted branch share-files 2026-10-13 14:55:22 +02:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: keb/lazysecrets#54