diff --git a/README.md b/README.md index 0b7ef092..fa26e931 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Options: -o, --output OUTPUT Write the result to the file at path OUTPUT. -a, --armor Encrypt to a PEM encoded format. -p, --passphrase Encrypt with a passphrase. + --pinentry Use pinentry to read the passphrase. -r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated. -R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated. -i, --identity PATH Use the identity file at PATH. Can be repeated. diff --git a/cmd/age/age.go b/cmd/age/age.go index 9c1707d9..7591572b 100644 --- a/cmd/age/age.go +++ b/cmd/age/age.go @@ -41,6 +41,7 @@ Options: -o, --output OUTPUT Write the result to the file at path OUTPUT. -a, --armor Encrypt to a PEM encoded format. -p, --passphrase Encrypt with a passphrase. + --pinentry Use pinentry to read the passphrase. -r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated. -R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated. -i, --identity PATH Use the identity file at PATH. Can be repeated. @@ -90,6 +91,7 @@ func main() { passFlag, versionFlag, armorFlag bool recipientFlags, identityFlags multiFlag recipientsFileFlags multiFlag + pinentryFlag bool ) flag.BoolVar(&versionFlag, "version", false, "print the version") @@ -109,6 +111,7 @@ func main() { flag.Var(&recipientsFileFlags, "recipients-file", "recipients file (can be repeated)") flag.Var(&identityFlags, "i", "identity (can be repeated)") flag.Var(&identityFlags, "identity", "identity (can be repeated)") + flag.BoolVar(&pinentryFlag, "pinentry", false, "use pinentry to read passphrase") flag.Parse() if versionFlag { @@ -212,20 +215,20 @@ func main() { switch { case decryptFlag: - decrypt(identityFlags, in, out) + decrypt(identityFlags, in, out, pinentryFlag) case passFlag: - pass, err := passphrasePromptForEncryption() + pass, err := passphrasePromptForEncryption(pinentryFlag) if err != nil { errorf("%v", err) } encryptPass(pass, in, out, armorFlag) default: - encryptKeys(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag) + encryptKeys(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag, pinentryFlag) } } -func passphrasePromptForEncryption() (string, error) { - pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):") +func passphrasePromptForEncryption(usePINEntry bool) (string, error) { + pass, err := readPassphrase("Enter passphrase (leave empty to autogenerate a secure one):", usePINEntry) if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } @@ -239,7 +242,7 @@ func passphrasePromptForEncryption() (string, error) { // TODO: consider printing this to the terminal, instead of stderr. fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p) } else { - confirm, err := readPassphrase("Confirm passphrase:") + confirm, err := readPassphrase("Confirm passphrase:", usePINEntry) if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } @@ -250,7 +253,7 @@ func passphrasePromptForEncryption() (string, error) { return p, nil } -func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) { +func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, armor, usePINEntry bool) { var recipients []age.Recipient for _, arg := range keys { r, err := parseRecipient(arg) @@ -272,7 +275,7 @@ func encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, recipients = append(recipients, recs...) } for _, name := range identities { - ids, err := parseIdentitiesFile(name) + ids, err := parseIdentitiesFile(name, usePINEntry) if err != nil { errorf("reading %q: %v", name, err) } @@ -315,15 +318,19 @@ func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor } } -func decrypt(keys []string, in io.Reader, out io.Writer) { +func decrypt(keys []string, in io.Reader, out io.Writer, usePINEntry bool) { identities := []age.Identity{ // If there is an scrypt recipient (it will have to be the only one and) // this identity will be invoked. - &LazyScryptIdentity{passphrasePrompt}, + &LazyScryptIdentity{ + Passphrase: func() (string, error) { + return passphrasePrompt(usePINEntry) + }, + }, } for _, name := range keys { - ids, err := parseIdentitiesFile(name) + ids, err := parseIdentitiesFile(name, usePINEntry) if err != nil { errorf("reading %q: %v", name, err) } @@ -346,8 +353,8 @@ func decrypt(keys []string, in io.Reader, out io.Writer) { } } -func passphrasePrompt() (string, error) { - pass, err := readPassphrase("Enter passphrase:") +func passphrasePrompt(usePINEntry bool) (string, error) { + pass, err := readPassphrase("Enter passphrase:", usePINEntry) if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } diff --git a/cmd/age/age_test.go b/cmd/age/age_test.go index e0aa32b1..9b8332d6 100644 --- a/cmd/age/age_test.go +++ b/cmd/age/age_test.go @@ -29,7 +29,7 @@ func TestVectors(t *testing.T) { } defaultIDs = append(defaultIDs, i) - ids, err := parseIdentitiesFile("testdata/default_key.txt") + ids, err := parseIdentitiesFile("testdata/default_key.txt", false) if err != nil { t.Fatal(err) } @@ -44,7 +44,7 @@ func TestVectors(t *testing.T) { expectNoMatch := strings.HasPrefix(name, "nomatch_") t.Run(name, func(t *testing.T) { identities := defaultIDs - ids, err := parseIdentitiesFile("testdata/" + name + "_key.txt") + ids, err := parseIdentitiesFile("testdata/"+name+"_key.txt", false) if err == nil { identities = ids } diff --git a/cmd/age/encrypted_keys.go b/cmd/age/encrypted_keys.go index ae1e27eb..9eefd4a2 100644 --- a/cmd/age/encrypted_keys.go +++ b/cmd/age/encrypted_keys.go @@ -10,8 +10,10 @@ import ( "fmt" "os" "runtime" + "strings" "filippo.io/age" + "github.com/twpayne/go-pinentry" "golang.org/x/term" ) @@ -105,7 +107,22 @@ func (i *EncryptedIdentity) decrypt() error { // 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) { +func readPassphrase(prompt string, usePINEntry bool) ([]byte, error) { + if usePINEntry { + client, err := pinentry.NewClient( + pinentry.WithBinaryNameFromGnuPGAgentConf(), + pinentry.WithDesc(strings.TrimSuffix(prompt, ":")+"."), + pinentry.WithGPGTTY(), + pinentry.WithPrompt("Passphrase:"), + pinentry.WithTitle("age"), + ) + if err != nil { + return nil, err + } + pin, _, err := client.GetPIN() + return []byte(pin), err + } + var in, out *os.File if runtime.GOOS == "windows" { var err error diff --git a/cmd/age/parse.go b/cmd/age/parse.go index 93a913ca..652e62c7 100644 --- a/cmd/age/parse.go +++ b/cmd/age/parse.go @@ -123,7 +123,7 @@ func sshKeyType(s string) (string, bool) { // parseIdentitiesFile parses a file that contains age or SSH keys. It returns // one or more of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, // *agessh.EncryptedSSHIdentity, or *EncryptedIdentity. -func parseIdentitiesFile(name string) ([]age.Identity, error) { +func parseIdentitiesFile(name string, usePINEntry bool) ([]age.Identity, error) { var f *os.File if name == "-" { if stdinInUse { @@ -162,7 +162,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) { return []age.Identity{&EncryptedIdentity{ Contents: contents, Passphrase: func() (string, error) { - pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for identity file %q:", name)) + pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for identity file %q:", name), usePINEntry) if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } @@ -183,7 +183,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) { if len(contents) == privateKeySizeLimit { return nil, fmt.Errorf("failed to read %q: file too long", name) } - return parseSSHIdentity(name, contents) + return parseSSHIdentity(name, contents, usePINEntry) // An unencrypted age identity file. default: @@ -195,7 +195,7 @@ func parseIdentitiesFile(name string) ([]age.Identity, error) { } } -func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) { +func parseSSHIdentity(name string, pemBytes []byte, usePINEntry bool) ([]age.Identity, error) { id, err := agessh.ParseIdentity(pemBytes) if sshErr, ok := err.(*ssh.PassphraseMissingError); ok { pubKey := sshErr.PublicKey @@ -206,7 +206,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) { } } passphrasePrompt := func() ([]byte, error) { - pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for %q:", name)) + pass, err := readPassphrase(fmt.Sprintf("Enter passphrase for %q:", name), usePINEntry) if err != nil { return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err) } diff --git a/go.mod b/go.mod index 2858c166..485cb431 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,10 @@ require ( golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b ) -require golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect +require ( + github.com/rs/zerolog v1.26.0 // indirect + github.com/twpayne/go-pinentry v0.0.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.7.0 // indirect + golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect +) diff --git a/go.sum b/go.sum index b26cf571..f3be63d5 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,55 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= +github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/twpayne/go-pinentry v0.0.2 h1:xncgnq3VGWgOq5gMx1SmK++7PrfvkvqZZY00SijBASs= +github.com/twpayne/go-pinentry v0.0.2/go.mod h1:OUbsOnVXqvfSr8PZzFkSNJdBTJOPepfM0NSlDmR5paY= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg= golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=