зеркало из https://github.com/mozilla/mig.git
170 строки
4.7 KiB
Go
170 строки
4.7 KiB
Go
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
//
|
|
// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr]
|
|
|
|
package pgp /* import "github.com/mozilla/mig/pgp" */
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"golang.org/x/crypto/openpgp"
|
|
"io"
|
|
"github.com/mozilla/mig/pgp/gpgagent"
|
|
"github.com/mozilla/mig/pgp/pinentry"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
var cachedPassphrase string
|
|
|
|
// CachePassphrase() can be used to pre-cache a passphrase and avoid asking
|
|
// for it. This would normally be called from the MIG client API.
|
|
func CachePassphrase(s string) {
|
|
cachedPassphrase = s
|
|
}
|
|
|
|
// Sign signs a string with a key identified by a key fingerprint or an email address
|
|
func Sign(data, keyid string, secringFile io.Reader) (sig string, err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("pgp.Sign(): %v", e)
|
|
}
|
|
}()
|
|
keyring, err := openpgp.ReadKeyRing(secringFile)
|
|
if err != nil {
|
|
err = fmt.Errorf("Keyring access failed: '%v'", err)
|
|
panic(err)
|
|
}
|
|
// find the entity in the keyring
|
|
var signer *openpgp.Entity
|
|
found := false
|
|
for _, entity := range keyring {
|
|
if entity.PrivateKey == nil {
|
|
panic("secring contains entity without private key data")
|
|
}
|
|
fingerprint := strings.ToUpper(hex.EncodeToString(entity.PrivateKey.PublicKey.Fingerprint[:]))
|
|
if keyid == fingerprint {
|
|
signer = entity
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
err = fmt.Errorf("Signer '%s' not found", keyid)
|
|
panic(err)
|
|
}
|
|
// if private key is encrypted, attempt to decrypt it with the cached passphrase
|
|
// then try with an agent or by asking the user for a passphrase
|
|
if signer.PrivateKey.Encrypted {
|
|
err = signer.PrivateKey.Decrypt([]byte(cachedPassphrase))
|
|
if err != nil {
|
|
var pass string
|
|
// get private key passphrase
|
|
signer, pass, err = decryptEntity(signer)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
if pass != "" {
|
|
cachedPassphrase = pass
|
|
}
|
|
}
|
|
}
|
|
// calculate signature
|
|
out := bytes.NewBuffer(nil)
|
|
message := bytes.NewBufferString(data)
|
|
err = openpgp.ArmoredDetachSign(out, signer, message, nil)
|
|
if err != nil {
|
|
err = fmt.Errorf("Signature failed: '%v'", err)
|
|
panic(err)
|
|
}
|
|
|
|
// convert the writer back to string
|
|
sig, err = deArmor(out.String())
|
|
if err != nil {
|
|
err = fmt.Errorf("Error converting signature to string: '%v'", err)
|
|
panic(err)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// deArmor takes a multi line armored GPG signature, and turns it back
|
|
// into a single line signature (thank you, camlistore folks)
|
|
func deArmor(sig string) (str string, err error) {
|
|
index1 := strings.Index(sig, "\n\n")
|
|
index2 := strings.Index(sig, "\n-----")
|
|
if index1 == -1 || index2 == -1 {
|
|
err = fmt.Errorf("Failed to parse signature from gpg.")
|
|
return
|
|
}
|
|
inner := sig[index1+2 : index2]
|
|
str = strings.Replace(inner, "\n", "", -1)
|
|
return
|
|
}
|
|
|
|
// decryptEntity calls gnupg-agent and pinentry to obtain a passphrase and
|
|
// decrypt the private key of a given entity (thank you, camlistore folks)
|
|
func decryptEntity(s *openpgp.Entity) (ds *openpgp.Entity, pass string, err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("pgp.decryptEntity(): %v", e)
|
|
}
|
|
}()
|
|
ds = s
|
|
// TODO: syscall.Mlock a region and keep pass phrase in it.
|
|
pubk := &ds.PrivateKey.PublicKey
|
|
desc := fmt.Sprintf("Need to unlock GPG key %s to use it for signing.",
|
|
pubk.KeyIdShortString())
|
|
|
|
conn, err := gpgagent.NewConn()
|
|
switch err {
|
|
case gpgagent.ErrNoAgent:
|
|
fmt.Fprintf(os.Stderr, "Note: gpg-agent not found; resorting to on-demand password entry.\n")
|
|
case nil:
|
|
defer conn.Close()
|
|
req := &gpgagent.PassphraseRequest{
|
|
CacheKey: "mig:pgpsign:" + pubk.KeyIdShortString(),
|
|
Prompt: "Passphrase",
|
|
Desc: desc,
|
|
}
|
|
for tries := 0; tries < 3; tries++ {
|
|
pass, err := conn.GetPassphrase(req)
|
|
if err == nil {
|
|
err = ds.PrivateKey.Decrypt([]byte(pass))
|
|
if err == nil {
|
|
return ds, pass, err
|
|
}
|
|
req.Error = "Passphrase failed to decrypt: " + err.Error()
|
|
conn.RemoveFromCache(req.CacheKey)
|
|
continue
|
|
}
|
|
if err == gpgagent.ErrCancel {
|
|
panic("failed to decrypt key; action canceled")
|
|
}
|
|
}
|
|
default:
|
|
panic(err)
|
|
}
|
|
|
|
pinReq := &pinentry.Request{Desc: desc, Prompt: "Passphrase"}
|
|
for tries := 0; tries < 3; tries++ {
|
|
pass, err = pinReq.GetPIN()
|
|
if err == nil {
|
|
|
|
err = ds.PrivateKey.Decrypt([]byte(pass))
|
|
if err == nil {
|
|
return ds, pass, err
|
|
}
|
|
pinReq.Error = "Passphrase failed to decrypt: " + err.Error()
|
|
continue
|
|
}
|
|
if err == pinentry.ErrCancel {
|
|
panic("failed to decrypt key; action canceled")
|
|
}
|
|
}
|
|
return ds, "", fmt.Errorf("decryptEntity(): failed to decrypt key %q: %v", pubk.KeyIdShortString(), err)
|
|
}
|