Skip to content

Commit e123837

Browse files
committed
feat(clip,show)!: req magic string in TOML entries
Do not show full TOML documents by default. This makes it more difficult to reveal an OTP URI by accident. v0.18.0
1 parent a9bc3a9 commit e123837

File tree

4 files changed

+94
-24
lines changed

4 files changed

+94
-24
lines changed

README.md

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,17 +247,35 @@ pago can store and retrieve structured data in the [TOML](https://toml.io/) form
247247
This is useful for storing multiple related values in a single entry, such as API keys, usernames, and URLs.
248248

249249
To create a TOML entry, use `pago add --multiline` and provide TOML content on standard input.
250+
The content must start with the string `# TOML`.
250251

251252
```shell
252253
pago add -m services/my-api <<EOF
254+
# TOML
253255
user = "jdoe"
254-
key = "abc-123"
256+
password = "abcdef"
257+
token = "tok-123"
255258
url = "https://api.example.com"
256259
numbers = [1, 1, 2, 3, 5]
257260
EOF
258261
```
259262

260-
You can then retrieve individual values from the TOML entry using the `-k`/`--key` option with the commands `show`, `clip`, and `pick`, or with the `key` command.
263+
When you `show` or `clip` a TOML entry without specifying a key, pago will use a default key.
264+
The default key is `password`.
265+
You can specify a different default key by adding a key `default` to the TOML entry.
266+
267+
```shell
268+
pago add -m services/my-api-custom-default <<EOF
269+
# TOML
270+
default = "api-key"
271+
api-key = "xyz-456"
272+
EOF
273+
274+
pago show services/my-api-custom-default
275+
# => xyz-456
276+
```
277+
278+
You can retrieve other values from the TOML entry using the `-k`/`--key` option with the commands `show`, `clip`, and `pick`, or with the `key` command.
261279

262280
```shell
263281
# Show the user from the TOML entry.
@@ -280,15 +298,15 @@ When an entry is parsed as TOML, pago can retrieve scalar values (strings, numbe
280298
Arrays and scalars other than strings are encoded as TOML for output.
281299
pago cannot retrieve tables.
282300

283-
If you show or clip a TOML entry without `--key`, the entire TOML document is returned.
284-
285301
### TOTP
286302

287303
pago can generate [time-based one-time passwords (TOTP)](https://en.wikipedia.org/wiki/Time-based_one-time_password) from a [TOML entry](#toml-entries).
288304
To use this feature, store the `otpauth://` URI in a key named `otp`.
305+
The entry must start with `# TOML`.
289306

290307
```shell
291308
pago add -m services/my-service <<EOF
309+
# TOML
292310
user = "jdoe"
293311
otp = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example"
294312
EOF

cmd/pago/main.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,12 @@ func getPassword(agentExecutable string, agentExpire time.Duration, agentMemlock
360360
return "", err
361361
}
362362

363-
if key == "" {
363+
isToml := strings.HasPrefix(content, "# TOML")
364+
if !isToml {
365+
if key != "" {
366+
return "", fmt.Errorf("entry %q is not a TOML entry; cannot use --key", name)
367+
}
368+
364369
return content, nil
365370
}
366371

@@ -369,6 +374,18 @@ func getPassword(agentExecutable string, agentExpire time.Duration, agentMemlock
369374
return "", fmt.Errorf("failed to parse entry as TOML: %w", err)
370375
}
371376

377+
if key == "" {
378+
key = "password"
379+
380+
if defaultKey, ok := data["default"]; ok {
381+
if defaultKeyStr, ok := defaultKey.(string); ok {
382+
key = defaultKeyStr
383+
} else {
384+
return "", fmt.Errorf(`key "default" must have string value`)
385+
}
386+
}
387+
}
388+
372389
value, ok := data[key]
373390
if !ok {
374391
return "", fmt.Errorf("key %q not found in entry %q", key, name)

config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const (
2222
ExitMemlockError = 3
2323
FilePerms = 0o600
2424
NameInvalidChars = `[\n]`
25-
Version = "0.17.0"
25+
Version = "0.18.0"
2626
WaitForSocket = 3 * time.Second
2727

2828
DefaultAgent = "pago-agent"

test/e2e_test.go

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -704,9 +704,10 @@ func TestShowKey(t *testing.T) {
704704

705705
_, err := withPagoDir(func(dataDir string) (string, error) {
706706
// Add a TOML entry.
707-
cmd := exec.Command(commandPago, "--dir", dataDir, "add", "toml-test", "--multiline")
708-
cmd.Stdin = strings.NewReader(`
707+
cmd := exec.Command(commandPago, "--dir", dataDir, "add", "toml", "--multiline")
708+
cmd.Stdin = strings.NewReader(`# TOML
709709
# Comment.
710+
password = "hunter2"
710711
foo = "string"
711712
bar = 5
712713
@@ -723,17 +724,43 @@ qux = {"key" = "value"}
723724
return stdout.String() + "\n" + stderr.String(), err
724725
}
725726

727+
// Add a TOML entry with a custom default key.
728+
cmd = exec.Command(commandPago, "--dir", dataDir, "add", "toml-default", "--multiline")
729+
cmd.Stdin = strings.NewReader(`# TOML
730+
default = "foo"
731+
foo = "secret"
732+
`)
733+
cmd.Stdout = &stdout
734+
cmd.Stderr = &stderr
735+
err = cmd.Run()
736+
if err != nil {
737+
return stdout.String() + "\n" + stderr.String(), err
738+
}
739+
740+
// Add a non-TOML entry.
741+
_, _, err = runCommandEnv(
742+
[]string{"PAGO_DIR=" + dataDir},
743+
"add", "not-toml", "--random",
744+
)
745+
if err != nil {
746+
return "", err
747+
}
748+
726749
testCases := []struct {
750+
entry string
727751
key string
728752
expected string
729753
wantErr bool
730754
}{
731-
{"foo", "string", false},
732-
{"bar", "5", false},
733-
{"phi", "1.68", false},
734-
{"baz", "[1, 2, 3, true, false]", false},
735-
{"quux", "", true},
736-
{"nonexistent", "", true},
755+
{"toml", "", "hunter2", false},
756+
{"toml", "foo", "string", false},
757+
{"toml", "bar", "5", false},
758+
{"toml", "phi", "1.68", false},
759+
{"toml", "baz", "[1, 2, 3, true, false]", false},
760+
{"toml", "quux", "", true},
761+
{"toml", "nonexistent", "", true},
762+
{"toml-default", "", "secret", false},
763+
{"not-toml", "foo", "", true},
737764
}
738765

739766
for _, tc := range testCases {
@@ -746,27 +773,32 @@ qux = {"key" = "value"}
746773

747774
buf.Reset()
748775

749-
cmd := exec.Command(commandPago, "--dir", dataDir, "--socket", "", "show", "--key", tc.key, "toml-test")
776+
var cmd *exec.Cmd
777+
if tc.key == "" {
778+
cmd = exec.Command(commandPago, "--dir", dataDir, "--socket", "", "show", tc.entry)
779+
} else {
780+
cmd = exec.Command(commandPago, "--dir", dataDir, "--socket", "", "show", "--key", tc.key, tc.entry)
781+
}
750782
cmd.Stdin = c.Tty()
751783
cmd.Stdout = &buf
752784
cmd.Stderr = c.Tty()
753785

754786
err = cmd.Start()
755787
if err != nil {
756-
return "", fmt.Errorf("failed to start command for key %q: %w", tc.key, err)
788+
return "", fmt.Errorf("failed to start command for entry %q key %q: %w", tc.entry, tc.key, err)
757789
}
758790

759791
_, _ = c.ExpectString("Enter password")
760792
_, _ = c.SendLine(password)
761793

762794
err = cmd.Wait()
763795
if (err != nil) != tc.wantErr {
764-
return "", fmt.Errorf("command failed for key %q: %w", tc.key, err)
796+
return "", fmt.Errorf("command failed for entry %q key %q: %w", tc.entry, tc.key, err)
765797
}
766798

767799
output := strings.TrimSpace(buf.String())
768-
if output != tc.expected {
769-
return "", fmt.Errorf("for key %q, expected %q, got %q", tc.key, tc.expected, output)
800+
if !tc.wantErr && output != tc.expected {
801+
return "", fmt.Errorf("for entry %q key %q, expected %q, got %q", tc.entry, tc.key, tc.expected, output)
770802
}
771803
}
772804

@@ -782,8 +814,9 @@ func TestKeyCmd(t *testing.T) {
782814

783815
_, err := withPagoDir(func(dataDir string) (string, error) {
784816
// Add a TOML entry.
785-
cmd := exec.Command(commandPago, "--dir", dataDir, "add", "toml-test", "--multiline")
786-
cmd.Stdin = strings.NewReader(`foo = "string"`)
817+
cmd := exec.Command(commandPago, "--dir", dataDir, "add", "toml", "--multiline")
818+
cmd.Stdin = strings.NewReader(`# TOML
819+
foo = "string"`)
787820
var stdout, stderr bytes.Buffer
788821
cmd.Stdout = &stdout
789822
cmd.Stderr = &stderr
@@ -801,7 +834,7 @@ func TestKeyCmd(t *testing.T) {
801834

802835
buf.Reset()
803836

804-
cmd = exec.Command(commandPago, "--dir", dataDir, "--socket", "", "key", "toml-test", "foo")
837+
cmd = exec.Command(commandPago, "--dir", dataDir, "--socket", "", "key", "toml", "foo")
805838
cmd.Stdin = c.Tty()
806839
cmd.Stdout = &buf
807840
cmd.Stderr = c.Tty()
@@ -837,7 +870,8 @@ func TestShowOTP(t *testing.T) {
837870
// Add a TOML entry with a 6-digit otpauth URI.
838871
cmd := exec.Command(commandPago, "--dir", dataDir, "add", "otp-test-6digit", "--multiline")
839872
// Example URI from https://github.com/google/google-authenticator/wiki/Key-Uri-Format
840-
cmd.Stdin = strings.NewReader(`otp = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example"`)
873+
cmd.Stdin = strings.NewReader(`# TOML
874+
otp = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example"`)
841875
var stdout, stderr bytes.Buffer
842876
cmd.Stdout = &stdout
843877
cmd.Stderr = &stderr
@@ -848,7 +882,8 @@ func TestShowOTP(t *testing.T) {
848882

849883
// Add another TOML entry with an 8-digit otpauth URI.
850884
cmd = exec.Command(commandPago, "--dir", dataDir, "add", "otp-test-8digit", "--multiline")
851-
cmd.Stdin = strings.NewReader(`otp = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=sha256&digits=8"`)
885+
cmd.Stdin = strings.NewReader(`# TOML
886+
otp = "otpauth://totp/Example:[email protected]?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=sha256&digits=8"`)
852887
cmd.Stdout = &stdout
853888
cmd.Stderr = &stderr
854889
err = cmd.Run()

0 commit comments

Comments
 (0)