mirror of
https://github.com/getsops/sops.git
synced 2026-02-05 12:45:21 +01:00
feat: add ssh support for age
Signed-off-by: Marvin Strangfeld <marvin@strangfeld.io>
This commit is contained in:
committed by
haoqixu
parent
d265c0e57c
commit
b888daa244
142
age/keysource.go
142
age/keysource.go
@@ -11,10 +11,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/getsops/sops/v3/logging"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -24,6 +26,9 @@ const (
|
||||
// SopsAgeKeyFileEnv can be set as an environment variable pointing to an
|
||||
// age keys file.
|
||||
SopsAgeKeyFileEnv = "SOPS_AGE_KEY_FILE"
|
||||
// SopsAgeSshPrivateKeyEnv can be set as an environment variable pointing to
|
||||
// a private SSH key file.
|
||||
SopsAgeSshPrivateKeyEnv = "SOPS_AGE_SSH_PRIVATE_KEY"
|
||||
// SopsAgeKeyUserConfigPath is the default age keys file path in
|
||||
// getUserConfigDir().
|
||||
SopsAgeKeyUserConfigPath = "sops/age/keys.txt"
|
||||
@@ -60,7 +65,7 @@ type MasterKey struct {
|
||||
parsedIdentities []age.Identity
|
||||
// parsedRecipient contains a parsed age public key.
|
||||
// It is used to lazy-load the Recipient at-most once.
|
||||
parsedRecipient *age.X25519Recipient
|
||||
parsedRecipient age.Recipient
|
||||
}
|
||||
|
||||
// MasterKeysFromRecipients takes a comma-separated list of Bech32-encoded
|
||||
@@ -233,6 +238,98 @@ func (key *MasterKey) TypeToIdentifier() string {
|
||||
return KeyTypeIdentifier
|
||||
}
|
||||
|
||||
// readPublicKeyFile attempts to read a public key based on the given private
|
||||
// key path. It assumes the public key is in the same directory, with the same
|
||||
// name, but with a ".pub" extension. If the public key cannot be read, an
|
||||
// error is returned.
|
||||
func readPublicKeyFile(privateKeyPath string) (ssh.PublicKey, error) {
|
||||
publicKeyPath := privateKeyPath + ".pub"
|
||||
f, err := os.Open(publicKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain public %q key for %q SSH key: %w", publicKeyPath, privateKeyPath, err)
|
||||
}
|
||||
defer f.Close()
|
||||
contents, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %w", publicKeyPath, err)
|
||||
}
|
||||
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse %q: %w", publicKeyPath, err)
|
||||
}
|
||||
return pubKey, nil
|
||||
}
|
||||
|
||||
// parseSSHIdentityFromPrivateKeyFile returns an age.Identity from the given
|
||||
// private key file. If the private key file is encrypted, it will configure
|
||||
// the identity to prompt for a passphrase.
|
||||
func parseSSHIdentityFromPrivateKeyFile(keyPath string) (age.Identity, error) {
|
||||
keyFile, err := os.Open(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer keyFile.Close()
|
||||
contents, err := io.ReadAll(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
id, err := agessh.ParseIdentity(contents)
|
||||
if sshErr, ok := err.(*ssh.PassphraseMissingError); ok {
|
||||
pubKey := sshErr.PublicKey
|
||||
if pubKey == nil {
|
||||
pubKey, err = readPublicKeyFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
passphrasePrompt := func() ([]byte, error) {
|
||||
pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for %q:", keyPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not read passphrase for %q: %v", keyPath, err)
|
||||
}
|
||||
return pass, nil
|
||||
}
|
||||
i, err := agessh.NewEncryptedSSHIdentity(pubKey, contents, passphrasePrompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create encrypted SSH identity: %w", err)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed SSH identity in %q: %w", keyPath, err)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// loadAgeSSHIdentity attempts to load the age SSH identity based on an SSH
|
||||
// private key from the SopsAgeSshPrivateKeyEnv environment variable. If the
|
||||
// environment variable is not present, it will fall back to `~/.ssh/id_ed25519`
|
||||
// or `~/.ssh/id_rsa`. If no age SSH identity is found, it will return nil.
|
||||
func loadAgeSSHIdentity() (age.Identity, error) {
|
||||
sshKeyFilePath, ok := os.LookupEnv(SopsAgeSshPrivateKeyEnv)
|
||||
if ok {
|
||||
return parseSSHIdentityFromPrivateKeyFile(sshKeyFilePath)
|
||||
}
|
||||
|
||||
userHomeDir, err := os.UserHomeDir()
|
||||
if err != nil || userHomeDir == "" {
|
||||
log.Warnf("could not determine the user home directory: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sshEd25519PrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_ed25519")
|
||||
if _, err := os.Stat(sshEd25519PrivateKeyPath); err == nil {
|
||||
return parseSSHIdentityFromPrivateKeyFile(sshEd25519PrivateKeyPath)
|
||||
}
|
||||
|
||||
sshRsaPrivateKeyPath := filepath.Join(userHomeDir, ".ssh", "id_rsa")
|
||||
if _, err := os.Stat(sshRsaPrivateKeyPath); err == nil {
|
||||
return parseSSHIdentityFromPrivateKeyFile(sshRsaPrivateKeyPath)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func getUserConfigDir() (string, error) {
|
||||
if runtime.GOOS == "darwin" {
|
||||
if userConfigDir, ok := os.LookupEnv(xdgConfigHome); ok && userConfigDir != "" {
|
||||
@@ -244,9 +341,19 @@ func getUserConfigDir() (string, error) {
|
||||
|
||||
// loadIdentities attempts to load the age identities based on runtime
|
||||
// environment configurations (e.g. SopsAgeKeyEnv, SopsAgeKeyFileEnv,
|
||||
// SopsAgeKeyUserConfigPath). It will load all found references, and expects
|
||||
// at least one configuration to be present.
|
||||
// SopsAgeSshPrivateKeyEnv, SopsAgeKeyUserConfigPath). It will load all
|
||||
// found references, and expects at least one configuration to be present.
|
||||
func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
|
||||
var identities ParsedIdentities
|
||||
|
||||
sshIdentity, err := loadAgeSSHIdentity()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get SSH identity: %w", err)
|
||||
}
|
||||
if sshIdentity != nil {
|
||||
identities = append(identities, sshIdentity)
|
||||
}
|
||||
|
||||
var readers = make(map[string]io.Reader, 0)
|
||||
|
||||
if ageKey, ok := os.LookupEnv(SopsAgeKeyEnv); ok {
|
||||
@@ -263,7 +370,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
|
||||
}
|
||||
|
||||
userConfigDir, err := getUserConfigDir()
|
||||
if err != nil && len(readers) == 0 {
|
||||
if err != nil && len(readers) == 0 && len(identities) == 0 {
|
||||
return nil, fmt.Errorf("user config directory could not be determined: %w", err)
|
||||
}
|
||||
if userConfigDir != "" {
|
||||
@@ -272,7 +379,7 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) && len(readers) == 0 {
|
||||
if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
|
||||
// If we have no other readers, presence of the file is required.
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
@@ -282,7 +389,6 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var identities ParsedIdentities
|
||||
for n, r := range readers {
|
||||
ids, err := age.ParseIdentities(r)
|
||||
if err != nil {
|
||||
@@ -294,13 +400,25 @@ func (key *MasterKey) loadIdentities() (ParsedIdentities, error) {
|
||||
}
|
||||
|
||||
// parseRecipient attempts to parse a string containing an encoded age public
|
||||
// key.
|
||||
func parseRecipient(recipient string) (*age.X25519Recipient, error) {
|
||||
parsedRecipient, err := age.ParseX25519Recipient(recipient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err)
|
||||
// key or a public ssh key.
|
||||
func parseRecipient(recipient string) (age.Recipient, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(recipient, "age1"):
|
||||
parsedRecipient, err := age.ParseX25519Recipient(recipient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse input as Bech32-encoded age public key: %w", err)
|
||||
}
|
||||
|
||||
return parsedRecipient, nil
|
||||
case strings.HasPrefix(recipient, "ssh-"):
|
||||
parsedRecipient, err := agessh.ParseRecipient(recipient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse input as age-ssh public key: %w", err)
|
||||
}
|
||||
return parsedRecipient, nil
|
||||
}
|
||||
return parsedRecipient, nil
|
||||
|
||||
return nil, fmt.Errorf("failed to parse input, unknown recipient type: %q", recipient)
|
||||
}
|
||||
|
||||
// parseIdentities attempts to parse the string set of encoded age identities.
|
||||
|
||||
@@ -28,6 +28,23 @@ EylloI7MNGbadPGb
|
||||
-----END AGE ENCRYPTED FILE-----`
|
||||
// mockEncryptedKeyPlain is the plain value of mockEncryptedKey.
|
||||
mockEncryptedKeyPlain string = "data"
|
||||
// mockSshRecipient is a mock age ssh recipient, it matches mockSshIdentity
|
||||
mockSshRecipient string = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID+Wi8WZw2bXfBpcs/WECttCzP39OkenS6pHWHWGFJvN Test"
|
||||
// mockSshIdentity is a mock age identity based on an OpenSSH private key (ed25519)
|
||||
mockSshIdentity string = `-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACA/lovFmcNm13waXLP1hArbQsz9/TpHp0uqR1h1hhSbzQAAAIgCXDMIAlwz
|
||||
CAAAAAtzc2gtZWQyNTUxOQAAACA/lovFmcNm13waXLP1hArbQsz9/TpHp0uqR1h1hhSbzQ
|
||||
AAAEBJdWTJ8dC0OnMcwy4gQ96sp6KG8GE9EiyhFGhKldKiST+Wi8WZw2bXfBpcs/WECttC
|
||||
zP39OkenS6pHWHWGFJvNAAAABFRlc3QB
|
||||
-----END OPENSSH PRIVATE KEY-----`
|
||||
mockEncryptedSshKey string = `-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDJjd0R4dyB2R3Ns
|
||||
VUNHaXBiTEJaNU5BMFFQZUpCYWJqODFyTTZ4WWZoRVpUd2M2aTBFCkduUFJHb1U2
|
||||
K3RqWVQrLzE4anZKZ3h2T3c2MFpZTHlGaHprcElXenByWTAKLS0tIG56MHFSZERl
|
||||
em9PWmRMMTY4aytYTnVZN04yeER5Z2E3TWxWT3JTZWR2ekUKp/HZLy4MzQqoszGk
|
||||
+P0hSPPNhOhvFwv4AqCw1+A+WyeHGQPq
|
||||
-----END AGE ENCRYPTED FILE-----`
|
||||
)
|
||||
|
||||
func TestMasterKeysFromRecipients(t *testing.T) {
|
||||
@@ -41,22 +58,32 @@ func TestMasterKeysFromRecipients(t *testing.T) {
|
||||
assert.Equal(t, got[0].Recipient, mockRecipient)
|
||||
})
|
||||
|
||||
t.Run("recipients", func(t *testing.T) {
|
||||
got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient)
|
||||
t.Run("recipient-ssh", func(t *testing.T) {
|
||||
got, err := MasterKeysFromRecipients(mockSshRecipient)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, got, 2)
|
||||
assert.Len(t, got, 1)
|
||||
assert.Equal(t, got[0].Recipient, mockSshRecipient)
|
||||
})
|
||||
|
||||
t.Run("recipients", func(t *testing.T) {
|
||||
got, err := MasterKeysFromRecipients(mockRecipient + "," + otherRecipient + "," + mockSshRecipient)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, got, 3)
|
||||
assert.Equal(t, got[0].Recipient, mockRecipient)
|
||||
assert.Equal(t, got[1].Recipient, otherRecipient)
|
||||
assert.Equal(t, got[2].Recipient, mockSshRecipient)
|
||||
})
|
||||
|
||||
t.Run("leading and trailing spaces", func(t *testing.T) {
|
||||
got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " ")
|
||||
got, err := MasterKeysFromRecipients(" " + mockRecipient + " , " + otherRecipient + " , " + mockSshRecipient + " ")
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, got, 2)
|
||||
assert.Len(t, got, 3)
|
||||
assert.Equal(t, got[0].Recipient, mockRecipient)
|
||||
assert.Equal(t, got[1].Recipient, otherRecipient)
|
||||
assert.Equal(t, got[2].Recipient, mockSshRecipient)
|
||||
})
|
||||
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
@@ -75,6 +102,14 @@ func TestMasterKeyFromRecipient(t *testing.T) {
|
||||
assert.Nil(t, got.parsedIdentities)
|
||||
})
|
||||
|
||||
t.Run("recipient-ssh", func(t *testing.T) {
|
||||
got, err := MasterKeyFromRecipient(mockSshRecipient)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, mockSshRecipient, got.Recipient)
|
||||
assert.NotNil(t, got.parsedRecipient)
|
||||
assert.Nil(t, got.parsedIdentities)
|
||||
})
|
||||
|
||||
t.Run("leading and trailing spaces", func(t *testing.T) {
|
||||
got, err := MasterKeyFromRecipient(" " + mockRecipient + " ")
|
||||
assert.NoError(t, err)
|
||||
@@ -83,6 +118,14 @@ func TestMasterKeyFromRecipient(t *testing.T) {
|
||||
assert.Nil(t, got.parsedIdentities)
|
||||
})
|
||||
|
||||
t.Run("leading and trailing spaces - ssh", func(t *testing.T) {
|
||||
got, err := MasterKeyFromRecipient(" " + mockSshRecipient + " ")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, mockSshRecipient, got.Recipient)
|
||||
assert.NotNil(t, got.parsedRecipient)
|
||||
assert.Nil(t, got.parsedIdentities)
|
||||
})
|
||||
|
||||
t.Run("invalid recipient", func(t *testing.T) {
|
||||
got, err := MasterKeyFromRecipient("invalid")
|
||||
assert.Error(t, err)
|
||||
@@ -111,6 +154,8 @@ func TestParsedIdentities_ApplyToMasterKey(t *testing.T) {
|
||||
func TestMasterKey_Encrypt(t *testing.T) {
|
||||
mockParsedRecipient, err := parseRecipient(mockRecipient)
|
||||
assert.NoError(t, err)
|
||||
mockSshParsedRecipient, err := parseRecipient(mockSshRecipient)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("recipient", func(t *testing.T) {
|
||||
key := &MasterKey{
|
||||
@@ -120,6 +165,14 @@ func TestMasterKey_Encrypt(t *testing.T) {
|
||||
assert.NotEmpty(t, key.EncryptedKey)
|
||||
})
|
||||
|
||||
t.Run("recipient ssh", func(t *testing.T) {
|
||||
key := &MasterKey{
|
||||
Recipient: mockSshRecipient,
|
||||
}
|
||||
assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain)))
|
||||
assert.NotEmpty(t, key.EncryptedKey)
|
||||
})
|
||||
|
||||
t.Run("parsed recipient", func(t *testing.T) {
|
||||
key := &MasterKey{
|
||||
parsedRecipient: mockParsedRecipient,
|
||||
@@ -128,13 +181,21 @@ func TestMasterKey_Encrypt(t *testing.T) {
|
||||
assert.NotEmpty(t, key.EncryptedKey)
|
||||
})
|
||||
|
||||
t.Run("parsed recipient ssh", func(t *testing.T) {
|
||||
key := &MasterKey{
|
||||
parsedRecipient: mockSshParsedRecipient,
|
||||
}
|
||||
assert.NoError(t, key.Encrypt([]byte(mockEncryptedKeyPlain)))
|
||||
assert.NotEmpty(t, key.EncryptedKey)
|
||||
})
|
||||
|
||||
t.Run("invalid recipient", func(t *testing.T) {
|
||||
key := &MasterKey{
|
||||
Recipient: "invalid",
|
||||
}
|
||||
err := key.Encrypt([]byte(mockEncryptedKeyPlain))
|
||||
assert.Error(t, err)
|
||||
assert.ErrorContains(t, err, "failed to parse input as Bech32-encoded age public key")
|
||||
assert.ErrorContains(t, err, "failed to parse input, unknown recipient type:")
|
||||
assert.Empty(t, key.EncryptedKey)
|
||||
})
|
||||
|
||||
@@ -188,6 +249,25 @@ func TestMasterKey_Decrypt(t *testing.T) {
|
||||
assert.EqualValues(t, mockEncryptedKeyPlain, got)
|
||||
})
|
||||
|
||||
t.Run("loaded identities ssh", func(t *testing.T) {
|
||||
key := &MasterKey{EncryptedKey: mockEncryptedSshKey}
|
||||
tmp := t.TempDir()
|
||||
overwriteUserConfigDir(t, tmp)
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
assert.NoError(t, err)
|
||||
keyPath := filepath.Join(homeDir, ".ssh/id_25519")
|
||||
assert.True(t, strings.HasPrefix(keyPath, homeDir))
|
||||
|
||||
assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700))
|
||||
assert.NoError(t, os.WriteFile(keyPath, []byte(mockSshIdentity), 0o644))
|
||||
t.Setenv(SopsAgeSshPrivateKeyEnv, keyPath)
|
||||
|
||||
got, err := key.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, mockEncryptedKeyPlain, got)
|
||||
})
|
||||
|
||||
t.Run("no identities", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
overwriteUserConfigDir(t, tmpDir)
|
||||
@@ -327,6 +407,25 @@ func TestMasterKey_loadIdentities(t *testing.T) {
|
||||
assert.Len(t, got, 1)
|
||||
})
|
||||
|
||||
t.Run(SopsAgeSshPrivateKeyEnv, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
overwriteUserConfigDir(t, tmpDir)
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
assert.NoError(t, err)
|
||||
keyPath := filepath.Join(homeDir, ".ssh/id_25519")
|
||||
assert.True(t, strings.HasPrefix(keyPath, homeDir))
|
||||
|
||||
assert.NoError(t, os.MkdirAll(filepath.Dir(keyPath), 0o700))
|
||||
assert.NoError(t, os.WriteFile(keyPath, []byte(mockSshIdentity), 0o644))
|
||||
t.Setenv(SopsAgeSshPrivateKeyEnv, keyPath)
|
||||
|
||||
key := &MasterKey{}
|
||||
got, err := key.loadIdentities()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, got, 1)
|
||||
})
|
||||
|
||||
t.Run("no identity", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
overwriteUserConfigDir(t, tmpDir)
|
||||
@@ -374,8 +473,8 @@ func TestMasterKey_loadIdentities(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// overwriteUserConfigDir sets the user config directory based on the
|
||||
// os.UserConfigDir logic.
|
||||
// overwriteUserConfigDir sets the user config directory and the user home directory
|
||||
// based on the os.UserConfigDir logic.
|
||||
func overwriteUserConfigDir(t *testing.T, path string) {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
@@ -384,6 +483,7 @@ func overwriteUserConfigDir(t *testing.T, path string) {
|
||||
t.Setenv("home", path)
|
||||
default: // Unix
|
||||
t.Setenv("XDG_CONFIG_HOME", path)
|
||||
t.Setenv("HOME", path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
53
age/tui.go
Normal file
53
age/tui.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// These functions have been copied from the age project
|
||||
// https://github.com/FiloSottile/age/blob/v1.0.0/cmd/age/encrypted_keys.go
|
||||
// Copyright 2021 The age Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package age
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
// readPassphrase reads a passphrase from the terminal. It does not read from a
|
||||
// non-terminal stdin, so it does not check stdinInUse.
|
||||
func readPassphrase(prompt string) ([]byte, error) {
|
||||
var in, out *os.File
|
||||
if runtime.GOOS == "windows" {
|
||||
var err error
|
||||
in, err = os.OpenFile("CONIN$", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer in.Close()
|
||||
out, err = os.OpenFile("CONOUT$", os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer out.Close()
|
||||
} else if _, err := os.Stat("/dev/tty"); err == nil {
|
||||
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tty.Close()
|
||||
in, out = tty, tty
|
||||
} else {
|
||||
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||
return nil, fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
|
||||
}
|
||||
in, out = os.Stdin, os.Stderr
|
||||
}
|
||||
fmt.Fprintf(out, "%s ", prompt)
|
||||
// Use CRLF to work around an apparent bug in WSL2's handling of CONOUT$.
|
||||
// Only when running a Windows binary from WSL2, the cursor would not go
|
||||
// back to the start of the line with a simple LF. Honestly, it's impressive
|
||||
// CONIN$ and CONOUT$ even work at all inside WSL2.
|
||||
defer fmt.Fprintf(out, "\r\n")
|
||||
return term.ReadPassword(int(in.Fd()))
|
||||
}
|
||||
3
go.mod
3
go.mod
@@ -34,6 +34,7 @@ require (
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/urfave/cli v1.22.16
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/term v0.29.0
|
||||
@@ -55,6 +56,7 @@ require (
|
||||
cloud.google.com/go/longrunning v0.6.3 // indirect
|
||||
cloud.google.com/go/monitoring v1.22.0 // indirect
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||
@@ -138,7 +140,6 @@ require (
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.33.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.34.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/oauth2 v0.26.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
|
||||
Reference in New Issue
Block a user