Skip to content

Commit 739560f

Browse files
Remove debian package dependencies (#1506)
* Use secret service on Linux, file backend on WSL with deterministic password On non-WSL Linux, prefer the Secret Service backend (gnome-keyring/KWallet) with the file backend as a fallback. On WSL, use only the file backend with a deterministic password derived from the machine ID via HMAC-SHA256, so the user is never prompted for a password in a headless environment. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Committed-By-Agent: claude * Remove debian package dependencies on libsecret and gnome-keyring The CLI no longer requires these system packages since WSL uses a file-backed keyring and non-WSL Linux falls back to file if the secret service is unavailable. Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Committed-By-Agent: claude --------- Co-authored-by: Claude Sonnet 4.6 <[email protected]>
1 parent 24f54b0 commit 739560f

File tree

3 files changed

+206
-15
lines changed

3 files changed

+206
-15
lines changed

.goreleaser/linux.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,6 @@ nfpms:
5757
license: Apache 2.0
5858
formats:
5959
- deb
60-
dependencies:
61-
- libsecret-1-0
62-
- gnome-keyring
63-
- libsecret-tools
64-
- dbus-x11
6560
- id: rpm
6661
package_name: stripe
6762
vendor: Stripe

pkg/config/profile.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package config
22

33
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
47
"encoding/json"
58
"errors"
69
"fmt"
@@ -11,7 +14,6 @@ import (
1114

1215
"github.com/99designs/keyring"
1316
"github.com/spf13/viper"
14-
"golang.org/x/term"
1517

1618
"github.com/stripe/stripe-cli/pkg/ansi"
1719
"github.com/stripe/stripe-cli/pkg/validators"
@@ -59,23 +61,52 @@ const (
5961
// KeyRing ...
6062
var KeyRing keyring.Keyring
6163

64+
func isWSLFromVersion(procVersion string) bool {
65+
lower := strings.ToLower(procVersion)
66+
return strings.Contains(lower, "microsoft") || strings.Contains(lower, "wsl")
67+
}
68+
69+
func isWSL() bool {
70+
data, err := os.ReadFile("/proc/version")
71+
if err != nil {
72+
return false
73+
}
74+
return isWSLFromVersion(string(data))
75+
}
76+
77+
func wslFilePasswordFromPaths(machineIDPath, bootIDPath string) (string, error) {
78+
machineID, err := os.ReadFile(machineIDPath)
79+
if err != nil {
80+
return "", fmt.Errorf("could not read %s: %w", machineIDPath, err)
81+
}
82+
bootID, err := os.ReadFile(bootIDPath)
83+
if err != nil {
84+
return "", fmt.Errorf("could not read %s: %w", bootIDPath, err)
85+
}
86+
const appKey = "stripe-cli-keyring-v1"
87+
mac := hmac.New(sha256.New, []byte(appKey))
88+
mac.Write([]byte(strings.TrimSpace(string(machineID))))
89+
mac.Write([]byte(strings.TrimSpace(string(bootID))))
90+
return hex.EncodeToString(mac.Sum(nil)), nil
91+
}
92+
93+
func wslFilePassword(_ string) (string, error) {
94+
return wslFilePasswordFromPaths("/etc/machine-id", "/proc/sys/kernel/random/boot_id")
95+
}
96+
6297
func getKeyringConfig() keyring.Config {
6398
c := keyring.Config{
6499
KeychainTrustApplication: true,
65100
ServiceName: KeyManagementService,
66101
}
67102

68103
if runtime.GOOS == "linux" {
69-
c.AllowedBackends = []keyring.BackendType{keyring.FileBackend}
70104
c.FileDir = getConfigFolder(os.Getenv("XDG_CONFIG_HOME"))
71-
c.FilePasswordFunc = func(prompt string) (string, error) {
72-
fmt.Fprintf(os.Stdout, "%s: ", prompt)
73-
b, err := term.ReadPassword(int(os.Stdin.Fd()))
74-
if err != nil {
75-
return "", err
76-
}
77-
fmt.Println()
78-
return string(b), nil
105+
c.FilePasswordFunc = wslFilePassword
106+
if isWSL() {
107+
c.AllowedBackends = []keyring.BackendType{keyring.FileBackend}
108+
} else {
109+
c.AllowedBackends = []keyring.BackendType{keyring.SecretServiceBackend, keyring.FileBackend}
79110
}
80111
}
81112

pkg/config/wsl_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package config
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"os"
8+
"path/filepath"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestIsWSLFromVersion(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
content string
18+
want bool
19+
}{
20+
{
21+
name: "microsoft keyword",
22+
content: "Linux version 5.15.0-microsoft-standard-WSL2",
23+
want: true,
24+
},
25+
{
26+
name: "Microsoft capitalised",
27+
content: "Linux version 5.15.0-Microsoft-standard",
28+
want: true,
29+
},
30+
{
31+
name: "wsl keyword",
32+
content: "Linux version 5.15.0 (wsl@build)",
33+
want: true,
34+
},
35+
{
36+
name: "WSL uppercase",
37+
content: "Linux version 5.15.0 (WSL2)",
38+
want: true,
39+
},
40+
{
41+
name: "plain linux",
42+
content: "Linux version 6.1.0-28-amd64 ([email protected])",
43+
want: false,
44+
},
45+
{
46+
name: "empty",
47+
content: "",
48+
want: false,
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
require.Equal(t, tt.want, isWSLFromVersion(tt.content))
55+
})
56+
}
57+
}
58+
59+
func TestIsWSL_UnreadableProcVersion(t *testing.T) {
60+
// isWSL() returns false when /proc/version cannot be read; we can verify
61+
// the helper directly covers that path via isWSLFromVersion with empty input.
62+
require.False(t, isWSLFromVersion(""))
63+
}
64+
65+
func wslExpectedPassword(t *testing.T, machineID, bootID string) string {
66+
t.Helper()
67+
const appKey = "stripe-cli-keyring-v1"
68+
mac := hmac.New(sha256.New, []byte(appKey))
69+
mac.Write([]byte(machineID))
70+
mac.Write([]byte(bootID))
71+
return hex.EncodeToString(mac.Sum(nil))
72+
}
73+
74+
func TestWslFilePasswordFromPaths_BothFiles(t *testing.T) {
75+
dir := t.TempDir()
76+
machineIDPath := filepath.Join(dir, "machine-id")
77+
bootIDPath := filepath.Join(dir, "boot_id")
78+
79+
require.NoError(t, os.WriteFile(machineIDPath, []byte("abc123\n"), 0600))
80+
require.NoError(t, os.WriteFile(bootIDPath, []byte("def456\n"), 0600))
81+
82+
got, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
83+
require.NoError(t, err)
84+
require.Equal(t, wslExpectedPassword(t, "abc123", "def456"), got)
85+
}
86+
87+
func TestWslFilePasswordFromPaths_MachineIDMissing(t *testing.T) {
88+
dir := t.TempDir()
89+
machineIDPath := filepath.Join(dir, "machine-id") // does not exist
90+
bootIDPath := filepath.Join(dir, "boot_id")
91+
92+
require.NoError(t, os.WriteFile(bootIDPath, []byte("def456\n"), 0600))
93+
94+
_, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
95+
require.Error(t, err)
96+
require.Contains(t, err.Error(), machineIDPath)
97+
}
98+
99+
func TestWslFilePasswordFromPaths_BootIDMissing(t *testing.T) {
100+
dir := t.TempDir()
101+
machineIDPath := filepath.Join(dir, "machine-id")
102+
bootIDPath := filepath.Join(dir, "boot_id") // does not exist
103+
104+
require.NoError(t, os.WriteFile(machineIDPath, []byte("abc123\n"), 0600))
105+
106+
_, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
107+
require.Error(t, err)
108+
require.Contains(t, err.Error(), bootIDPath)
109+
}
110+
111+
func TestWslFilePasswordFromPaths_BothMissing(t *testing.T) {
112+
dir := t.TempDir()
113+
machineIDPath := filepath.Join(dir, "machine-id")
114+
bootIDPath := filepath.Join(dir, "boot_id")
115+
116+
_, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
117+
require.Error(t, err)
118+
}
119+
120+
func TestWslFilePasswordFromPaths_Deterministic(t *testing.T) {
121+
dir := t.TempDir()
122+
machineIDPath := filepath.Join(dir, "machine-id")
123+
bootIDPath := filepath.Join(dir, "boot_id")
124+
125+
require.NoError(t, os.WriteFile(machineIDPath, []byte("stable-id\n"), 0600))
126+
require.NoError(t, os.WriteFile(bootIDPath, []byte("stable-boot\n"), 0600))
127+
128+
first, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
129+
require.NoError(t, err)
130+
second, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
131+
require.NoError(t, err)
132+
133+
require.Equal(t, first, second)
134+
}
135+
136+
func TestWslFilePasswordFromPaths_DifferentIDsDifferentPasswords(t *testing.T) {
137+
dir := t.TempDir()
138+
bootIDPath := filepath.Join(dir, "boot_id")
139+
require.NoError(t, os.WriteFile(bootIDPath, []byte("same-boot\n"), 0600))
140+
141+
pathA := filepath.Join(dir, "machine-id-a")
142+
pathB := filepath.Join(dir, "machine-id-b")
143+
require.NoError(t, os.WriteFile(pathA, []byte("id-aaa\n"), 0600))
144+
require.NoError(t, os.WriteFile(pathB, []byte("id-bbb\n"), 0600))
145+
146+
pwA, err := wslFilePasswordFromPaths(pathA, bootIDPath)
147+
require.NoError(t, err)
148+
pwB, err := wslFilePasswordFromPaths(pathB, bootIDPath)
149+
require.NoError(t, err)
150+
151+
require.NotEqual(t, pwA, pwB)
152+
}
153+
154+
func TestWslFilePasswordFromPaths_TrimsWhitespace(t *testing.T) {
155+
dir := t.TempDir()
156+
machineIDPath := filepath.Join(dir, "machine-id")
157+
bootIDPath := filepath.Join(dir, "boot_id")
158+
159+
require.NoError(t, os.WriteFile(machineIDPath, []byte(" trimmed-id \n"), 0600))
160+
require.NoError(t, os.WriteFile(bootIDPath, []byte(" trimmed-boot \n"), 0600))
161+
162+
got, err := wslFilePasswordFromPaths(machineIDPath, bootIDPath)
163+
require.NoError(t, err)
164+
require.Equal(t, wslExpectedPassword(t, "trimmed-id", "trimmed-boot"), got)
165+
}

0 commit comments

Comments
 (0)