From 58e8baed8c2c9f866c85611afadc7679508caa24 Mon Sep 17 00:00:00 2001 From: Dominik Schulz Date: Sun, 4 Mar 2018 14:11:10 +0100 Subject: [PATCH] Add openpgp pure-Go backend (#670) Add openpgp pure-Go backend --- Gopkg.lock | 3 +- Makefile | 2 +- action/clihelper.go | 5 +- backend/context.go | 4 + backend/crypto.go | 2 + backend/crypto/gpg/openpgp/gpg.go | 284 +++++++++++++ backend/crypto/gpg/openpgp/keyring.go | 160 ++++++++ backend/crypto/gpg/openpgp/utils.go | 124 ++++++ store/sub/init.go | 2 +- store/sub/store.go | 8 + store/sub/write.go | 1 + utils/termio/ask.go | 13 +- .../x/crypto/openpgp/clearsign/clearsign.go | 376 ++++++++++++++++++ 13 files changed, 975 insertions(+), 9 deletions(-) create mode 100644 backend/crypto/gpg/openpgp/gpg.go create mode 100644 backend/crypto/gpg/openpgp/keyring.go create mode 100644 backend/crypto/gpg/openpgp/utils.go create mode 100644 vendor/golang.org/x/crypto/openpgp/clearsign/clearsign.go diff --git a/Gopkg.lock b/Gopkg.lock index b60a057949..1c3a05d780 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -243,6 +243,7 @@ "nacl/secretbox", "openpgp", "openpgp/armor", + "openpgp/clearsign", "openpgp/elgamal", "openpgp/errors", "openpgp/packet", @@ -385,6 +386,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f4570d29b1c074b48aa81ea55d134b0d966957e52c3198f906fe67420da5fe66" + inputs-digest = "9606db4b0325abdc1f3609ba446754d9dc675d211736c7708254dea57c906b95" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index cd7a113850..563c8175a1 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ FIRST_GOPATH := $(firstword $(subst :, ,$(GOPATH))) -PKGS := $(shell go list ./... | grep -v /tests | grep -v /xcpb) +PKGS := $(shell go list ./... | grep -v /tests | grep -v /xcpb | grep -v /openpgp) GOFILES_NOVENDOR := $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name "*.pb.go") GOFILES_NOTEST := $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name "*_test.go" -not -name "*.pb.go") GOPASS_VERSION ?= $(shell cat VERSION) diff --git a/action/clihelper.go b/action/clihelper.go index 4d2baf0b82..75a23322bf 100644 --- a/action/clihelper.go +++ b/action/clihelper.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" + "github.com/justwatchcom/gopass/backend/crypto/gpg" "github.com/justwatchcom/gopass/utils/ctxutil" "github.com/justwatchcom/gopass/utils/cui" "github.com/justwatchcom/gopass/utils/out" @@ -65,7 +66,7 @@ func (s *Action) askForPrivateKey(ctx context.Context, name, prompt string) (str } crypto := s.Store.Crypto(ctx, name) - kl, err := crypto.ListPrivateKeyIDs(ctx) + kl, err := crypto.ListPrivateKeyIDs(gpg.WithAlwaysTrust(ctx, false)) if err != nil { return "", err } @@ -88,7 +89,7 @@ func (s *Action) askForPrivateKey(ctx context.Context, name, prompt string) (str for i, k := range kl { fmt.Fprintf(stdout, "[%d] %s\n", i, crypto.FormatKey(ctx, k)) } - iv, err := termio.AskForInt(ctx, fmt.Sprintf("Please enter the number of a key (0-%d)", len(kl)-1), 0) + iv, err := termio.AskForInt(ctx, fmt.Sprintf("Please enter the number of a key (0-%d, [q]uit)", len(kl)-1), 0) if err != nil { continue } diff --git a/backend/context.go b/backend/context.go index 0a2a10cba6..61a3fd25ab 100644 --- a/backend/context.go +++ b/backend/context.go @@ -19,6 +19,8 @@ func CryptoBackendName(cb CryptoBackend) string { return "gpgcli" case XC: return "xc" + case OpenPGP: + return "openpgp" default: return "" } @@ -35,6 +37,8 @@ func WithCryptoBackendString(ctx context.Context, be string) context.Context { return WithCryptoBackend(ctx, GPGMock) case "xc": return WithCryptoBackend(ctx, XC) + case "openpgp": + return WithCryptoBackend(ctx, OpenPGP) default: return ctx } diff --git a/backend/crypto.go b/backend/crypto.go index 1b37d3c255..d47bf55d6d 100644 --- a/backend/crypto.go +++ b/backend/crypto.go @@ -16,6 +16,8 @@ const ( GPGCLI // XC is an experimental crypto backend XC + // OpenPGP is a GPG1.x compatible pure-Go crypto backend + OpenPGP ) // Keyring is a public/private key manager diff --git a/backend/crypto/gpg/openpgp/gpg.go b/backend/crypto/gpg/openpgp/gpg.go new file mode 100644 index 0000000000..75ffc232d7 --- /dev/null +++ b/backend/crypto/gpg/openpgp/gpg.go @@ -0,0 +1,284 @@ +package openpgp + +import ( + "bytes" + "context" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" + + "github.com/blang/semver" + "github.com/pkg/errors" +) + +// GPG is a no-op GPG mock +type GPG struct { + pubfn string + pubring openpgp.EntityList + secfn string + secring openpgp.EntityList + client agentClient +} + +// New creates a new GPG mock +func New(ctx context.Context) (*GPG, error) { + pubfn := filepath.Join(gpgHome(ctx), "pubring.gpg") + pubring, err := readKeyring(pubfn) + if err != nil { + return nil, err + } + secfn := filepath.Join(gpgHome(ctx), "secring.gpg") + secring, err := readKeyring(secfn) + if err != nil { + return nil, err + } + g := &GPG{ + pubring: pubring, + secring: secring, + pubfn: pubfn, + secfn: secfn, + } + return g, nil +} + +// RecipientIDs returns the recipients of the encrypted message +func (g *GPG) RecipientIDs(ctx context.Context, ciphertext []byte) ([]string, error) { + recps := make([]string, 0, 1) + packets := packet.NewReader(bytes.NewReader(ciphertext)) + for { + p, err := packets.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + switch p := p.(type) { + case *packet.EncryptedKey: + for _, key := range g.pubring { + if key.PrimaryKey == nil { + continue + } + if key.PrimaryKey.KeyId == p.KeyId { + recps = append(recps, key.PrimaryKey.KeyIdString()) + } + } + } + } + return recps, nil +} + +// Encrypt encrypts the plaintext for the given recipients +func (g *GPG) Encrypt(ctx context.Context, plaintext []byte, recipients []string) ([]byte, error) { + ciphertext := &bytes.Buffer{} + ents := g.recipientsToEntities(recipients) + wc, err := openpgp.Encrypt(ciphertext, ents, nil, nil, nil) + if err != nil { + return nil, errors.Wrapf(err, "failed to encrypt") + } + if _, err := io.Copy(wc, bytes.NewReader(plaintext)); err != nil { + return nil, errors.Wrapf(err, "failed to write plaintext to encoder") + } + if err := wc.Close(); err != nil { + return nil, errors.Wrapf(err, "failed to finalize encryption") + } + return ciphertext.Bytes(), nil +} + +// Decrypt decryptes the ciphertext +// see https://gist.github.com/stuart-warren/93750a142d3de4e8fdd2 +func (g *GPG) Decrypt(ctx context.Context, ciphertext []byte) ([]byte, error) { + md, err := openpgp.ReadMessage(bytes.NewReader(ciphertext), g, g.mkPromptFunc(), nil) + if err != nil { + return nil, err + } + buf := &bytes.Buffer{} + if _, err := io.Copy(buf, md.UnverifiedBody); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// ExportPublicKey does nothing +func (g *GPG) ExportPublicKey(ctx context.Context, id string) ([]byte, error) { + ent := g.findEntity(id) + if ent == nil { + return nil, fmt.Errorf("key not found") + } + + buf := &bytes.Buffer{} + err := ent.PrimaryKey.Serialize(buf) + return buf.Bytes(), err +} + +// ImportPublicKey does nothing +func (g *GPG) ImportPublicKey(ctx context.Context, buf []byte) error { + el, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(buf)) + if err != nil { + return err + } + g.pubring = append(g.pubring, el...) + return nil +} + +// Version returns dummy version info +func (g *GPG) Version(context.Context) semver.Version { + return semver.Version{Major: 1} +} + +// Binary always returns '' +func (g *GPG) Binary() string { + return "" +} + +// Sign is not implemented +func (g *GPG) Sign(ctx context.Context, in string, sigf string) error { + signKeys := g.SigningKeys() + if len(signKeys) < 1 { + return fmt.Errorf("no signing keys available") + } + + sigfh, err := os.OpenFile(sigf, os.O_WRONLY|os.O_CREATE, 0644) + if err != nil { + return err + } + defer sigfh.Close() + + wc, err := clearsign.Encode(sigfh, signKeys[0].PrivateKey, nil) + if err != nil { + return err + } + infh, err := os.Open(in) + if err != nil { + return err + } + defer infh.Close() + + if _, err := io.Copy(wc, infh); err != nil { + return err + } + if err := wc.Close(); err != nil { + return err + } + return nil +} + +// Verify is not implemented +func (g *GPG) Verify(ctx context.Context, sigf string, in string) error { + sig, err := ioutil.ReadFile(sigf) + if err != nil { + return err + } + b, _ := clearsign.Decode(sig) + infh, err := os.Open(in) + if err != nil { + return err + } + defer infh.Close() + _, err = openpgp.CheckDetachedSignature(g.pubring, infh, bytes.NewReader(b.Bytes)) + if err != nil { + return err + } + return nil +} + +// CreatePrivateKey is not implemented +func (g *GPG) CreatePrivateKey(ctx context.Context) error { + return fmt.Errorf("not yet implemented") +} + +// CreatePrivateKeyBatch is not implemented +func (g *GPG) CreatePrivateKeyBatch(ctx context.Context, name, email, pw string) error { + ent, err := openpgp.NewEntity(name, "", email, &packet.Config{ + RSABits: 4096, + }) + if err != nil { + return err + } + g.secring = append(g.secring, ent) + return g.saveSecring() +} + +// EmailFromKey returns the email for this key +func (g *GPG) EmailFromKey(ctx context.Context, id string) string { + ent := g.findEntity(id) + if ent == nil || ent.Identities == nil { + return "" + } + for name, id := range ent.Identities { + if id.UserId == nil { + return name + } + return id.UserId.Email + } + return "" +} + +// NameFromKey is returns the name for this key +func (g *GPG) NameFromKey(ctx context.Context, id string) string { + ent := g.findEntity(id) + if ent == nil || ent.Identities == nil { + return "" + } + for name, id := range ent.Identities { + if id.UserId == nil { + return name + } + return id.UserId.Name + } + return "" +} + +// FormatKey returns the id +func (g *GPG) FormatKey(ctx context.Context, id string) string { + ent := g.findEntity(id) + if ent == nil || ent.Identities == nil { + return "" + } + for name := range ent.Identities { + return name + } + return "" +} + +// Initialized returns nil +func (g *GPG) Initialized(context.Context) error { + return nil +} + +// Name returns openpgp +func (g *GPG) Name() string { + return "openpgp" +} + +// Ext returns gpg +func (g *GPG) Ext() string { + return "gpg" +} + +// IDFile returns .gpg-id +func (g *GPG) IDFile() string { + return ".gpg-id" +} + +// ReadNamesFromKey unmarshals and returns the names associated with the given public key +func (g *GPG) ReadNamesFromKey(ctx context.Context, buf []byte) ([]string, error) { + el, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(buf)) + if err != nil { + return nil, errors.Wrapf(err, "failed to read key ring") + } + if len(el) != 1 { + return nil, errors.Errorf("Public Key must contain exactly one Entity") + } + names := make([]string, 0, len(el[0].Identities)) + for _, v := range el[0].Identities { + names = append(names, v.Name) + } + return names, nil +} diff --git a/backend/crypto/gpg/openpgp/keyring.go b/backend/crypto/gpg/openpgp/keyring.go new file mode 100644 index 0000000000..b1aa6b5ae9 --- /dev/null +++ b/backend/crypto/gpg/openpgp/keyring.go @@ -0,0 +1,160 @@ +package openpgp + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + + "golang.org/x/crypto/openpgp" +) + +// ListPublicKeyIDs does nothing +func (g *GPG) ListPublicKeyIDs(context.Context) ([]string, error) { + if g.pubring == nil { + return nil, fmt.Errorf("pubring is not initialized") + } + ids := listKeyIDs(g.pubring) + if g.secring != nil { + ids = append(ids, listKeyIDs(g.secring)...) + } + return ids, nil +} + +// FindPublicKeys does nothing +func (g *GPG) FindPublicKeys(ctx context.Context, keys ...string) ([]string, error) { + kl, err := g.ListPublicKeyIDs(ctx) + if err != nil { + return nil, err + } + for i, key := range keys { + if strings.HasPrefix(key, "0x") { + key = strings.TrimPrefix(key, "0x") + } + keys[i] = strings.ToUpper(key) + } + matches := make([]string, 0, len(keys)) + for _, key := range kl { + for _, needle := range keys { + if strings.HasSuffix(key, needle) { + matches = append(matches, key) + } + } + } + if m, err := g.FindPrivateKeys(ctx, keys...); err == nil { + matches = append(matches, m...) + } + return matches, nil +} + +// ListPrivateKeyIDs does nothing +func (g *GPG) ListPrivateKeyIDs(context.Context) ([]string, error) { + if g.secring == nil { + return nil, fmt.Errorf("secring is not initialized") + } + return listKeyIDs(g.secring), nil +} + +// FindPrivateKeys does nothing +func (g *GPG) FindPrivateKeys(ctx context.Context, keys ...string) ([]string, error) { + kl, err := g.ListPrivateKeyIDs(ctx) + if err != nil { + return nil, err + } + for i, key := range keys { + if strings.HasPrefix(key, "0x") { + key = strings.TrimPrefix(key, "0x") + } + keys[i] = strings.ToUpper(key) + } + matches := make([]string, 0, len(keys)) + for _, key := range kl { + for _, needle := range keys { + if strings.HasSuffix(key, needle) { + matches = append(matches, key) + } + } + } + return matches, nil +} + +func (g *GPG) savePubring() error { + pubfnTmp := g.pubfn + ".tmp" + if err := g.writePubring(pubfnTmp); err != nil { + os.Remove(pubfnTmp) + return errors.Wrapf(err, "failed to write pubring") + } + return os.Rename(pubfnTmp, g.pubfn) +} + +func (g *GPG) writePubring(fn string) error { + fh, err := os.OpenFile(fn, os.O_WRONLY, 0644) + if err != nil { + return errors.Wrapf(err, "unable to save pubring, failed to open file: %s", err) + } + defer fh.Close() + for _, e := range g.pubring { + if err := e.Serialize(fh); err != nil { + return err + } + } + return nil +} + +func (g *GPG) saveSecring() error { + secfnTmp := g.secfn + ".tmp" + if err := g.writeSecring(secfnTmp); err != nil { + os.Remove(secfnTmp) + return errors.Wrapf(err, "failed to write secring") + } + return os.Rename(secfnTmp, g.secfn) +} + +func (g *GPG) writeSecring(fn string) error { + fh, err := os.OpenFile(fn, os.O_WRONLY, 0644) + if err != nil { + return errors.Wrapf(err, "unable to save secring, failed to open file: %s", err) + } + defer fh.Close() + for _, e := range g.secring { + if err := e.SerializePrivate(fh, nil); err != nil { + return err + } + } + return nil +} + +// KeysById implements openpgp.Keyring +func (g *GPG) KeysById(id uint64) []openpgp.Key { + return append(g.secring.KeysById(id), g.pubring.KeysById(id)...) +} + +// KeysByIdUsage implements openpgp.Keyring +func (g *GPG) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key { + return append(g.secring.KeysByIdUsage(id, requiredUsage), g.pubring.KeysByIdUsage(id, requiredUsage)...) +} + +// DecryptionKeys implements openpgp.Keyring +func (g *GPG) DecryptionKeys() []openpgp.Key { + return append(g.secring.DecryptionKeys(), g.pubring.DecryptionKeys()...) +} + +// SigningKeys returns a list of signing keys +func (g *GPG) SigningKeys() []openpgp.Key { + keys := []openpgp.Key{} + for _, e := range g.secring { + for _, subKey := range e.Subkeys { + if subKey.PrivateKey != nil && (!subKey.Sig.FlagsValid || subKey.Sig.FlagSign) { + keys = append(keys, openpgp.Key{ + Entity: e, + PublicKey: subKey.PublicKey, + PrivateKey: subKey.PrivateKey, + SelfSignature: subKey.Sig, + }) + } + } + } + return keys +} diff --git a/backend/crypto/gpg/openpgp/utils.go b/backend/crypto/gpg/openpgp/utils.go new file mode 100644 index 0000000000..7b921439c5 --- /dev/null +++ b/backend/crypto/gpg/openpgp/utils.go @@ -0,0 +1,124 @@ +package openpgp + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "github.com/justwatchcom/gopass/utils/out" + homedir "github.com/mitchellh/go-homedir" + "golang.org/x/crypto/openpgp" +) + +type agentClient interface { + Ping() error + Passphrase(string, string) (string, error) + Remove(string) error +} + +var maxUnlockAttempts = 3 + +func (g *GPG) mkPromptFunc() func([]openpgp.Key, bool) ([]byte, error) { + attempt := 0 + return func(keys []openpgp.Key, symmetric bool) ([]byte, error) { + attempt++ + if attempt > maxUnlockAttempts { + return nil, fmt.Errorf("out of retries") + } + for i, key := range keys { + if key.PublicKey == nil || key.PrivateKey == nil { + continue + } + fp := key.PublicKey.KeyIdString() + passphrase, err := g.client.Passphrase(fp, fmt.Sprintf("Unlock private key %s", fp)) + if err != nil { + continue + } + if err := keys[i].PrivateKey.Decrypt([]byte(passphrase)); err == nil { + return []byte(passphrase), nil + } + if err := g.client.Remove(fp); err != nil { + return nil, err + } + time.Sleep(10 * time.Millisecond) + } + return nil, nil + } +} + +func (g *GPG) findEntity(id string) *openpgp.Entity { + return g.findEntityInLists(id, g.secring, g.pubring) +} + +func (g *GPG) findEntityInLists(id string, els ...openpgp.EntityList) *openpgp.Entity { + id = strings.TrimPrefix(id, "0x") + for _, el := range els { + for _, ent := range el { + if ent.PrimaryKey == nil { + continue + } + fp := fmt.Sprintf("%X", ent.PrimaryKey.Fingerprint) + if strings.HasSuffix(fp, id) { + return ent + } + } + } + return nil +} + +func (g *GPG) recipientsToEntities(recipients []string) []*openpgp.Entity { + ents := make([]*openpgp.Entity, 0, len(recipients)) + for _, key := range g.pubring { + if key.PrimaryKey == nil { + continue + } + fp := fmt.Sprintf("%X", key.PrimaryKey.Fingerprint) + for _, recp := range recipients { + recp = strings.TrimPrefix(recp, "0x") + if strings.HasSuffix(fp, recp) { + ents = append(ents, key) + } + } + } + return ents +} + +func listKeyIDs(el openpgp.EntityList) []string { + ids := make([]string, 0, len(el)) + for _, key := range el { + if key.PrimaryKey == nil { + continue + } + ids = append(ids, key.PrimaryKey.KeyIdString()) + } + return ids +} + +func readKeyring(fn string) (openpgp.EntityList, error) { + fh, err := os.Open(fn) + if err != nil { + if os.IsNotExist(err) { + return openpgp.EntityList{}, nil + } + return nil, err + } + defer fh.Close() + + return openpgp.ReadKeyRing(fh) +} + +// gpgHome returns the gnupg homedir +func gpgHome(ctx context.Context) string { + if gh := os.Getenv("GNUPGHOME"); gh != "" { + return gh + } + hd, err := homedir.Dir() + if err != nil { + out.Debug(ctx, "Failed to get homedir: %s", err) + return "" + } + return filepath.Join(hd, ".gnupg") +} diff --git a/store/sub/init.go b/store/sub/init.go index 5766d81b7d..07223eb069 100644 --- a/store/sub/init.go +++ b/store/sub/init.go @@ -40,7 +40,7 @@ You can add secondary stores with gopass init --path - } if len(recipients) < 1 { - return errors.Errorf("failed to initialize store: no valid recipients given") + return errors.Errorf("failed to initialize store: no valid recipients given in %+v", ids) } kl, err := s.crypto.FindPrivateKeys(ctx, recipients...) diff --git a/store/sub/store.go b/store/sub/store.go index 0464cb6427..cb98936c6f 100644 --- a/store/sub/store.go +++ b/store/sub/store.go @@ -9,6 +9,7 @@ import ( "github.com/justwatchcom/gopass/backend" gpgcli "github.com/justwatchcom/gopass/backend/crypto/gpg/cli" gpgmock "github.com/justwatchcom/gopass/backend/crypto/gpg/mock" + "github.com/justwatchcom/gopass/backend/crypto/gpg/openpgp" "github.com/justwatchcom/gopass/backend/crypto/xc" "github.com/justwatchcom/gopass/backend/store/fs" kvmock "github.com/justwatchcom/gopass/backend/store/kv/mock" @@ -105,6 +106,13 @@ func New(ctx context.Context, alias, path string, cfgdir string) (*Store, error) //out.Red(ctx, "WARNING: Using no-op crypto backend (NO ENCRYPTION)!") s.crypto = gpgmock.New() out.Debug(ctx, "Using Crypto Backend: gpg-mock") + case backend.OpenPGP: + crypto, err := openpgp.New(ctx) + if err != nil { + return nil, err + } + s.crypto = crypto + out.Debug(ctx, "Using Crypto Backend: openpgp") default: return nil, fmt.Errorf("no valid crypto backend selected") } diff --git a/store/sub/write.go b/store/sub/write.go index d2c16e3a36..dfed8a8ab6 100644 --- a/store/sub/write.go +++ b/store/sub/write.go @@ -45,6 +45,7 @@ func (s *Store) Set(ctx context.Context, name string, sec *secret.Secret) error ciphertext, err := s.crypto.Encrypt(ctx, buf, recipients) if err != nil { + out.Debug(ctx, "Failed encrypt secret: %s", err) return store.ErrEncrypt } diff --git a/utils/termio/ask.go b/utils/termio/ask.go index 2ba14587a9..2d381f5c9e 100644 --- a/utils/termio/ask.go +++ b/utils/termio/ask.go @@ -58,9 +58,9 @@ func AskForBool(ctx context.Context, text string, def bool) (bool, error) { return def, nil } - choices := "y/N" + choices := "y/N/q" if def { - choices = "Y/n" + choices = "Y/n/q" } str, err := AskForString(ctx, text, choices) @@ -68,9 +68,9 @@ func AskForBool(ctx context.Context, text string, def bool) (bool, error) { return false, errors.Wrapf(err, "failed to read user input") } switch str { - case "Y/n": + case "Y/n/q": return true, nil - case "y/N": + case "y/N/q": return false, nil } @@ -80,6 +80,8 @@ func AskForBool(ctx context.Context, text string, def bool) (bool, error) { return true, nil case "n": return false, nil + case "q": + return false, errors.Errorf("user aborted") default: return false, errors.Errorf("Unknown answer: %s", str) } @@ -96,6 +98,9 @@ func AskForInt(ctx context.Context, text string, def int) (int, error) { if err != nil { return 0, err } + if str == "q" { + return 0, errors.Errorf("user aborted") + } intVal, err := strconv.Atoi(str) if err != nil { return 0, errors.Wrapf(err, "failed to convert to number") diff --git a/vendor/golang.org/x/crypto/openpgp/clearsign/clearsign.go b/vendor/golang.org/x/crypto/openpgp/clearsign/clearsign.go new file mode 100644 index 0000000000..def4cabaff --- /dev/null +++ b/vendor/golang.org/x/crypto/openpgp/clearsign/clearsign.go @@ -0,0 +1,376 @@ +// Copyright 2012 The Go 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 clearsign generates and processes OpenPGP, clear-signed data. See +// RFC 4880, section 7. +// +// Clearsigned messages are cryptographically signed, but the contents of the +// message are kept in plaintext so that it can be read without special tools. +package clearsign // import "golang.org/x/crypto/openpgp/clearsign" + +import ( + "bufio" + "bytes" + "crypto" + "hash" + "io" + "net/textproto" + "strconv" + + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/errors" + "golang.org/x/crypto/openpgp/packet" +) + +// A Block represents a clearsigned message. A signature on a Block can +// be checked by passing Bytes into openpgp.CheckDetachedSignature. +type Block struct { + Headers textproto.MIMEHeader // Optional message headers + Plaintext []byte // The original message text + Bytes []byte // The signed message + ArmoredSignature *armor.Block // The signature block +} + +// start is the marker which denotes the beginning of a clearsigned message. +var start = []byte("\n-----BEGIN PGP SIGNED MESSAGE-----") + +// dashEscape is prefixed to any lines that begin with a hyphen so that they +// can't be confused with endText. +var dashEscape = []byte("- ") + +// endText is a marker which denotes the end of the message and the start of +// an armored signature. +var endText = []byte("-----BEGIN PGP SIGNATURE-----") + +// end is a marker which denotes the end of the armored signature. +var end = []byte("\n-----END PGP SIGNATURE-----") + +var crlf = []byte("\r\n") +var lf = byte('\n') + +// getLine returns the first \r\n or \n delineated line from the given byte +// array. The line does not include the \r\n or \n. The remainder of the byte +// array (also not including the new line bytes) is also returned and this will +// always be smaller than the original argument. +func getLine(data []byte) (line, rest []byte) { + i := bytes.Index(data, []byte{'\n'}) + var j int + if i < 0 { + i = len(data) + j = i + } else { + j = i + 1 + if i > 0 && data[i-1] == '\r' { + i-- + } + } + return data[0:i], data[j:] +} + +// Decode finds the first clearsigned message in data and returns it, as well +// as the suffix of data which remains after the message. +func Decode(data []byte) (b *Block, rest []byte) { + // start begins with a newline. However, at the very beginning of + // the byte array, we'll accept the start string without it. + rest = data + if bytes.HasPrefix(data, start[1:]) { + rest = rest[len(start)-1:] + } else if i := bytes.Index(data, start); i >= 0 { + rest = rest[i+len(start):] + } else { + return nil, data + } + + // Consume the start line. + _, rest = getLine(rest) + + var line []byte + b = &Block{ + Headers: make(textproto.MIMEHeader), + } + + // Next come a series of header lines. + for { + // This loop terminates because getLine's second result is + // always smaller than its argument. + if len(rest) == 0 { + return nil, data + } + // An empty line marks the end of the headers. + if line, rest = getLine(rest); len(line) == 0 { + break + } + + i := bytes.Index(line, []byte{':'}) + if i == -1 { + return nil, data + } + + key, val := line[0:i], line[i+1:] + key = bytes.TrimSpace(key) + val = bytes.TrimSpace(val) + b.Headers.Add(string(key), string(val)) + } + + firstLine := true + for { + start := rest + + line, rest = getLine(rest) + if len(line) == 0 && len(rest) == 0 { + // No armored data was found, so this isn't a complete message. + return nil, data + } + if bytes.Equal(line, endText) { + // Back up to the start of the line because armor expects to see the + // header line. + rest = start + break + } + + // The final CRLF isn't included in the hash so we don't write it until + // we've seen the next line. + if firstLine { + firstLine = false + } else { + b.Bytes = append(b.Bytes, crlf...) + } + + if bytes.HasPrefix(line, dashEscape) { + line = line[2:] + } + line = bytes.TrimRight(line, " \t") + b.Bytes = append(b.Bytes, line...) + + b.Plaintext = append(b.Plaintext, line...) + b.Plaintext = append(b.Plaintext, lf) + } + + // We want to find the extent of the armored data (including any newlines at + // the end). + i := bytes.Index(rest, end) + if i == -1 { + return nil, data + } + i += len(end) + for i < len(rest) && (rest[i] == '\r' || rest[i] == '\n') { + i++ + } + armored := rest[:i] + rest = rest[i:] + + var err error + b.ArmoredSignature, err = armor.Decode(bytes.NewBuffer(armored)) + if err != nil { + return nil, data + } + + return b, rest +} + +// A dashEscaper is an io.WriteCloser which processes the body of a clear-signed +// message. The clear-signed message is written to buffered and a hash, suitable +// for signing, is maintained in h. +// +// When closed, an armored signature is created and written to complete the +// message. +type dashEscaper struct { + buffered *bufio.Writer + h hash.Hash + hashType crypto.Hash + + atBeginningOfLine bool + isFirstLine bool + + whitespace []byte + byteBuf []byte // a one byte buffer to save allocations + + privateKey *packet.PrivateKey + config *packet.Config +} + +func (d *dashEscaper) Write(data []byte) (n int, err error) { + for _, b := range data { + d.byteBuf[0] = b + + if d.atBeginningOfLine { + // The final CRLF isn't included in the hash so we have to wait + // until this point (the start of the next line) before writing it. + if !d.isFirstLine { + d.h.Write(crlf) + } + d.isFirstLine = false + } + + // Any whitespace at the end of the line has to be removed so we + // buffer it until we find out whether there's more on this line. + if b == ' ' || b == '\t' || b == '\r' { + d.whitespace = append(d.whitespace, b) + d.atBeginningOfLine = false + continue + } + + if d.atBeginningOfLine { + // At the beginning of a line, hyphens have to be escaped. + if b == '-' { + // The signature isn't calculated over the dash-escaped text so + // the escape is only written to buffered. + if _, err = d.buffered.Write(dashEscape); err != nil { + return + } + d.h.Write(d.byteBuf) + d.atBeginningOfLine = false + } else if b == '\n' { + // Nothing to do because we delay writing CRLF to the hash. + } else { + d.h.Write(d.byteBuf) + d.atBeginningOfLine = false + } + if err = d.buffered.WriteByte(b); err != nil { + return + } + } else { + if b == '\n' { + // We got a raw \n. Drop any trailing whitespace and write a + // CRLF. + d.whitespace = d.whitespace[:0] + // We delay writing CRLF to the hash until the start of the + // next line. + if err = d.buffered.WriteByte(b); err != nil { + return + } + d.atBeginningOfLine = true + } else { + // Any buffered whitespace wasn't at the end of the line so + // we need to write it out. + if len(d.whitespace) > 0 { + d.h.Write(d.whitespace) + if _, err = d.buffered.Write(d.whitespace); err != nil { + return + } + d.whitespace = d.whitespace[:0] + } + d.h.Write(d.byteBuf) + if err = d.buffered.WriteByte(b); err != nil { + return + } + } + } + } + + n = len(data) + return +} + +func (d *dashEscaper) Close() (err error) { + if !d.atBeginningOfLine { + if err = d.buffered.WriteByte(lf); err != nil { + return + } + } + sig := new(packet.Signature) + sig.SigType = packet.SigTypeText + sig.PubKeyAlgo = d.privateKey.PubKeyAlgo + sig.Hash = d.hashType + sig.CreationTime = d.config.Now() + sig.IssuerKeyId = &d.privateKey.KeyId + + if err = sig.Sign(d.h, d.privateKey, d.config); err != nil { + return + } + + out, err := armor.Encode(d.buffered, "PGP SIGNATURE", nil) + if err != nil { + return + } + + if err = sig.Serialize(out); err != nil { + return + } + if err = out.Close(); err != nil { + return + } + if err = d.buffered.Flush(); err != nil { + return + } + return +} + +// Encode returns a WriteCloser which will clear-sign a message with privateKey +// and write it to w. If config is nil, sensible defaults are used. +func Encode(w io.Writer, privateKey *packet.PrivateKey, config *packet.Config) (plaintext io.WriteCloser, err error) { + if privateKey.Encrypted { + return nil, errors.InvalidArgumentError("signing key is encrypted") + } + + hashType := config.Hash() + name := nameOfHash(hashType) + if len(name) == 0 { + return nil, errors.UnsupportedError("unknown hash type: " + strconv.Itoa(int(hashType))) + } + + if !hashType.Available() { + return nil, errors.UnsupportedError("unsupported hash type: " + strconv.Itoa(int(hashType))) + } + h := hashType.New() + + buffered := bufio.NewWriter(w) + // start has a \n at the beginning that we don't want here. + if _, err = buffered.Write(start[1:]); err != nil { + return + } + if err = buffered.WriteByte(lf); err != nil { + return + } + if _, err = buffered.WriteString("Hash: "); err != nil { + return + } + if _, err = buffered.WriteString(name); err != nil { + return + } + if err = buffered.WriteByte(lf); err != nil { + return + } + if err = buffered.WriteByte(lf); err != nil { + return + } + + plaintext = &dashEscaper{ + buffered: buffered, + h: h, + hashType: hashType, + + atBeginningOfLine: true, + isFirstLine: true, + + byteBuf: make([]byte, 1), + + privateKey: privateKey, + config: config, + } + + return +} + +// nameOfHash returns the OpenPGP name for the given hash, or the empty string +// if the name isn't known. See RFC 4880, section 9.4. +func nameOfHash(h crypto.Hash) string { + switch h { + case crypto.MD5: + return "MD5" + case crypto.SHA1: + return "SHA1" + case crypto.RIPEMD160: + return "RIPEMD160" + case crypto.SHA224: + return "SHA224" + case crypto.SHA256: + return "SHA256" + case crypto.SHA384: + return "SHA384" + case crypto.SHA512: + return "SHA512" + } + return "" +}