MFA fix, added IP blacklist, update webmail client
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
package smime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Entity is a MIME entity: its own part-level headers (Content-Type,
|
||||
// Content-Transfer-Encoding, Content-Disposition — never envelope headers like
|
||||
// From/To/Subject/Date) plus its body. Sign/Encrypt/Decrypt/VerifySigned all operate
|
||||
// on an Entity, not a flat raw RFC822 message — the caller (webui's compose/read
|
||||
// handlers) is responsible for keeping envelope headers separate, since S/MIME only
|
||||
// ever transforms the message body's own MIME entity, never the envelope.
|
||||
type Entity struct {
|
||||
Headers []string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// bytes renders the entity as it would appear on the wire: headers, a blank line,
|
||||
// then the body normalized to CRLF line endings — MIME's canonical form, which is
|
||||
// what gets hashed/signed/encrypted. Both Sign and Encrypt must operate on exactly
|
||||
// this rendering so a receiving client's own canonicalization matches ours.
|
||||
func (e Entity) bytes() []byte {
|
||||
var buf bytes.Buffer
|
||||
for _, h := range e.Headers {
|
||||
buf.WriteString(h)
|
||||
buf.WriteString("\r\n")
|
||||
}
|
||||
buf.WriteString("\r\n")
|
||||
buf.Write(toCRLF(e.Body))
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// parseEntity splits raw bytes (headers, a blank line, then body) back into an
|
||||
// Entity — used to recover the inner MIME entity after Decrypt or the signed part
|
||||
// after VerifySigned, both of which hand back a full "headers+body" byte blob.
|
||||
func parseEntity(raw []byte) (Entity, error) {
|
||||
idx := bytes.Index(raw, []byte("\r\n\r\n"))
|
||||
sep := 4
|
||||
if idx < 0 {
|
||||
idx = bytes.Index(raw, []byte("\n\n"))
|
||||
sep = 2
|
||||
}
|
||||
if idx < 0 {
|
||||
return Entity{Body: raw}, nil
|
||||
}
|
||||
var headers []string
|
||||
for _, line := range strings.Split(string(raw[:idx]), "\n") {
|
||||
line = strings.TrimRight(line, "\r")
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
headers = append(headers, line)
|
||||
}
|
||||
return Entity{Headers: headers, Body: raw[idx+sep:]}, nil
|
||||
}
|
||||
|
||||
// toCRLF normalizes line endings to CRLF — first collapsing any existing CRLF to a
|
||||
// bare LF so a mixed or already-CRLF input doesn't end up double-terminated.
|
||||
func toCRLF(b []byte) []byte {
|
||||
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
|
||||
return bytes.ReplaceAll(b, []byte("\n"), []byte("\r\n"))
|
||||
}
|
||||
|
||||
// HeaderValue is a case-insensitive lookup over a MIME entity's header lines —
|
||||
// exported so callers outside this package (e.g. webui's read-integration, which
|
||||
// must inspect a message's Content-Type before deciding whether to unwrap it) don't
|
||||
// need to reimplement it.
|
||||
func HeaderValue(headers []string, name string) string {
|
||||
for _, h := range headers {
|
||||
idx := strings.Index(h, ":")
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(h[:idx]), name) {
|
||||
return strings.TrimSpace(h[idx+1:])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isBase64CTE(cte string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(cte), "base64")
|
||||
}
|
||||
|
||||
func decodeBase64(data []byte) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(stripWhitespace(string(data)))
|
||||
}
|
||||
|
||||
func stripWhitespace(s string) string {
|
||||
var b strings.Builder
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case ' ', '\t', '\r', '\n':
|
||||
continue
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func newBoundary() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return "----=_SMIME_" + hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// wrapBase64 base64-encodes data at the RFC 2045-recommended 76 characters per line —
|
||||
// cosmetic (a decoder doesn't care), but matches what every real MTA/MUA produces.
|
||||
func wrapBase64(data []byte) string {
|
||||
encoded := base64.StdEncoding.EncodeToString(data)
|
||||
var b strings.Builder
|
||||
for i := 0; i < len(encoded); i += 76 {
|
||||
end := min(i+76, len(encoded))
|
||||
b.WriteString(encoded[i:end])
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
return strings.TrimRight(b.String(), "\r\n")
|
||||
}
|
||||
|
||||
// splitMultipartRaw extracts each part's *exact* original bytes between boundary
|
||||
// delimiters — deliberately not using mime/multipart.Reader, whose Part API parses
|
||||
// headers away from the raw body and would require re-serializing them to recover
|
||||
// signable bytes. A detached S/MIME signature covers the literal octets of the
|
||||
// signed part (RFC 8551 §3.4.3), so reconstruction-from-parsed-headers risks a
|
||||
// byte-for-byte mismatch (header order, casing, whitespace) that breaks verification
|
||||
// even for semantically-identical content. Real S/MIME implementations extract raw
|
||||
// byte ranges for exactly this reason.
|
||||
func splitMultipartRaw(body []byte, boundary string) ([][]byte, error) {
|
||||
delim := []byte("--" + boundary)
|
||||
segments := bytes.Split(body, delim)
|
||||
if len(segments) < 3 {
|
||||
return nil, errors.New("smime: malformed multipart body")
|
||||
}
|
||||
// segments[0] is the preamble (ignored); the last segment starts with "--" (the
|
||||
// closing delimiter) and anything after is the epilogue (ignored). Everything in
|
||||
// between is one part, each still wrapped in the CRLF that separated it from its
|
||||
// boundary line.
|
||||
parts := make([][]byte, 0, len(segments)-2)
|
||||
for _, seg := range segments[1 : len(segments)-1] {
|
||||
seg = bytes.TrimPrefix(seg, []byte("\r\n"))
|
||||
seg = bytes.TrimSuffix(seg, []byte("\r\n"))
|
||||
parts = append(parts, seg)
|
||||
}
|
||||
return parts, nil
|
||||
}
|
||||
Reference in New Issue
Block a user