pax_global_header00006660000000000000000000000064145452646530014530gustar00rootroot0000000000000052 comment=8d0fb004e861f12866f72681854b9c35f2c9fd44 go-pgpmail-0.2.1/000077500000000000000000000000001454526465300135645ustar00rootroot00000000000000go-pgpmail-0.2.1/.build.yml000066400000000000000000000003211454526465300154600ustar00rootroot00000000000000image: alpine/latest packages: - go sources: - https://github.com/emersion/go-pgpmail tasks: - build: | cd go-pgpmail go build -v ./... - test: | cd go-pgpmail go test -v ./... go-pgpmail-0.2.1/.gitignore000066400000000000000000000004411454526465300155530ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # editors stuff .idea # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof go-pgpmail-0.2.1/LICENSE000066400000000000000000000020521454526465300145700ustar00rootroot00000000000000MIT License Copyright (c) 2020 Simon Ser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-pgpmail-0.2.1/README.md000066400000000000000000000005221454526465300150420ustar00rootroot00000000000000# go-pgpmail [![godocs.io](https://godocs.io/github.com/emersion/go-pgpmail?status.svg)](https://godocs.io/github.com/emersion/go-pgpmail) [![builds.sr.ht status](https://builds.sr.ht/~emersion/go-pgpmail/commits.svg)](https://builds.sr.ht/~emersion/go-pgpmail/commits?) A mail library that encrypts messages with PGP. ## License MIT go-pgpmail-0.2.1/example_test.go000066400000000000000000000063521454526465300166130ustar00rootroot00000000000000package pgpmail_test import ( "bytes" "io" "log" "github.com/ProtonMail/go-crypto/openpgp" "github.com/emersion/go-message" "github.com/emersion/go-message/mail" "github.com/emersion/go-pgpmail" ) func ExampleRead() { // Let's assume r contains an e-mail, which is maybe encrypted or signed var r io.Reader // A private key is needed in case the message is encrypted var privateKey *openpgp.Entity pgpReader, err := pgpmail.Read(r, openpgp.EntityList{privateKey}, nil, nil) if err != nil { log.Fatal(err) } log.Printf("Header: %v", pgpReader.Header) // pgpReader.MessageDetails.UnverifiedBody contains the whole wrapped e-mail entity, err := message.Read(pgpReader.MessageDetails.UnverifiedBody) if err != nil { log.Fatal(err) } // Do something with the wrapped e-mail log.Printf("Wrapped header: %v", entity.Header) var buf bytes.Buffer if _, err := io.Copy(&buf, entity.Body); err != nil { log.Fatal(err) } // Now that the wrapped e-mail has been read, we can check the signature. // We can only do this if the wrapped e-mail has been fully consumed. if err := pgpReader.MessageDetails.SignatureError; err != nil { log.Fatal(err) } log.Printf("Signed: %v", pgpReader.MessageDetails.IsSigned) log.Printf("Encrypted: %v", pgpReader.MessageDetails.IsEncrypted) } func ExampleEncrypt() { // to are the recipients' keys, signer is the sender's key var to []*openpgp.Entity var signer *openpgp.Entity var mailHeader mail.Header mailHeader.SetAddressList("From", []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}) mailHeader.SetAddressList("To", []*mail.Address{{"Taki Tachibana", "taki.tachibana@example.org"}}) var encryptedHeader mail.Header encryptedHeader.SetContentType("text/plain", nil) encryptedText := "Hi! I'm Mitsuha Miyamizu." var buf bytes.Buffer cleartext, err := pgpmail.Encrypt(&buf, mailHeader.Header.Header, to, signer, nil) if err != nil { log.Fatal(err) } defer cleartext.Close() body, err := mail.CreateSingleInlineWriter(cleartext, encryptedHeader) if err != nil { log.Fatal(err) } defer body.Close() if _, err := io.WriteString(body, encryptedText); err != nil { log.Fatal(err) } if err := body.Close(); err != nil { log.Fatal(err) } if err := cleartext.Close(); err != nil { log.Fatal(err) } log.Print(buf.String()) } func ExampleSign() { // signer is the sender's key var signer *openpgp.Entity var mailHeader mail.Header mailHeader.SetAddressList("From", []*mail.Address{{"Mitsuha Miyamizu", "mitsuha.miyamizu@example.org"}}) mailHeader.SetAddressList("To", []*mail.Address{{"Taki Tachibana", "taki.tachibana@example.org"}}) var signedHeader mail.Header signedHeader.SetContentType("text/plain", nil) signedText := "Hi! I'm Mitsuha Miyamizu." var buf bytes.Buffer cleartext, err := pgpmail.Sign(&buf, mailHeader.Header.Header, signer, nil) if err != nil { log.Fatal(err) } defer cleartext.Close() body, err := mail.CreateSingleInlineWriter(cleartext, signedHeader) if err != nil { log.Fatal(err) } defer body.Close() if _, err := io.WriteString(body, signedText); err != nil { log.Fatal(err) } if err := body.Close(); err != nil { log.Fatal(err) } if err := cleartext.Close(); err != nil { log.Fatal(err) } log.Print(buf.String()) } go-pgpmail-0.2.1/go.mod000066400000000000000000000003631454526465300146740ustar00rootroot00000000000000module github.com/emersion/go-pgpmail go 1.12 require ( github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c github.com/cloudflare/circl v1.3.7 // indirect github.com/emersion/go-message v0.17.0 golang.org/x/text v0.14.0 ) go-pgpmail-0.2.1/go.sum000066400000000000000000000132421454526465300147210ustar00rootroot00000000000000github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/emersion/go-message v0.17.0 h1:NIdSKHiVUx4qKqdd0HyJFD41cW8iFguM2XJnRZWQH04= github.com/emersion/go-message v0.17.0/go.mod h1:/9Bazlb1jwUNB0npYYBsdJ2EMOiiyN3m5UVHbY7GoNw= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 h1:IbFBtwoTQyw0fIM5xv1HF+Y+3ZijDR839WMulgxCcUY= github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/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-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= go-pgpmail-0.2.1/pgpmail.go000066400000000000000000000006741454526465300155530ustar00rootroot00000000000000// Package pgpmail implements PGP encryption for e-mail messages. // // PGP/MIME is defined in RFC 3156. package pgpmail import ( "crypto" ) // See RFC 4880, section 9.4. var hashAlgs = map[string]crypto.Hash{ "pgp-md5": crypto.MD5, "pgp-sha1": crypto.SHA1, "pgp-ripemd160": crypto.RIPEMD160, "pgp-sha256": crypto.SHA256, "pgp-sha384": crypto.SHA384, "pgp-sha512": crypto.SHA512, "pgp-sha224": crypto.SHA224, } go-pgpmail-0.2.1/pgpmail_data_test.go000066400000000000000000000124131454526465300175750ustar00rootroot00000000000000package pgpmail const testPrivateKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK----- 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 +5anTnUn =gemU -----END PGP PRIVATE KEY BLOCK----- ` const testPublicKeyArmored = `-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBF5FJf8BCACvlKhSSsv4P8C3Wbv391SrNUBtFquoMuWKtuCr/Ks6KHuofGLn bM55uBSQp908aITBDPkaOPsQ3OvwgF7SM8bNIDVpO7FHzCEg2Ysp99iPET/+LsbY ugc8oYSuvA5aFFIOMYbAbI+HmbIBuCs+xp0AcU1cemAPzPBDCZs4xl5Y+/ce2yQz ZGK9O/tQQIKoBUOWLo/0byAWyD6Gwn/Le3fVxxK6RPeeizDV6VfzHLxhxBNkMgmd QUkBkvqF154wYxhzsHn72ushbJpspKz1LQN7d5u6QOq3h2sLwcLbT457qbMfZsZs HLhoOibOd+yJ7C6TRbbyC4sQRr+K1CNGcvhJABEBAAG0NEpvaG4gRG9lIChUaGlz IGlzIGEgdGVzdCBrZXkpIDxqb2huLmRvZUBleGFtcGxlLm9yZz6JAU4EEwEIADgW IQSxqGaTVBU7eZ8iF78wchXBPfepZAUCXkUl/wIbAwULCQgHAgYVCgkICwIEFgID AQIeAQIXgAAKCRAwchXBPfepZF4iB/49B7q4AfO3xHEa8LK2H+f7Mnm4dRfS2YPo v2p6TRe1h2DxwpTevNQUhXw2U0nfRIEKBAZqgb7NVktkoh0DWtKatms2yHMAS+ah lQoHb2gRgXa9M9Tq0x5u9sl0NYnx7Wu5uu6Ybw9luPKoAfO91T0vei0p3eMn3fIV 0O012ITvmgKJPppQDKFJHGZJMbVDO4TNxP89HgyhB41RO7AZadvu73S00x2K6x+O R4s/++4Y98vScCPm3DUOXeoHXKGqFcNYTxJL9bsE2I0uYgvJSxNoK1dVnmvxp3zz hcxAdzizgMz0ufY6YLMCjy5MDOzPARkmYPXdkJ6jceOIqGLUw1kquQENBF5FJf8B CACpsh5cyHB7eEwQvLzJVsXpTW0Rh/Fe36AwC2Vz13WeE6GFrOvw1qATvtYB1919 M4B44YH9J7I5SrFZad86Aw4n5Gi0BwLlGNa/oCMvYzlNHaTXURA271ghJqdZizqV UETj3WNoaYm4mYMfb0dcayDJvVPWP7InzOsdIRU9WXBUSyVMxNMXccr2UvIuhdPg lmVT8NtsWR+q8xBoL2Dp0ojYLVD3MlwKe1pE5mEwasYCkWePLWyGdfDW1MhUDsPH 3K1IjpPLWU9FBk8KM4z8WooY9/kyMIyRw39MvOHGfgcFBpiZwlELNZGSFhbRun03 PMk2Qd3k+0FGV1IhFAYsr7QRABEBAAGJATYEGAEIACAWIQSxqGaTVBU7eZ8iF78w chXBPfepZAUCXkUl/wIbDAAKCRAwchXBPfepZOtqB/9xsGEgQgm70KYID39H91k4 ef/RlpRDY1ndC0MoPfqE03IEXTC/MjtU+ksPKEoZeQsxVaUJ2WBueI5WGJ3Y73pO HAd7N0SyGHT5s6gK1FSx29be1qiPwUu5KR2jpm3RjgpbymnOWe4C6iiYCFQ85IX+ LzpE+p9bB02PUrmzOb4MBV6E5mg30UjXIX01+bwZq5XSB4/FaUrQOAxLuRvVRjK0 CEcFbPGIlkPSW6s4M9xCC2sQi7caFKVK6Zqf78KbOwAHqfS0x9u2jtTIhsgCjGTI AOQ5lNwpLEMjwLias6e5sM6hcK9Wo+A9Sw23f8lMau5clOZTJeyAUAff+5anTnUn =ZjQT -----END PGP PUBLIC KEY BLOCK----- ` go-pgpmail-0.2.1/pgpmail_test.go000066400000000000000000000016571454526465300166140ustar00rootroot00000000000000package pgpmail import ( "fmt" "strings" "time" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/packet" ) type zeroReader struct{} func (zeroReader) Read(buf []byte) (int, error) { for i := range buf { buf[i] = 0 } return len(buf), nil } var testPrivateKey = mustReadArmoredEntity(testPrivateKeyArmored) var testPublicKey = mustReadArmoredEntity(testPublicKeyArmored) var testConfig = &packet.Config{ Rand: &zeroReader{}, Time: func() time.Time { return time.Date(2020, 2, 20, 0, 0, 0, 0, time.UTC) }, } func mustReadArmoredEntity(s string) *openpgp.Entity { el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(s)) if err != nil { panic(fmt.Errorf("pgpmail: failed to read test key: %v", err)) } if len(el) != 1 { panic("pgpmail: test keyring doesn't contain exactly one key") } return el[0] } func toCRLF(s string) string { return strings.ReplaceAll(s, "\n", "\r\n") } go-pgpmail-0.2.1/reader.go000066400000000000000000000175471454526465300153730ustar00rootroot00000000000000package pgpmail import ( "bufio" "bytes" "crypto" "fmt" "hash" "io" "mime" "strings" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/emersion/go-message/textproto" ) type Reader struct { Header textproto.Header MessageDetails *openpgp.MessageDetails } func NewReader(h textproto.Header, body io.Reader, keyring openpgp.KeyRing, prompt openpgp.PromptFunction, config *packet.Config) (*Reader, error) { t, params, err := mime.ParseMediaType(h.Get("Content-Type")) if err != nil { return nil, err } if strings.EqualFold(t, "multipart/encrypted") && strings.EqualFold(params["protocol"], "application/pgp-encrypted") { mr := textproto.NewMultipartReader(body, params["boundary"]) return newEncryptedReader(h, mr, keyring, prompt, config) } if strings.EqualFold(t, "multipart/signed") && strings.EqualFold(params["protocol"], "application/pgp-signature") { micalg := params["micalg"] mr := textproto.NewMultipartReader(body, params["boundary"]) return newSignedReader(h, mr, micalg, keyring, prompt, config) } var headerBuf bytes.Buffer textproto.WriteHeader(&headerBuf, h) return &Reader{ Header: h, MessageDetails: &openpgp.MessageDetails{ UnverifiedBody: io.MultiReader(&headerBuf, body), }, }, nil } func Read(r io.Reader, keyring openpgp.KeyRing, prompt openpgp.PromptFunction, config *packet.Config) (*Reader, error) { br := bufio.NewReader(r) h, err := textproto.ReadHeader(br) if err != nil { return nil, err } return NewReader(h, br, keyring, prompt, config) } func newEncryptedReader(h textproto.Header, mr *textproto.MultipartReader, keyring openpgp.KeyRing, prompt openpgp.PromptFunction, config *packet.Config) (*Reader, error) { p, err := mr.NextPart() if err != nil { return nil, fmt.Errorf("pgpmail: failed to read first part in multipart/encrypted message: %v", err) } t, _, err := mime.ParseMediaType(p.Header.Get("Content-Type")) if err != nil { return nil, fmt.Errorf("pgpmail: failed to parse Content-Type of first part in multipart/encrypted message: %v", err) } if !strings.EqualFold(t, "application/pgp-encrypted") { return nil, fmt.Errorf("pgpmail: first part in multipart/encrypted message has type %q, not application/pgp-encrypted", t) } metadata, err := textproto.ReadHeader(bufio.NewReader(p)) if err != nil { return nil, fmt.Errorf("pgpmail: failed to parse application/pgp-encrypted part: %v", err) } if s := metadata.Get("Version"); s != "1" { return nil, fmt.Errorf("pgpmail: unsupported PGP/MIME version: %q", s) } p, err = mr.NextPart() if err != nil { return nil, fmt.Errorf("pgpmail: failed to read second part in multipart/encrypted message: %v", err) } t, _, err = mime.ParseMediaType(p.Header.Get("Content-Type")) if err != nil { return nil, fmt.Errorf("pgpmail: failed to parse Content-Type of second part in multipart/encrypted message: %v", err) } if !strings.EqualFold(t, "application/octet-stream") { return nil, fmt.Errorf("pgpmail: second part in multipart/encrypted message has type %q, not application/octet-stream", t) } block, err := armor.Decode(p) if err != nil { return nil, fmt.Errorf("pgpmail: failed to parse encrypted armored data: %v", err) } md, err := openpgp.ReadMessage(block.Body, keyring, prompt, config) if err != nil { return nil, fmt.Errorf("pgpmail: failed to read PGP message: %v", err) } cleartext := bufio.NewReader(md.UnverifiedBody) cleartextHeader, err := textproto.ReadHeader(cleartext) if err != nil { return nil, fmt.Errorf("pgpmail: failed to read encrypted header: %v", err) } t, params, err := mime.ParseMediaType(cleartextHeader.Get("Content-Type")) if err != nil { return nil, err } if md.IsEncrypted && !md.IsSigned && strings.EqualFold(t, "multipart/signed") && strings.EqualFold(params["protocol"], "application/pgp-signature") { // RFC 1847 encapsulation, see RFC 3156 section 6.1 micalg := params["micalg"] mr := textproto.NewMultipartReader(cleartext, params["boundary"]) sr, err := newSignedReader(cleartextHeader, mr, micalg, keyring, prompt, config) if err != nil { return nil, fmt.Errorf("pgpmail: failed to read encapsulated multipart/signed message: %v", err) } sr.MessageDetails.IsEncrypted = md.IsEncrypted sr.MessageDetails.EncryptedToKeyIds = md.EncryptedToKeyIds sr.MessageDetails.IsSymmetricallyEncrypted = md.IsSymmetricallyEncrypted sr.MessageDetails.DecryptedWith = md.DecryptedWith return sr, nil } var headerBuf bytes.Buffer textproto.WriteHeader(&headerBuf, cleartextHeader) md.UnverifiedBody = io.MultiReader(&headerBuf, cleartext) return &Reader{ Header: h, MessageDetails: md, }, nil } type signedReader struct { keyring openpgp.KeyRing multipart *textproto.MultipartReader signed io.Reader hashFunc crypto.Hash hash hash.Hash md *openpgp.MessageDetails } func (r *signedReader) Read(b []byte) (int, error) { n, err := r.signed.Read(b) r.hash.Write(b[:n]) if err == io.EOF { r.md.SignatureError = r.check() } return n, err } func (r *signedReader) check() error { part, err := r.multipart.NextPart() if err != nil { return fmt.Errorf("pgpmail: failed to read signature part of multipart/signed message: %v", err) } t, _, err := mime.ParseMediaType(part.Header.Get("Content-Type")) if err != nil { return fmt.Errorf("pgpmail: failed to parse Content-Type of signature part in multipart/encrypted message: %v", err) } if !strings.EqualFold(t, "application/pgp-signature") { return fmt.Errorf("pgpmail: signature part in multipart/encrypted message has type %q, not application/pgp-signature", t) } block, err := armor.Decode(part) if err != nil { return fmt.Errorf("pgpmail: failed to read armored signature block: %v", err) } var p packet.Packet var keys []openpgp.Key var sigErr error pr := packet.NewReader(block.Body) for { p, err = pr.Next() if err == io.EOF { break } else if err != nil { return fmt.Errorf("pgpmail: failed to read signature: %v", err) } sig, ok := p.(*packet.Signature) if !ok { return fmt.Errorf("pgpmail: non signature packet found") } if sig.IssuerKeyId == nil { return fmt.Errorf("pgpmail: signature doesn't have an issuer") } issuerKeyId := *sig.IssuerKeyId hashFunc := sig.Hash r.md.SignedByKeyId = issuerKeyId if hashFunc != r.hashFunc { return fmt.Errorf("pgpmail: micalg mismatch: multipart header indicates %v but signature packet indicates %v", r.hashFunc, hashFunc) } keys = r.keyring.KeysByIdUsage(issuerKeyId, packet.KeyFlagSign) if len(keys) == 0 { continue } for i, key := range keys { sigErr := key.PublicKey.VerifySignature(r.hash, sig) if sigErr == nil { r.md.SignedBy = &keys[i] return nil } } } if sigErr != nil { return sigErr } return pgperrors.ErrUnknownIssuer } func newSignedReader(h textproto.Header, mr *textproto.MultipartReader, micalg string, keyring openpgp.KeyRing, prompt openpgp.PromptFunction, config *packet.Config) (*Reader, error) { micalg = strings.ToLower(micalg) hashFunc, ok := hashAlgs[micalg] if !ok { return nil, fmt.Errorf("pgpmail: unsupported micalg %q", micalg) } if !hashFunc.Available() { return nil, fmt.Errorf("pgpmail: micalg %q unavailable", micalg) } hash := hashFunc.New() p, err := mr.NextPart() if err != nil { return nil, fmt.Errorf("pgpmail: failed to read signed part in multipart/signed message: %v", err) } var headerBuf bytes.Buffer textproto.WriteHeader(&headerBuf, p.Header) // TODO: convert line endings to CRLF md := &openpgp.MessageDetails{IsSigned: true} sr := &signedReader{ keyring: keyring, multipart: mr, signed: io.MultiReader(&headerBuf, p), hashFunc: hashFunc, hash: hash, md: md, } md.UnverifiedBody = sr return &Reader{ Header: h, MessageDetails: md, }, nil } go-pgpmail-0.2.1/reader_test.go000066400000000000000000000226311454526465300164200ustar00rootroot00000000000000package pgpmail import ( "bytes" "io" "io/ioutil" "strings" "testing" "github.com/ProtonMail/go-crypto/openpgp" pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" ) func checkSignature(t *testing.T, md *openpgp.MessageDetails) { primaryKeyId := testPrivateKey.PrimaryKey.KeyId if md.SignatureError != nil { t.Errorf("MessageDetails.SignatureError = %v", md.SignatureError) } if !md.IsSigned { t.Errorf("MessageDetails.IsSigned != true") } if md.SignedBy == nil { t.Errorf("MessageDetails.SignedBy == nil") } if md.SignedByKeyId != primaryKeyId { t.Errorf("MessageDetails.SignedByKeyId = %v, want %v", md.SignedByKeyId, primaryKeyId) } } func checkEncryption(t *testing.T, md *openpgp.MessageDetails) { encryptedTo := testPrivateKey.Subkeys[0].PublicKey.KeyId if !md.IsEncrypted { t.Errorf("MessageDetails.IsEncrypted != true") } if len(md.EncryptedToKeyIds) != 1 { t.Errorf("MessageDetails.EncryptedToKeyIds = %v, want exactly one key", md.EncryptedToKeyIds) } else if md.EncryptedToKeyIds[0] != encryptedTo { t.Errorf("MessageDetails.EncryptedToKeyIds = %v, want key %v", md.EncryptedToKeyIds, encryptedTo) } } func TestReader_encryptedSignedPGPMIME(t *testing.T) { sr := strings.NewReader(testPGPMIMEEncryptedSigned) r, err := Read(sr, openpgp.EntityList{testPrivateKey}, nil, nil) if err != nil { t.Fatalf("pgpmail.Read() = %v", err) } var buf bytes.Buffer if _, err := io.Copy(&buf, r.MessageDetails.UnverifiedBody); err != nil { t.Fatalf("io.Copy() = %v", err) } checkSignature(t, r.MessageDetails) checkEncryption(t, r.MessageDetails) if s := buf.String(); s != testEncryptedBody { t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, testEncryptedBody) } } func TestReader_encryptedSignedEncapsulatedPGPMIME(t *testing.T) { sr := strings.NewReader(testPGPMIMEEncryptedSignedEncapsulated) r, err := Read(sr, openpgp.EntityList{testPrivateKey}, nil, nil) if err != nil { t.Fatalf("pgpmail.Read() = %v", err) } var buf bytes.Buffer if _, err := io.Copy(&buf, r.MessageDetails.UnverifiedBody); err != nil { t.Fatalf("io.Copy() = %v", err) } checkSignature(t, r.MessageDetails) checkEncryption(t, r.MessageDetails) if s := buf.String(); s != testSignedBody { t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, testSignedBody) } } func TestReader_signedPGPMIME(t *testing.T) { sr := strings.NewReader(testPGPMIMESigned) r, err := Read(sr, openpgp.EntityList{testPublicKey}, nil, nil) if err != nil { t.Fatalf("pgpmail.Read() = %v", err) } var buf bytes.Buffer if _, err := io.Copy(&buf, r.MessageDetails.UnverifiedBody); err != nil { t.Fatalf("io.Copy() = %v", err) } if r.MessageDetails.IsEncrypted { t.Errorf("MessageDetails.IsEncrypted != false") } checkSignature(t, r.MessageDetails) if s := buf.String(); s != testSignedBody { t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, testSignedBody) } } func TestReader_signedPGPMIMEInvalid(t *testing.T) { sr := strings.NewReader(testPGPMIMESignedInvalid) r, err := Read(sr, openpgp.EntityList{testPrivateKey}, nil, nil) if err != nil { t.Fatalf("pgpmail.Read() = %v", err) } if _, err := io.Copy(ioutil.Discard, r.MessageDetails.UnverifiedBody); err != nil { t.Fatalf("io.Copy() = %v", err) } if err := r.MessageDetails.SignatureError; err == nil { t.Errorf("MessageDetails.SignatureError = nil") } } func TestReader_signedPGPMIMEUnknownIssuer(t *testing.T) { sr := strings.NewReader(testPGPMIMESigned) r, err := Read(sr, openpgp.EntityList{}, nil, nil) if err != nil { t.Fatalf("pgpmail.Read() = %v", err) } if _, err := io.Copy(ioutil.Discard, r.MessageDetails.UnverifiedBody); err != nil { t.Fatalf("io.Copy() = %v", err) } if err := r.MessageDetails.SignatureError; err != pgperrors.ErrUnknownIssuer { t.Errorf("MessageDetails.SignatureError = %v, want ErrUnknownIssuer", err) } } func TestReader_plaintext(t *testing.T) { sr := strings.NewReader(testPlaintext) r, err := Read(sr, openpgp.EntityList(nil), nil, nil) if err != nil { t.Fatalf("pgpmail.Read() = %v", err) } var buf bytes.Buffer if _, err := io.Copy(&buf, r.MessageDetails.UnverifiedBody); err != nil { t.Fatalf("io.Copy() = %v", err) } if r.MessageDetails.IsEncrypted { t.Errorf("MessageDetails.IsEncrypted != false") } if r.MessageDetails.IsSigned { t.Errorf("MessageDetails.IsSigned != false") } if s := buf.String(); s != testPlaintext { t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, testPlaintext) } } var testEncryptedBody = toCRLF(`Content-Type: text/plain This is an encrypted message! `) var testSignedBody = toCRLF(`Content-Type: text/plain This is a signed message! `) var testPGPMIMEEncryptedSigned = toCRLF(`From: John Doe To: John Doe Mime-Version: 1.0 Content-Type: multipart/encrypted; boundary=foo; protocol="application/pgp-encrypted" --foo Content-Type: application/pgp-encrypted Version: 1 --foo Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- hQEMAxF0jxulHQ8+AQf/SBK2FIIgMA4OkCvlqty/1GmAumWq6J0T+pRLppXHvYFb jbXRzz2h3pE/OoouI6vWzBwb8xU/5f8neen+fvdsF1N6PyLjZcHRB91oPvP8TuHA 0vEpiQDbP+0wlQ8BmMnnV06HokWJoKXGmIle0L4QszT/QCbrT80UgKrqXNVHKQtN DUcytFsUCmolZRj074FEpEetjH6QGEX5hAYNBUJziXmOv7vdd4AFgNbbgC5j5ezz h8tCAKUqeUiproYaAMrI0lfqh/t8bacJNkljI2LOxYfdJ/2317Npwly0OqpCM3YT Q4dHuuGM6IuZHtIc9sneIBRhKf8WnWt14hLkHUT80dLA/AHKl0jGYqO34Dxd9JNB EEwQ4j6rxauOEbKLAuYYaEqCzNYBasBrPmpNb4Fx2syWkCoYzwvzv7nj4I8vIBmm FGsAQLX4c18qtZI4XaG4FPUvFQ01Y0rjTxAV3u51lrYjCxFuI5ZEtiT0J/Tv2Unw R6xwtARkEf3W0agegmohEjjkAexKNxGrlulLiPk2j9/dnlAxeGpOuhYuYU2kYbKq x3TkcVYRs1FkmCX0YHNJ2zVWLfDYd2f3UVkXINe7mODGx2A2BxvK9Ig7NMuNmWZE ELiLSIvQk9jlgqWUMwSGPQKaHPrac02EjcBHef2zCoFbTg0TXQeDr5SV7yguX8jB zZnoNs+6+GR1gA6poKzFdiG4NRr0SNgEHazPPkXp3P2KyOINyFJ7SA+HX8iegTqL CTPYPK7UNRmb5s2u5B4e9NiQB9L85W4p7p7uemCSu9bxjs8rkCJpvx9Kb8jzPW17 wnEUe10A4JNDBhxiMg+Fm5oM2VxQVy+eDVFOOq7pDYVcSmZc36wO+EwAKph9shby O4sDS4l/8eQTEYUxTavdtQ9O9ZMXvf/L3Rl1uFJXw1lFwPReXwtpA485e031/A== =P0jf -----END PGP MESSAGE----- --foo-- `) var testPGPMIMEEncryptedSignedEncapsulated = toCRLF(`From: John Doe To: John Doe Mime-Version: 1.0 Content-Type: multipart/encrypted; boundary=foo; protocol="application/pgp-encrypted" --foo Content-Type: application/pgp-encrypted Version: 1 --foo Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- hQEMAxF0jxulHQ8+AQf9FCth8p+17rzWL0AtKP+aWndvVUYmaKiUZd+Ya8D9cRnc FAP//JnRvTPhdOyl8x1FQkVxyuKcgpjaClb6/OLgD0lGYLC15p43G4QyU+jtOOQW FFjZj2z8wUuiev8ejNd7DMiOQRSm4d+IIK+Qa2BJ10Y9AuLQtMI8D+joP1D11NeX 4FO3SYFEuwH5VWlXGo3bRjg8fKFVG/r/xCwBibqRpfjVnS4EgI04XCsnhqdaCRvE Bw2XEaF62m2MUNbaan410WajzVSbSIqIHw8U7vpR/1nisS+SZmScuCXWFa6W9YgR 0nSWi1io2Ratf4F9ORCy0o7QPh7FlpsIUGmp4paF39LpAQ2q0OUnFhkIdLVQscQT JJXLbZwp0CYTAgqwdRWFwY7rEPm2k/Oe4cHKJLEn0hS+X7wch9FAYEMifeqa0FcZ GjxocAlyhmlM0sXIDYP8xx49t4O8JIQU1ep/SX2+rUAKIh2WRdYDy8GrrHba8V8U aBCU9zIMhmOtu7r+FE1djMUhcaSbbvC9zLDMLV8QxogGhxrqaUM8Pj+q1H6myaAr o1xd65b6r2Bph6GUmcMwl28i78u9bKoM0mI+EdUuLwS9EbmjtIwEgxNv4LqK8xw2 /tjCe9JSqg+HDaBYnO4QTM29Y+PltRIe6RxpnBcYULTLcSt1UK3YV1KvhqfXMjoZ THsvtxLbmPYFv+g0hiUpuKtyG9NGidKCxrjvNq30KCSUWzNFkh+qv6CPm26sXr5F DTsVpFTM/lomg4Po8sE20BZsk/9IzEh4ERSOu3k0m3mI4QAyJmrOpVGUjd//4cqz Zhhc3tV78BtEYNh0a+78fAHGtdLocLj5IfOCYQWW//EtOY93TnVAtP0puaiNOc8q Vvb5WMamiRJZ9nQXP3paDoqD14B9X6bvNWsDQDkkrWls2sYg7KzqpOM/nlXLBKQd Ok4EJfOpd0hICPwo6tJ6sK2meRcDLxtGJybADE7UHJ4t0SrQBfn/sQhRytQtg2wr U1Thy6RujlrrrdUryo3Mi+xc9Ot1o35JszCjNQGL6BCFsGi9fx5pjWM+lLiJ15aJ jh02mSd/8j7IaJCGgTuyq6uK45EoVqWd1WRSYl4s5tg1g1jckigYYjJdAKNnU/rZ iTk5F8GSyv30EXnqvrs= =Ibxd -----END PGP MESSAGE----- --foo-- `) var testPGPMIMESigned = toCRLF(`From: John Doe To: John Doe Mime-Version: 1.0 Content-Type: multipart/signed; boundary=bar; micalg=pgp-SHA256; protocol="application/pgp-signature" --bar Content-Type: text/plain This is a signed message! --bar Content-Type: application/pgp-signature -----BEGIN PGP SIGNATURE----- iQEzBAABCAAdFiEEsahmk1QVO3mfIhe/MHIVwT33qWQFAl5FRLgACgkQMHIVwT33 qWSEQQf/YgRlKlQzSyvm6A52lGIRU3F/z9EGjhCryxj+hSdPlk8O7iZFIjnco4Ea 7QIlsOj6D4AlLdhyK6c8IZV7rZoTNE5rc6I5UZjM4Qa0XoyLjao28zR252TtwwWJ e4+wrTQKcVhCyHO6rkvcCpru4qF5CU+Mi8+sf8CNJJyBgw1Pri35rJWMdoTPTqqz kcIGN1JySaI8bbVitJQmnm0FtFTiB7zznv94rMBCiPmPUWd9BSpSBJteJoBLZ+K7 Y7ws2Dzp2sBo/RLUM18oXd0N9PLXvFGI3IuF8ey1SPzQH3QbBdJSTmLzRlPjK7A1 HVHFb3vTjd71z9j5IGQQ3Awdw30zMg== =gOul -----END PGP SIGNATURE----- --bar-- `) var testPGPMIMESignedInvalid = toCRLF(`From: John Doe To: John Doe Mime-Version: 1.0 Content-Type: multipart/signed; boundary=bar; micalg=pgp-sha256; protocol="application/pgp-signature" --bar Content-Type: text/plain This is a signed message, but the signature is invalid. --bar Content-Type: application/pgp-signature -----BEGIN PGP SIGNATURE----- iQEzBAABCAAdFiEEsahmk1QVO3mfIhe/MHIVwT33qWQFAl5FRLgACgkQMHIVwT33 qWSEQQf/YgRlKlQzSyvm6A52lGIRU3F/z9EGjhCryxj+hSdPlk8O7iZFIjnco4Ea 7QIlsOj6D4AlLdhyK6c8IZV7rZoTNE5rc6I5UZjM4Qa0XoyLjao28zR252TtwwWJ e4+wrTQKcVhCyHO6rkvcCpru4qF5CU+Mi8+sf8CNJJyBgw1Pri35rJWMdoTPTqqz kcIGN1JySaI8bbVitJQmnm0FtFTiB7zznv94rMBCiPmPUWd9BSpSBJteJoBLZ+K7 Y7ws2Dzp2sBo/RLUM18oXd0N9PLXvFGI3IuF8ey1SPzQH3QbBdJSTmLzRlPjK7A1 HVHFb3vTjd71z9j5IGQQ3Awdw30zMg== =gOul -----END PGP SIGNATURE----- --bar-- `) var testPlaintext = toCRLF(`From: John Doe To: John Doe Mime-Version: 1.0 Content-Type: text/plain This is a plaintext message! `) go-pgpmail-0.2.1/writer.go000066400000000000000000000156301454526465300154340ustar00rootroot00000000000000package pgpmail import ( "bufio" "bytes" "fmt" "io" "mime" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/emersion/go-message/textproto" "golang.org/x/text/transform" ) // for tests var forceBoundary = "" type multiCloser []io.Closer func (mc multiCloser) Close() error { for _, c := range mc { if err := c.Close(); err != nil { return err } } return nil } func Encrypt(w io.Writer, h textproto.Header, to []*openpgp.Entity, signed *openpgp.Entity, config *packet.Config) (io.WriteCloser, error) { mw := textproto.NewMultipartWriter(w) if forceBoundary != "" { mw.SetBoundary(forceBoundary) } params := map[string]string{ "boundary": mw.Boundary(), "protocol": "application/pgp-encrypted", } h.Set("Content-Type", mime.FormatMediaType("multipart/encrypted", params)) if err := textproto.WriteHeader(w, h); err != nil { return nil, err } var controlHeader textproto.Header controlHeader.Set("Content-Type", "application/pgp-encrypted") controlWriter, err := mw.CreatePart(controlHeader) if err != nil { return nil, err } if _, err := controlWriter.Write([]byte("Version: 1\r\n")); err != nil { return nil, err } var encryptedHeader textproto.Header encryptedHeader.Set("Content-Type", "application/octet-stream") encryptedWriter, err := mw.CreatePart(encryptedHeader) if err != nil { return nil, err } // armor uses LF lines endings, but we need CRLF crlfWriter := transform.NewWriter(encryptedWriter, &crlfTransformer{}) armorWriter, err := armor.Encode(crlfWriter, "PGP MESSAGE", nil) if err != nil { return nil, err } plaintext, err := openpgp.EncryptText(armorWriter, to, signed, nil, config) if err != nil { return nil, err } return struct { io.Writer io.Closer }{ plaintext, multiCloser{ plaintext, armorWriter, mw, }, }, nil } type signer struct { io.Writer pw *io.PipeWriter done <-chan error sigBuf bytes.Buffer mw *textproto.MultipartWriter closed bool } func (s *signer) Close() error { if s.closed { return fmt.Errorf("pgpmail: signer already closed") } s.closed = true // Close the pipe to let openpgp.DetachSign finish if err := s.pw.Close(); err != nil { return err } if err := <-s.done; err != nil { return err } // At this point s.sigBuf contains the complete signature var sigHeader textproto.Header sigHeader.Set("Content-Type", "application/pgp-signature") sigWriter, err := s.mw.CreatePart(sigHeader) if err != nil { return err } // armor uses LF lines endings, but we need CRLF crlfWriter := transform.NewWriter(sigWriter, &crlfTransformer{}) armorWriter, err := armor.Encode(crlfWriter, "PGP MESSAGE", nil) if err != nil { return err } if _, err := io.Copy(armorWriter, &s.sigBuf); err != nil { return err } if err := armorWriter.Close(); err != nil { return err } return s.mw.Close() } func Sign(w io.Writer, header textproto.Header, signed *openpgp.Entity, config *packet.Config) (io.WriteCloser, error) { // We need to grab the header written to the returned io.WriteCloser, then // use it to create a new part in the multipart/signed message mw := textproto.NewMultipartWriter(w) if forceBoundary != "" { mw.SetBoundary(forceBoundary) } var micalg string for name, hash := range hashAlgs { if hash == config.Hash() { micalg = name break } } if micalg == "" { return nil, fmt.Errorf("pgpmail: unknown hash algorithm %v", config.Hash()) } params := map[string]string{ "boundary": mw.Boundary(), "protocol": "application/pgp-signature", "micalg": micalg, } header.Set("Content-Type", mime.FormatMediaType("multipart/signed", params)) if err := textproto.WriteHeader(w, header); err != nil { return nil, err } handleHeader := func(signedHeader textproto.Header) (io.WriteCloser, error) { signedWriter, err := mw.CreatePart(signedHeader) if err != nil { return nil, err } // TODO: canonicalize text written to signedWriter pr, pw := io.Pipe() done := make(chan error, 1) s := &signer{ Writer: io.MultiWriter(pw, signedWriter), pw: pw, done: done, mw: mw, } go func() { err := openpgp.DetachSignText(&s.sigBuf, signed, pr, config) // Close the pipe to make sure textproto.WriteHeader doesn't block pr.CloseWithError(err) done <- err }() if err := textproto.WriteHeader(pw, signedHeader); err != nil { pw.Close() return nil, err } return s, nil } return &headerWriter{handle: handleHeader}, nil } var ( doubleCRLF = []byte("\r\n\r\n") doubleLF = []byte("\n\n") ) func hasDoubleCRLFSuffix(b []byte) bool { return bytes.HasSuffix(b, doubleCRLF) || bytes.HasSuffix(b, doubleLF) } // headerWriter collects a header written to itself, calls handle, and writes the // body to the returned io.WriteCloser. // // If handle returns an io.WriteCloser, its Close method is guaranteed to be // called when the headerWriter is closed. type headerWriter struct { handle func(textproto.Header) (io.WriteCloser, error) headerBuf bytes.Buffer headerComplete bool bodyWriter io.WriteCloser err error } func (hw *headerWriter) Write(buf []byte) (int, error) { if hw.headerComplete { if hw.err != nil { return 0, hw.err } return hw.bodyWriter.Write(buf) } hw.headerBuf.Grow(len(buf)) gotDoubleCRLF := false N := 0 for _, b := range buf { hw.headerBuf.WriteByte(b) N++ if b == '\n' && hasDoubleCRLFSuffix(hw.headerBuf.Bytes()) { gotDoubleCRLF = true break } } if gotDoubleCRLF { if err := hw.parseHeader(); err != nil { return N, err } n, err := hw.bodyWriter.Write(buf[N:]) return N + n, err } return N, nil } func (hw *headerWriter) Close() error { // Ensure we always close the underlying io.WriterCloser, to avoid leaking // resources if hw.bodyWriter != nil { defer hw.bodyWriter.Close() } if !hw.headerComplete { if err := hw.parseHeader(); err != nil { return err } } if hw.err != nil { return hw.err } return hw.bodyWriter.Close() } func (hw *headerWriter) parseHeader() error { hw.headerComplete = true h, err := textproto.ReadHeader(bufio.NewReader(&hw.headerBuf)) if err != nil { hw.err = err return err } hw.bodyWriter, hw.err = hw.handle(h) return hw.err } // crlfTranformer transforms lone LF characters with CRLF. type crlfTransformer struct { cr bool } func (tr *crlfTransformer) Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) { for _, c := range src { if c == '\r' { tr.cr = true } if c == '\n' { if tr.cr { tr.cr = false } else { if nDst+1 >= len(dst) { err = transform.ErrShortDst break } dst[nDst] = '\r' nDst++ } } if nDst >= len(dst) { err = transform.ErrShortDst break } dst[nDst] = c nDst++ nSrc++ } return nDst, nSrc, err } func (tr *crlfTransformer) Reset() { tr.cr = false } var _ transform.Transformer = (*crlfTransformer)(nil) go-pgpmail-0.2.1/writer_test.go000066400000000000000000000077301454526465300164750ustar00rootroot00000000000000package pgpmail import ( "bytes" "io" "io/ioutil" "strings" "testing" "github.com/ProtonMail/go-crypto/openpgp" "github.com/emersion/go-message/textproto" ) func init() { forceBoundary = "foo" } func TestEncrypt(t *testing.T) { var h textproto.Header h.Set("From", "John Doe ") h.Set("To", "John Doe ") var encryptedHeader textproto.Header encryptedHeader.Set("Content-Type", "text/plain") var encryptedBody = "This is an encrypted message!" to := []*openpgp.Entity{testPublicKey} var buf bytes.Buffer cleartext, err := Encrypt(&buf, h, to, testPrivateKey, testConfig) if err != nil { t.Fatalf("Encrypt() = %v", err) } if err := textproto.WriteHeader(cleartext, encryptedHeader); err != nil { t.Fatalf("textproto.WriteHeader() = %v", err) } if _, err := io.WriteString(cleartext, encryptedBody); err != nil { t.Fatalf("io.WriteString() = %v", err) } if err := cleartext.Close(); err != nil { t.Fatalf("ciphertext.Close() = %v", err) } s := buf.String() if !strings.HasPrefix(s, wantEncryptedPrefix) || !strings.HasSuffix(s, wantEncryptedSuffix) { t.Errorf("Encrypt() has invalid structure:\n%v", s) } r, err := Read(&buf, openpgp.EntityList{testPrivateKey}, nil, nil) if err != nil { t.Fatalf("Read() = %v", err) } b, err := ioutil.ReadAll(r.MessageDetails.UnverifiedBody) if err != nil { t.Fatalf("ReadAll() = %v", err) } checkSignature(t, r.MessageDetails) checkEncryption(t, r.MessageDetails) encryptedMessage := formatMessage(encryptedHeader, encryptedBody) if s := string(b); s != encryptedMessage { t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, encryptedMessage) } } func TestSign(t *testing.T) { var h textproto.Header h.Set("From", "John Doe ") h.Set("To", "John Doe ") var signedHeader textproto.Header signedHeader.Set("Content-Type", "text/plain") var signedBody = "This is a signed message!" var buf bytes.Buffer cleartext, err := Sign(&buf, h, testPrivateKey, testConfig) if err != nil { t.Fatalf("Sign() = %v", err) } if err := textproto.WriteHeader(cleartext, signedHeader); err != nil { t.Fatalf("textproto.WriteHeader() = %v", err) } if _, err := io.WriteString(cleartext, signedBody); err != nil { t.Fatalf("io.WriteString() = %v", err) } if err := cleartext.Close(); err != nil { t.Fatalf("ciphertext.Close() = %v", err) } s := buf.String() if !strings.HasPrefix(s, wantSignedPrefix) || !strings.HasSuffix(s, wantSignedSuffix) { t.Errorf("Sign() has invalid structure:\n%v", s) } r, err := Read(&buf, openpgp.EntityList{testPrivateKey}, nil, nil) if err != nil { t.Fatalf("Read() = %v", err) } b, err := ioutil.ReadAll(r.MessageDetails.UnverifiedBody) if err != nil { t.Fatalf("ReadAll() = %v", err) } checkSignature(t, r.MessageDetails) signedMessage := formatMessage(signedHeader, signedBody) if s := string(b); s != signedMessage { t.Errorf("MessagesDetails.UnverifiedBody = \n%v\n but want \n%v", s, signedMessage) } } func formatMessage(h textproto.Header, body string) string { var sb strings.Builder textproto.WriteHeader(&sb, h) sb.WriteString(body) return sb.String() } var wantEncryptedPrefix = toCRLF(`Content-Type: multipart/encrypted; boundary=foo; protocol="application/pgp-encrypted" To: John Doe From: John Doe --foo Content-Type: application/pgp-encrypted Version: 1 --foo Content-Type: application/octet-stream -----BEGIN PGP MESSAGE----- `) var wantEncryptedSuffix = toCRLF(` -----END PGP MESSAGE----- --foo-- `) var wantSignedPrefix = toCRLF(`Content-Type: multipart/signed; boundary=foo; micalg=pgp-sha256; protocol="application/pgp-signature" To: John Doe From: John Doe --foo Content-Type: text/plain This is a signed message! --foo Content-Type: application/pgp-signature -----BEGIN PGP MESSAGE----- `) var wantSignedSuffix = toCRLF(` -----END PGP MESSAGE----- --foo-- `)