2 Commits
Author SHA1 Message Date
sneak 73ec3035c0 Hold the encrypt and decrypt result in memory until it is written (closes #143)
check / check (push) Canceled after 0s
With --output, secret encrypt and secret decrypt emptied that file before
reading any input, so a failed command left it empty, and encrypting a file
onto itself gave the encryption of nothing.

With --output they now hold the result in memory and open --output, with the
same call as before, only once encryption or decryption has succeeded. A
failure to encrypt or decrypt leaves --output untouched, and --input can be the
same file. A failure while writing --output, such as a full disk, can leave it
empty or partly written; an error from closing it is reported. Without
--output the result still goes to stdout as it is produced.

Model: opus-5-5
2026-10-15 04:49:49 +00:00
clawbot 42f4961937 Print every time in UTC, whatever the time zone (closes #169)
check / check (push) Canceled after 0s
secret list, secret version list, secret unlocker list and secret info
printed times in the machine's time zone without naming it, so one vault
showed different times on two machines. Entry() now sets time.Local to
UTC before anything else, as AGENTS.md policy 6 asks, so every time the
program prints or writes is in UTC: --json times end in Z, and a new
version's name takes the UTC date. The README says so.

A test runs the built secret list with TZ=America/New_York and checks the
table and the JSON against the UTC time set on the secret's current file.

Rule suppressed: gosmopolitan, which flags any use of time.Local.

Model: opus-5-5
2026-10-15 06:48:51 +02:00
3 changed files with 64 additions and 1 deletions
+5 -1
View File
@@ -67,6 +67,10 @@ and unlocker set. This allows for complete isolation between different contexts
## Commands Reference
Every time `secret` prints, in a table or in `--json` output, is in UTC,
whatever `TZ` or the system's time zone is set to. The date in a version's name
is the UTC date too.
### Confirmation Before Removal
`secret rm`, `secret version rm`, `secret vault remove` and
@@ -318,7 +322,7 @@ With `--output`, `secret encrypt` and `secret decrypt` hold the result in memory
and write it to `--output` only once encryption or decryption has succeeded, so
a failure to encrypt or decrypt leaves `--output` untouched. A failure while
writing `--output`, such as a full disk, can leave it empty or partly written.
The command needs about as much memory as the result.
The command can need up to about four times as much memory as the result.
#### `secret encrypt <secret-name> [--input=file] [--output=file]`
+54
View File
@@ -3,6 +3,9 @@ package cli_test
import (
"bytes"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"testing"
"time"
@@ -125,6 +128,57 @@ func TestMovedSecretKeepsItsTimes(t *testing.T) {
"updated %s, then %s", beforeMove.UpdatedAt, afterMove.UpdatedAt)
}
// TestListedTimesAreUTC is a regression test for
// https://git.eeqj.de/sneak/secret/issues/169, where times were printed
// in the local time zone. It runs `secret list` on the built binary with
// TZ set to New York, where the secret's last update, 03:04:05 UTC on 2
// January, is 22:04:05 the day before.
func TestListedTimesAreUTC(t *testing.T) {
t.Parallel()
stateDir := t.TempDir()
vlt, err := vault.CreateVault(
afero.NewOsFs(), stateDir, "default", testMnemonicBuffer(t), nil)
require.NoError(t, err)
vlt.Mnemonic = testMnemonicBuffer(t)
err = vlt.AddSecret("x", memguard.NewBufferFromBytes([]byte("one")), false)
require.NoError(t, err)
updated := time.Date(2026, time.January, 2, 3, 4, 5, 0, time.UTC)
current := filepath.Join(stateDir, "vaults.d", "default", "secrets.d", "x",
"current")
require.NoError(t, os.Chtimes(current, updated, updated))
tests := []struct {
flags []string
want string
}{
{flags: nil, want: "2026-01-02 03:04"},
{
flags: []string{"--json"},
want: `"updated_at": "2026-01-02T03:04:05Z"`,
},
}
for _, tt := range tests {
args := append([]string{"list"}, tt.flags...)
//nolint:gosec // G204: test executes the freshly built secret binary
cmd := exec.CommandContext(t.Context(), secretBinaryPath(t), args...)
cmd.Env = []string{
secret.EnvStateDir + "=" + stateDir,
"TZ=America/New_York",
"PATH=" + os.Getenv("PATH"),
}
output, err := cmd.CombinedOutput()
require.NoError(t, err, "%v: %s", args, output)
assert.Contains(t, string(output), tt.want, "%v", args)
}
}
// listSecrets returns the output of `secret list`, or of
// `secret list --json` when jsonOutput is set.
func listSecrets(t *testing.T, c *cli.Instance, jsonOutput bool) string {
+5
View File
@@ -2,6 +2,7 @@ package cli
import (
"os"
"time"
"github.com/awnumar/memguard"
"github.com/spf13/cobra"
@@ -14,6 +15,10 @@ import (
// every memguard buffer before it returns, so the caller must do nothing
// but exit with the code.
func Entry() int {
// Every time secret prints or writes is in UTC, whatever TZ or the
// system's time zone is set to.
time.Local = time.UTC //nolint:gosmopolitan // this makes local time UTC
raiseLockedMemoryLimit()
// On SIGINT or SIGTERM memguard runs this function, wipes every buffer