Refactor the whole library.

* Replace NewEncrypter with Encrypt.
* Add Sign
* Remove {Encode,Decode}ArmoredMessage
Fixes #1
This commit is contained in:
emersion 2017-08-06 18:56:02 +02:00
parent 4360180c2a
commit d579059ccf
No known key found for this signature in database
GPG Key ID: 0FDE7BE0E88F5E48
9 changed files with 374 additions and 135 deletions

View File

@ -2,7 +2,7 @@
[![GoDoc](https://godoc.org/github.com/emersion/go-pgpmime?status.svg)](https://godoc.org/github.com/emersion/go-pgpmime) [![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 ## License

4
armor.go Normal file
View File

@ -0,0 +1,4 @@
package pgpmime
// MessageType is the armored type for PGP encrypted messages.
const MessageType = "PGP MESSAGE"

103
encrypt.go Normal file
View File

@ -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,
}
}

View File

@ -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,
}
}

100
example_test.go Normal file
View File

@ -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())
}

View File

@ -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
}

27
mime.go Normal file
View File

@ -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
}

14
pgpmime.go Normal file
View File

@ -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
}

125
sign.go Normal file
View File

@ -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,
}
}