1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
|
// Package clipboard pipes a password field to the OS clipboard command.
// On macOS (UNAME=Darwin) it uses pbcopy; on Linux it uses gpaste-client.
package clipboard
import (
"context"
"fmt"
"os"
"os/exec"
"regexp"
)
// matchRe captures the first non-whitespace:non-whitespace pair in a string,
// identifying the user and password fields. Compiled once at startup.
var matchRe = regexp.MustCompile(`(\S+):(\S+)`)
// censorRe replaces every non-whitespace:non-whitespace token with word:CENSORED
// so the password is redacted while the user and context are preserved.
var censorRe = regexp.MustCompile(`(\S+):\S+`)
// Clipboard holds the OS-specific clipboard command to spawn.
// The command receives the password via stdin.
type Clipboard struct {
cmd string
}
// New returns a Clipboard configured for the current platform.
// If UNAME == "Darwin" the macosCmd is used; otherwise gnomeCmd is used.
// This mirrors the Ruby: ENV['UNAME'] == 'Darwin' ? Config.macos_clipboard_cmd : Config.gnome_clipboard_cmd
func New(gnomeCmd, macosCmd string) *Clipboard {
cmd := gnomeCmd
if os.Getenv("UNAME") == "Darwin" {
cmd = macosCmd
}
return &Clipboard{cmd: cmd}
}
// Paste extracts the password from data, pipes it to the clipboard command,
// and prints the censored form of data to stdout so the operator can see
// contextual information without the secret being visible.
//
// The clipboard command is started and immediately detached (the wait is done
// in a goroutine), matching the Ruby `Process.detach` behaviour so the caller
// is not blocked waiting for the clipboard daemon to exit.
func (c *Clipboard) Paste(ctx context.Context, data string) error {
user, password, censored, err := extract(data)
if err != nil {
return err
}
if c.cmd == "" {
return fmt.Errorf("can't paste to clipboard")
}
// Spawn the clipboard command; the password is written to its stdin.
clipCmd := exec.CommandContext(ctx, c.cmd) //nolint:gosec // cmd is caller-supplied config
stdin, err := clipCmd.StdinPipe()
if err != nil {
return fmt.Errorf("opening stdin pipe for clipboard command: %w", err)
}
if err := clipCmd.Start(); err != nil {
return fmt.Errorf("starting clipboard command %q: %w", c.cmd, err)
}
// Write only the password — never the full data — to the clipboard.
if _, err := fmt.Fprint(stdin, password); err != nil {
return fmt.Errorf("writing password to clipboard command stdin: %w", err)
}
stdin.Close()
// Detach: reap the child in the background so the parent is not blocked.
// This matches the Ruby `Process.detach(pid)` call.
go func() { _ = clipCmd.Wait() }()
// Print the censored representation so the operator sees context.
fmt.Println(censored)
fmt.Printf("> Pasted password for user '%s' to the clipboard\n", user)
return nil
}
// extract parses data for the first "user:password" token and returns:
// - user – everything before the colon in the first match
// - password – everything after the colon in the first match
// - censored – data with every "word:secret" token replaced by "word:CENSORED"
//
// Regex mirrors the Ruby: /(\S+):(\S+)/ for matching and substitution.
// Both sides of the match are greedy (\S+), so for multi-colon tokens like
// "user:pass:extra" the split occurs at the last colon, yielding
// user="user:pass" and password="extra" — identical to Ruby's behaviour.
func extract(data string) (user, password, censored string, err error) {
parts := matchRe.FindStringSubmatch(data)
if parts == nil {
return "", "", "", fmt.Errorf("no user:password pattern found in data")
}
user = parts[1]
password = parts[2]
// Replace all occurrences with "$1:CENSORED", preserving the word before the colon.
censored = censorRe.ReplaceAllString(data, "${1}:CENSORED")
return user, password, censored, nil
}
|