mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 18:50:37 +01:00
217 lines
5.9 KiB
Go
217 lines
5.9 KiB
Go
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mail
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"mime/quotedprintable"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidPrivateKey should be used if private key is invalid
|
|
ErrInvalidPrivateKey = errors.New("invalid private key")
|
|
|
|
// ErrInvalidCertificate should be used if the certificate is invalid
|
|
ErrInvalidCertificate = errors.New("invalid certificate")
|
|
)
|
|
|
|
// privateKeyHolder is the representation of a private key
|
|
type privateKeyHolder struct {
|
|
ecdsa *ecdsa.PrivateKey
|
|
rsa *rsa.PrivateKey
|
|
}
|
|
|
|
// get returns the private key of the privateKeyHolder
|
|
func (p privateKeyHolder) get() crypto.PrivateKey {
|
|
if p.ecdsa != nil {
|
|
return p.ecdsa
|
|
}
|
|
return p.rsa
|
|
}
|
|
|
|
// SMime is used to sign messages with S/MIME
|
|
type SMime struct {
|
|
privateKey privateKeyHolder
|
|
certificate *x509.Certificate
|
|
intermediateCertificate *x509.Certificate
|
|
}
|
|
|
|
// newSMimeWithRSA construct a new instance of SMime with provided parameters
|
|
// privateKey as *rsa.PrivateKey
|
|
// certificate as *x509.Certificate
|
|
// intermediateCertificate (optional) as *x509.Certificate
|
|
func newSMimeWithRSA(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) {
|
|
if privateKey == nil {
|
|
return nil, ErrInvalidPrivateKey
|
|
}
|
|
|
|
if certificate == nil {
|
|
return nil, ErrInvalidCertificate
|
|
}
|
|
|
|
return &SMime{
|
|
privateKey: privateKeyHolder{rsa: privateKey},
|
|
certificate: certificate,
|
|
intermediateCertificate: intermediateCertificate,
|
|
}, nil
|
|
}
|
|
|
|
// newSMimeWithECDSA construct a new instance of SMime with provided parameters
|
|
// privateKey as *ecdsa.PrivateKey
|
|
// certificate as *x509.Certificate
|
|
// intermediateCertificate (optional) as *x509.Certificate
|
|
func newSMimeWithECDSA(privateKey *ecdsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) {
|
|
if privateKey == nil {
|
|
return nil, ErrInvalidPrivateKey
|
|
}
|
|
|
|
if certificate == nil {
|
|
return nil, ErrInvalidCertificate
|
|
}
|
|
|
|
return &SMime{
|
|
privateKey: privateKeyHolder{ecdsa: privateKey},
|
|
certificate: certificate,
|
|
intermediateCertificate: intermediateCertificate,
|
|
}, nil
|
|
}
|
|
|
|
// signMessage signs the message with S/MIME
|
|
func (sm *SMime) signMessage(message string) (*string, error) {
|
|
lines := parseLines([]byte(message))
|
|
toBeSigned := lines.bytesFromLines([]byte("\r\n"))
|
|
|
|
signedData, err := newSignedData(toBeSigned)
|
|
if err != nil || signedData == nil {
|
|
return nil, fmt.Errorf("could not initialize signed data: %w", err)
|
|
}
|
|
|
|
if err = signedData.addSigner(sm.certificate, sm.privateKey.get(), SignerInfoConfig{}); err != nil {
|
|
return nil, fmt.Errorf("could not add signer message: %w", err)
|
|
}
|
|
|
|
if sm.intermediateCertificate != nil {
|
|
signedData.addCertificate(sm.intermediateCertificate)
|
|
}
|
|
|
|
signedData.detach()
|
|
|
|
signatureDER, err := signedData.finish()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not finish signing: %w", err)
|
|
}
|
|
|
|
pemMsg, err := encodeToPEM(signatureDER)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not encode to PEM: %w", err)
|
|
}
|
|
|
|
return pemMsg, nil
|
|
}
|
|
|
|
// createMessage prepares the message that will be used for the sign method later
|
|
func (sm *SMime) prepareMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) (*string, error) {
|
|
encodedMessage, err := sm.encodeMessage(encoding, string(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
preparedMessage := fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, *encodedMessage)
|
|
return &preparedMessage, nil
|
|
}
|
|
|
|
// encodeMessage encodes the message with the given encoding
|
|
func (sm *SMime) encodeMessage(encoding Encoding, message string) (*string, error) {
|
|
if encoding != EncodingQP {
|
|
return &message, nil
|
|
}
|
|
|
|
buffer := bytes.Buffer{}
|
|
writer := quotedprintable.NewWriter(&buffer)
|
|
if _, err := writer.Write([]byte(message)); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
encodedMessage := buffer.String()
|
|
|
|
return &encodedMessage, nil
|
|
}
|
|
|
|
// encodeToPEM uses the method pem.Encode from the standard library but cuts the typical PEM preamble
|
|
func encodeToPEM(msg []byte) (*string, error) {
|
|
block := &pem.Block{Bytes: msg}
|
|
|
|
var arrayBuffer bytes.Buffer
|
|
if err := pem.Encode(&arrayBuffer, block); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r := arrayBuffer.String()
|
|
r = strings.TrimPrefix(r, "-----BEGIN -----")
|
|
r = strings.Trim(r, "\n")
|
|
r = strings.TrimSuffix(r, "-----END -----")
|
|
r = strings.Trim(r, "\n")
|
|
|
|
return &r, nil
|
|
}
|
|
|
|
// line is the representation of one line of the message that will be used for signing purposes
|
|
type line struct {
|
|
line []byte
|
|
endOfLine []byte
|
|
}
|
|
|
|
// lines is the representation of a message that will be used for signing purposes
|
|
type lines []line
|
|
|
|
// bytesFromLines creates the line representation with the given endOfLine char
|
|
func (ls lines) bytesFromLines(sep []byte) []byte {
|
|
var raw []byte
|
|
for i := range ls {
|
|
raw = append(raw, ls[i].line...)
|
|
if len(ls[i].endOfLine) != 0 && sep != nil {
|
|
raw = append(raw, sep...)
|
|
} else {
|
|
raw = append(raw, ls[i].endOfLine...)
|
|
}
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// parseLines constructs the lines representation of a given message
|
|
func parseLines(raw []byte) lines {
|
|
oneLine := line{raw, nil}
|
|
lines := lines{oneLine}
|
|
lines = lines.splitLine([]byte("\r\n"))
|
|
lines = lines.splitLine([]byte("\r"))
|
|
lines = lines.splitLine([]byte("\n"))
|
|
return lines
|
|
}
|
|
|
|
// splitLine uses the given endOfLine to split the given line
|
|
func (ls lines) splitLine(sep []byte) lines {
|
|
nl := lines{}
|
|
for _, l := range ls {
|
|
split := bytes.Split(l.line, sep)
|
|
if len(split) > 1 {
|
|
for i := 0; i < len(split)-1; i++ {
|
|
nl = append(nl, line{split[i], sep})
|
|
}
|
|
nl = append(nl, line{split[len(split)-1], l.endOfLine})
|
|
} else {
|
|
nl = append(nl, l)
|
|
}
|
|
}
|
|
return nl
|
|
}
|