diff --git a/README.md b/README.md index c3e4ca4..5ef8c53 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GoDoc](https://godoc.org/github.com/emersion/go-pgpmime?status.svg)](https://godoc.org/github.com/emersion/go-pgpmime) -A [PGP/MIME](https://tools.ietf.org/html/rfc3156) library written in Go +A [PGP/MIME](https://tools.ietf.org/html/rfc3156) library written in Go. ## License diff --git a/armor.go b/armor.go new file mode 100644 index 0000000..31a9535 --- /dev/null +++ b/armor.go @@ -0,0 +1,4 @@ +package pgpmime + +// MessageType is the armored type for PGP encrypted messages. +const MessageType = "PGP MESSAGE" diff --git a/encrypt.go b/encrypt.go new file mode 100644 index 0000000..eea5faf --- /dev/null +++ b/encrypt.go @@ -0,0 +1,103 @@ +package pgpmime + +import ( + "io" + "mime" + "mime/multipart" + "net/textproto" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/armor" + "golang.org/x/crypto/openpgp/packet" +) + +type encryptWriter struct { + multipart *multipart.Writer + armored io.WriteCloser + cleartext io.WriteCloser + + h textproto.MIMEHeader + to []*openpgp.Entity + signed *openpgp.Entity + config *packet.Config +} + +func (ew *encryptWriter) open() error { + // Create control information + h := make(textproto.MIMEHeader) + h.Add("Content-Type", "application/pgp-encrypted") + w, err := ew.multipart.CreatePart(h) + if err != nil { + return err + } + if _, err := io.WriteString(w, "Version: 1\r\n"); err != nil { + return err + } + + // Create body part + h = make(textproto.MIMEHeader) + h.Add("Content-Type", "application/octet-stream") + h.Add("Content-Disposition", "inline") + w, err = ew.multipart.CreatePart(h) + if err != nil { + return err + } + + // Create encrypted part + ew.armored, err = armor.Encode(w, MessageType, nil) + if err != nil { + return err + } + ew.cleartext, err = openpgp.Encrypt(ew.armored, ew.to, ew.signed, nil, nil) + if err != nil { + return err + } + + return writeMIMEHeader(ew.cleartext, ew.h) +} + +func (ew *encryptWriter) Write(b []byte) (n int, err error) { + // Make sure parts required at the begining of the message have been written + if ew.cleartext == nil { + if err := ew.open(); err != nil { + return 0, err + } + } + + return ew.cleartext.Write(b) +} + +func (ew *encryptWriter) Close() error { + if ew.cleartext == nil { + if err := ew.open(); err != nil { + return err + } + } + + if err := ew.cleartext.Close(); err != nil { + return err + } + if err := ew.armored.Close(); err != nil { + return err + } + return ew.multipart.Close() +} + +func (ew *encryptWriter) ContentType() string { + return mime.FormatMediaType("multipart/encrypted", map[string]string{ + "boundary": ew.multipart.Boundary(), + "protocol": "application/pgp-encrypted", + }) +} + +// Encrypt creates a new encrypted PGP/MIME message writer. +func Encrypt(w io.Writer, h textproto.MIMEHeader, to []*openpgp.Entity, signed *openpgp.Entity, config *packet.Config) (cleartext Writer) { + return &encryptWriter{ + multipart: multipart.NewWriter(w), + + h: h, + to: to, + signed: signed, + config: config, + } +} diff --git a/encrypted.go b/encrypted.go deleted file mode 100644 index de44b11..0000000 --- a/encrypted.go +++ /dev/null @@ -1,102 +0,0 @@ -// Implements MIME security with OpenPGP, as defined in RFC 3156. -package pgpmime - -import ( - "io" - "mime/multipart" - "net/textproto" - - "golang.org/x/crypto/openpgp" -) - -// A PGP/MIME encrypter. -type Encrypter struct { - multipart *multipart.Writer - armored io.WriteCloser - encrypted io.WriteCloser - - to []*openpgp.Entity - signed *openpgp.Entity - - opened bool -} - -// Write control information and create encrypted part. -func (ew *Encrypter) open() (err error) { - // Create control information - h := make(textproto.MIMEHeader) - h.Add("Content-Type", "application/pgp-encrypted") - hw, err := ew.multipart.CreatePart(h) - if err != nil { - return - } - if _, err = io.WriteString(hw, "Version: 1\r\n"); err != nil { - return - } - - // Create body part - h = make(textproto.MIMEHeader) - h.Add("Content-Type", "application/octet-stream") - h.Add("Content-Disposition", "inline") - bw, err := ew.multipart.CreatePart(h) - if err != nil { - return - } - - // Create encrypted part - if ew.armored, err = EncodeArmoredMessage(bw); err != nil { - return - } - if ew.encrypted, err = openpgp.Encrypt(ew.armored, ew.to, ew.signed, nil, nil); err != nil { - return - } - - return -} - -// Write encrypted data. -func (ew *Encrypter) Write(b []byte) (n int, err error) { - // Make sure parts required at the begining of the message have been written - if !ew.opened { - if err = ew.open(); err != nil { - return - } - ew.opened = true - } - - return ew.encrypted.Write(b) -} - -// Finish the PGP/MIME message. -func (ew *Encrypter) Close() (err error) { - if !ew.opened { - if err = ew.open(); err != nil { - return - } - ew.opened = true - } - - if err = ew.encrypted.Close(); err != nil { - return - } - if err = ew.armored.Close(); err != nil { - return - } - err = ew.multipart.Close() - return -} - -// Get the Content-Type of this PGP/MIME message. -func (ew *Encrypter) ContentType() string { - return "multipart/encrypted; boundary=" + ew.multipart.Boundary() + "; protocol=\"application/pgp-encrypted\"" -} - -// Create a new PGP/MIME encrypter. -func NewEncrypter(w io.Writer, to []*openpgp.Entity, signed *openpgp.Entity) *Encrypter { - return &Encrypter{ - multipart: multipart.NewWriter(w), - - to: to, - signed: signed, - } -} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..a039e4c --- /dev/null +++ b/example_test.go @@ -0,0 +1,100 @@ +package pgpmime_test + +import ( + "bytes" + "io" + "log" + "net/textproto" + + "github.com/emersion/go-message" + "github.com/emersion/go-message/mail" + "github.com/emersion/go-pgpmime" + "golang.org/x/crypto/openpgp" +) + +var to []*openpgp.Entity + +func ExampleEncrypt() { + var b bytes.Buffer + + // Create the mail header + mh := mail.NewHeader() + mh.SetAddressList("From", []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}) + mh.SetSubject("Your Name") + + // Create the text part header + th := mail.NewTextHeader() + th.SetContentType("text/plain", nil) + + // Create a new PGP/MIME writer + var ciphertext struct{*message.Writer} + cleartext := pgpmime.Encrypt(&ciphertext, textproto.MIMEHeader(th.Header), to, nil, nil) + + // Add the PGP/MIME Content-Type header field to the mail header + mh.Set("Content-Type", cleartext.ContentType()) + + // Create a new mail writer with our mail header + mw, err := message.CreateWriter(&b, mh.Header) + if err != nil { + log.Fatal(err) + } + // Set the PGP/MIME writer output to the mail body + ciphertext.Writer = mw + + // Write the cleartext body + _, err = io.WriteString(cleartext, "What's your name?") + if err != nil { + log.Fatal(err) + } + + // Close all writers + if err := cleartext.Close(); err != nil { + log.Fatal(err) + } + if err := mw.Close(); err != nil { + log.Fatal(err) + } + + log.Println(b.String()) +} + +func ExampleSign() { + var b bytes.Buffer + + e, err := openpgp.NewEntity("Mitsuha Miyamizu", "", "mitsuha.miyamizu@example.org", nil) + if err != nil { + log.Fatal(err) + } + + mh := mail.NewHeader() + mh.SetAddressList("From", []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}) + mh.SetSubject("Your Name") + + bh := mail.NewTextHeader() + bh.SetContentType("text/plain", nil) + + var signed struct{*message.Writer} + body := pgpmime.Sign(&signed, textproto.MIMEHeader(bh.Header), e, nil) + + mh.Set("Content-Type", body.ContentType()) + + mw, err := message.CreateWriter(&b, mh.Header) + if err != nil { + log.Fatal(err) + } + signed.Writer = mw + + _, err = io.WriteString(body, "What's your name?") + if err != nil { + log.Fatal(err) + } + + if err := body.Close(); err != nil { + log.Fatal(err) + } + if err := mw.Close(); err != nil { + log.Fatal(err) + } + + log.Println(b.String()) +} diff --git a/message.go b/message.go deleted file mode 100644 index 81dd480..0000000 --- a/message.go +++ /dev/null @@ -1,32 +0,0 @@ -package pgpmime - -import ( - "errors" - "io" - - "golang.org/x/crypto/openpgp/armor" -) - -// Armored type for PGP encrypted messages. -const MessageType = "PGP MESSAGE" - -// Encode a PGP message armor. -func EncodeArmoredMessage(w io.Writer) (io.WriteCloser, error) { - return armor.Encode(w, MessageType, nil) -} - -// Decode an armored PGP message. -func DecodeArmoredMessage(in io.Reader) (out io.Reader, err error) { - block, err := armor.Decode(in) - if err != nil { - return - } - - if block.Type != MessageType { - err = errors.New("Not an armored PGP message") - return - } - - out = block.Body - return -} diff --git a/mime.go b/mime.go new file mode 100644 index 0000000..3f04ea3 --- /dev/null +++ b/mime.go @@ -0,0 +1,27 @@ +package pgpmime + +import ( + "bytes" + "fmt" + "io" + "net/textproto" + "sort" +) + +// Borrowed from https://golang.org/src/mime/multipart/writer.go?s=2140:2215#L76 +func writeMIMEHeader(w io.Writer, header textproto.MIMEHeader) error { + var b bytes.Buffer + keys := make([]string, 0, len(header)) + for k := range header { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + for _, v := range header[k] { + fmt.Fprintf(&b, "%s: %s\r\n", k, v) + } + } + fmt.Fprintf(&b, "\r\n") + _, err := io.Copy(w, &b) + return err +} diff --git a/pgpmime.go b/pgpmime.go new file mode 100644 index 0000000..022771e --- /dev/null +++ b/pgpmime.go @@ -0,0 +1,14 @@ +// pgpmime implements MIME security with OpenPGP, as defined in RFC 3156. +package pgpmime + +import ( + "io" +) + +// Writer writes a PGP/MIME message body. +type Writer interface { + io.WriteCloser + + // ContentType returns the content type of the PGP/MIME message. + ContentType() string +} diff --git a/sign.go b/sign.go new file mode 100644 index 0000000..54fc979 --- /dev/null +++ b/sign.go @@ -0,0 +1,125 @@ +package pgpmime + +import ( + "bytes" + "io" + "mime" + "mime/multipart" + "net/textproto" + "crypto" + + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +func hashName(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" + default: + panic("pgpmime: unknown hash algorithm") + } +} + +type signWriter struct { + multipart *multipart.Writer + body io.WriteCloser + signature <-chan io.Reader + + h textproto.MIMEHeader + signer *openpgp.Entity + config *packet.Config +} + +func (sw *signWriter) open() error { + w, err := sw.multipart.CreatePart(sw.h) + if err != nil { + return err + } + + pr, pw := io.Pipe() + ch := make(chan io.Reader, 1) + sw.signature = ch + go func() { + var b bytes.Buffer + err := openpgp.ArmoredDetachSign(&b, sw.signer, pr, sw.config) + pr.CloseWithError(err) + ch <- &b + }() + + sw.body = struct{ + io.Writer + io.Closer + }{ + io.MultiWriter(w, pw), + pw, + } + return nil +} + +func (sw *signWriter) Write(b []byte) (n int, err error) { + if sw.body == nil { + if err := sw.open(); err != nil { + return 0, err + } + } + return sw.body.Write(b) +} + +func (sw *signWriter) Close() error { + if sw.body == nil { + if err := sw.open(); err != nil { + return err + } + } + + if err := sw.body.Close(); err != nil { + return err + } + + sig := <-sw.signature + + // Create signature part + h := make(textproto.MIMEHeader) + h.Add("Content-Type", "application/pgp-signature") + w, err := sw.multipart.CreatePart(h) + if err != nil { + return err + } + if _, err := io.Copy(w, sig); err != nil { + return err + } + + return sw.multipart.Close() +} + +func (sw *signWriter) ContentType() string { + return mime.FormatMediaType("multipart/signed", map[string]string{ + "boundary": sw.multipart.Boundary(), + "micalg": "pgp-" + hashName(sw.config.Hash()), + "protocol": "application/pgp-signature", + }) +} + +// Sign creates a new signed PGP/MIME message writer. +func Sign(w io.Writer, h textproto.MIMEHeader, signer *openpgp.Entity, config *packet.Config) (message Writer) { + return &signWriter{ + multipart: multipart.NewWriter(w), + + h: h, + signer: signer, + config: config, + } +}