Skip to content

Commit 2b5e5e2

Browse files
nikicatclaude
andcommitted
feat: add gopass-secret get <type> command for secret lookup
Searches by type (auto-supplying xdg:schema) plus optional attribute filters, prints the first match's secret value to stdout. Analogous to `secret-tool lookup` but with gopass-secret's type system. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6357731 commit 2b5e5e2

2 files changed

Lines changed: 160 additions & 0 deletions

File tree

cmd/gopass-secret/get.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strings"
9+
10+
"github.com/godbus/dbus/v5"
11+
12+
dbustypes "github.com/nikicat/gopass-secret-service/internal/dbus"
13+
"github.com/nikicat/gopass-secret-service/internal/schema"
14+
)
15+
16+
func runGet(args []string) {
17+
if len(args) == 0 {
18+
printGetUsage()
19+
os.Exit(1)
20+
}
21+
22+
typeName := args[0]
23+
if typeName == "-h" || typeName == "--help" {
24+
printGetUsage()
25+
return
26+
}
27+
st, ok := schema.Get(typeName)
28+
if !ok {
29+
fmt.Fprintf(os.Stderr, "Unknown secret type: %s\n\n", typeName)
30+
printGetUsage()
31+
os.Exit(1)
32+
}
33+
34+
fs := flag.NewFlagSet("get "+typeName, flag.ExitOnError)
35+
36+
// Register type-specific attribute flags (all optional for search)
37+
attrFlags := make(map[string]*string)
38+
for _, attr := range st.Attributes {
39+
attrFlags[attr.Name] = fs.String(attr.Name, "", attr.Help)
40+
}
41+
42+
mustParse(fs, args[1:])
43+
44+
// Build attributes map
45+
attrs := map[string]string{
46+
"xdg:schema": st.Schema,
47+
}
48+
49+
for name, val := range attrFlags {
50+
if *val != "" {
51+
attrs[name] = *val
52+
}
53+
}
54+
55+
// For generic type, collect remaining positional args as key=value
56+
if typeName == "generic" {
57+
for _, arg := range fs.Args() {
58+
k, v, ok := strings.Cut(arg, "=")
59+
if !ok {
60+
log.Fatalf("Invalid attribute (expected key=value): %s", arg)
61+
}
62+
if k == "xdg:schema" {
63+
log.Fatal("Cannot override xdg:schema via positional args")
64+
}
65+
attrs[k] = v
66+
}
67+
}
68+
69+
secret, err := lookupSecret(attrs)
70+
if err != nil {
71+
log.Fatal(err)
72+
}
73+
74+
os.Stdout.Write(secret)
75+
}
76+
77+
func lookupSecret(attrs map[string]string) ([]byte, error) {
78+
conn, err := dbus.SessionBus()
79+
if err != nil {
80+
return nil, fmt.Errorf("connecting to session bus: %w", err)
81+
}
82+
defer conn.Close()
83+
84+
svc := conn.Object(dbustypes.ServiceName, dbustypes.ServicePath)
85+
86+
// Open session
87+
var sessionOutput dbus.Variant
88+
var sessionPath dbus.ObjectPath
89+
err = svc.Call(
90+
dbustypes.SecretServiceInterface+".OpenSession",
91+
0,
92+
dbustypes.AlgorithmPlain,
93+
dbus.MakeVariant(""),
94+
).Store(&sessionOutput, &sessionPath)
95+
if err != nil {
96+
return nil, fmt.Errorf("opening session: %w", err)
97+
}
98+
defer conn.Object(dbustypes.ServiceName, sessionPath).Call(
99+
dbustypes.SessionInterface+".Close", 0,
100+
)
101+
102+
// SearchItems returns (unlocked, locked)
103+
var unlocked, locked []dbus.ObjectPath
104+
err = svc.Call(
105+
dbustypes.SecretServiceInterface+".SearchItems",
106+
0,
107+
attrs,
108+
).Store(&unlocked, &locked)
109+
if err != nil {
110+
return nil, fmt.Errorf("searching items: %w", err)
111+
}
112+
113+
if len(locked) > 0 {
114+
fmt.Fprintf(os.Stderr, "Warning: %d locked item(s) skipped\n", len(locked))
115+
}
116+
117+
if len(unlocked) == 0 {
118+
return nil, fmt.Errorf("no matching secrets found")
119+
}
120+
121+
// GetSecrets for the first unlocked item
122+
var secrets map[dbus.ObjectPath]dbustypes.Secret
123+
err = svc.Call(
124+
dbustypes.SecretServiceInterface+".GetSecrets",
125+
0,
126+
unlocked[:1],
127+
sessionPath,
128+
).Store(&secrets)
129+
if err != nil {
130+
return nil, fmt.Errorf("getting secret: %w", err)
131+
}
132+
133+
for _, s := range secrets {
134+
return s.Value, nil
135+
}
136+
137+
return nil, fmt.Errorf("no matching secrets found")
138+
}
139+
140+
func printGetUsage() {
141+
fmt.Fprint(os.Stderr, `Usage: gopass-secret get <type> [flags...] [key=value...]
142+
143+
Searches for a secret by type and optional attributes, prints its value to stdout.
144+
145+
Secret types:
146+
`)
147+
for _, name := range schema.TypeNames() {
148+
st := schema.Types[name]
149+
fmt.Fprintf(os.Stderr, " %-12s %s\n", name, st.Help)
150+
for _, attr := range st.Attributes {
151+
fmt.Fprintf(os.Stderr, " --%-10s %s\n", attr.Name, attr.Help)
152+
}
153+
}
154+
fmt.Fprint(os.Stderr, `
155+
The generic type accepts arbitrary key=value positional arguments.
156+
`)
157+
}

cmd/gopass-secret/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ func main() {
7171
runConfig(os.Args[2:])
7272
case "add":
7373
runAdd(os.Args[2:])
74+
case "get":
75+
runGet(os.Args[2:])
7476
case "list", "ls":
7577
runList(os.Args[2:])
7678
case "version", "--version":
@@ -94,6 +96,7 @@ Commands:
9496
service Run the D-Bus Secret Service daemon
9597
config Show configuration
9698
add Add a secret to the store
99+
get Look up a secret by type and attributes
97100
list, ls List secrets with attributes
98101
version Print version
99102
help Show this help

0 commit comments

Comments
 (0)