557 lines
13 KiB
Go
557 lines
13 KiB
Go
//
|
|
// message.go
|
|
//
|
|
package srnd
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/sha512"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/dchest/blake2b"
|
|
"io"
|
|
"log"
|
|
"mime"
|
|
"mime/multipart"
|
|
"net/mail"
|
|
"net/textproto"
|
|
"strings"
|
|
)
|
|
|
|
type ArticleHeaders map[string][]string
|
|
|
|
func (self ArticleHeaders) Has(key string) bool {
|
|
_, ok := self[key]
|
|
return ok
|
|
}
|
|
|
|
func (self ArticleHeaders) Set(key, val string) {
|
|
self[key] = []string{val}
|
|
}
|
|
|
|
func (self ArticleHeaders) Add(key, val string) {
|
|
if self.Has(key) {
|
|
self[key] = append(self[key], val)
|
|
} else {
|
|
self.Set(key, val)
|
|
}
|
|
}
|
|
|
|
func (self ArticleHeaders) Len() (l int) {
|
|
for k := range self {
|
|
l += len(self[k])
|
|
}
|
|
return
|
|
}
|
|
|
|
func (self ArticleHeaders) Get(key, fallback string) string {
|
|
val, ok := self[key]
|
|
if ok {
|
|
str := ""
|
|
for _, k := range val {
|
|
str += k + ", "
|
|
}
|
|
return str[:len(str)-2]
|
|
} else {
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
type NNTPMessage interface {
|
|
// this message's messsge id
|
|
MessageID() string
|
|
// the parent message's messageid if it's specified
|
|
Reference() string
|
|
// the newsgroup this post is in
|
|
Newsgroup() string
|
|
// the name of the poster
|
|
Name() string
|
|
// any email address associated with the post
|
|
Email() string
|
|
// the subject of the post
|
|
Subject() string
|
|
// when this was posted
|
|
Posted() int64
|
|
// the path header
|
|
Path() string
|
|
// get signed part
|
|
SignedPart() NNTPAttachment
|
|
// append something to path
|
|
// return message with new path
|
|
AppendPath(part string) NNTPMessage
|
|
// the type of this message usually a mimetype
|
|
ContentType() string
|
|
// was this post a sage?
|
|
Sage() bool
|
|
// was this post a root post?
|
|
OP() bool
|
|
// all attachments
|
|
Attachments() []NNTPAttachment
|
|
FrontendPubkey() string
|
|
// all headers
|
|
Headers() ArticleHeaders
|
|
MIMEHeader() textproto.MIMEHeader
|
|
// write out everything
|
|
WriteTo(wr io.Writer, limit int64) error
|
|
// write out body
|
|
WriteBody(wr io.Writer, limit int64) error
|
|
// attach a file
|
|
Attach(att NNTPAttachment)
|
|
// get the plaintext message if it exists
|
|
Message() string
|
|
// pack the whole message and prepare for write
|
|
Pack()
|
|
// get the pubkey for this message if it was signed, otherwise empty string
|
|
Pubkey() string
|
|
// get the origin encrypted address, i2p destination or empty string for onion posters
|
|
Addr() string
|
|
// reset contents
|
|
Reset()
|
|
// get body as reader
|
|
BodyReader() io.Reader
|
|
}
|
|
|
|
func (self *nntpArticle) FrontendPubkey() string {
|
|
return self.headers.Get("X-Frontend-Pubkey", "")
|
|
}
|
|
|
|
type nntpArticle struct {
|
|
// mime header
|
|
headers ArticleHeaders
|
|
// multipart boundary
|
|
boundary string
|
|
// the text part of the message
|
|
message string
|
|
// any attachments
|
|
attachments []NNTPAttachment
|
|
// the inner nntp message to be verified
|
|
signedPart *nntpAttachment
|
|
}
|
|
|
|
func (self *nntpArticle) Reset() {
|
|
self.headers = nil
|
|
self.boundary = ""
|
|
self.message = ""
|
|
if self.attachments != nil {
|
|
for idx := range self.attachments {
|
|
self.attachments[idx].Reset()
|
|
self.attachments[idx] = nil
|
|
}
|
|
}
|
|
self.attachments = nil
|
|
if self.signedPart != nil {
|
|
self.signedPart.Reset()
|
|
self.signedPart = nil
|
|
}
|
|
}
|
|
|
|
func (self *nntpArticle) SignedPart() NNTPAttachment {
|
|
return self.signedPart
|
|
}
|
|
|
|
// create a simple plaintext nntp message
|
|
func newPlaintextArticle(message, email, subject, name, instance, message_id, newsgroup string) NNTPMessage {
|
|
nntp := &nntpArticle{
|
|
headers: make(ArticleHeaders),
|
|
}
|
|
nntp.headers.Set("From", formatAddress(name, email))
|
|
nntp.headers.Set("Subject", subject)
|
|
if isSage(subject) {
|
|
nntp.headers.Set("X-Sage", "1")
|
|
}
|
|
nntp.headers.Set("Path", instance)
|
|
nntp.headers.Set("Message-ID", message_id)
|
|
// posted now
|
|
nntp.headers.Set("Date", timeNowStr())
|
|
nntp.headers.Set("Newsgroups", newsgroup)
|
|
nntp.message = strings.Trim(message, "\r")
|
|
nntp.Pack()
|
|
return nntp
|
|
}
|
|
|
|
// sign an article with a seed
|
|
func signArticle(nntp NNTPMessage, seed []byte) (signed *nntpArticle, err error) {
|
|
signed = new(nntpArticle)
|
|
signed.headers = make(ArticleHeaders)
|
|
h := nntp.Headers()
|
|
// copy headers
|
|
// copy into signed part
|
|
for k := range h {
|
|
if k == "X-PubKey-Ed25519" || k == "X-Signature-Ed25519-SHA512" || k == "X-Signature-Ed25519-BLAKE2B" {
|
|
// don't set signature or pubkey header
|
|
} else if k == "Content-Type" {
|
|
signed.headers.Set(k, "message/rfc822")
|
|
} else {
|
|
v := h[k][0]
|
|
signed.headers.Set(k, v)
|
|
}
|
|
}
|
|
sha := sha512.New()
|
|
blake := blake2b.New512()
|
|
signed.signedPart = &nntpAttachment{}
|
|
// write body to sign buffer
|
|
mw := io.MultiWriter(sha, blake, signed.signedPart)
|
|
err = nntp.WriteTo(mw, MaxMessageSize)
|
|
mw.Write([]byte{10})
|
|
if err == nil {
|
|
// build keypair
|
|
pk, sk := naclSeedToKeyPair(seed)
|
|
// sign it nigguh
|
|
digest := sha.Sum(nil)
|
|
sig := cryptoSignFucky(digest, sk)
|
|
// log that we signed it
|
|
// log.Printf("signed %s pubkey=%s sig=%s hash=%s", nntp.MessageID(), pk, sig, hexify(digest))
|
|
signed.headers.Set("X-Signature-Ed25519-SHA512", sig)
|
|
signed.headers.Set("X-PubKey-Ed25519", hexify(pk[:]))
|
|
//sig = cryptoSignProper(blake.Sum(nil), sk)
|
|
//signed.headers.Set("X-Signature-Ed25519-BLAKE2B", sig)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (self *nntpArticle) BodyReader() io.Reader {
|
|
if self.Pubkey() == "" {
|
|
buff := new(bytes.Buffer)
|
|
self.WriteBody(buff, MaxMessageSize)
|
|
return buff
|
|
} else {
|
|
return self.signedPart.body
|
|
}
|
|
}
|
|
|
|
func (self *nntpArticle) WriteTo(wr io.Writer, limit int64) (err error) {
|
|
// write headers
|
|
var n int
|
|
hdrs := self.headers
|
|
for hdr, hdr_vals := range hdrs {
|
|
for _, hdr_val := range hdr_vals {
|
|
n, err = wr.Write([]byte(hdr))
|
|
limit -= int64(n)
|
|
n, err = wr.Write([]byte(": "))
|
|
limit -= int64(n)
|
|
n, err = wr.Write([]byte(hdr_val))
|
|
limit -= int64(n)
|
|
n, err = wr.Write([]byte{10})
|
|
limit -= int64(n)
|
|
if err != nil {
|
|
log.Println("error while writing headers", err)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// done headers
|
|
n, err = wr.Write([]byte{10})
|
|
limit -= int64(n)
|
|
if err != nil {
|
|
log.Println("error while writing body", err)
|
|
return
|
|
}
|
|
|
|
if limit > 0 {
|
|
err = self.WriteBody(wr, limit)
|
|
} else {
|
|
err = ErrOversizedMessage
|
|
}
|
|
return
|
|
}
|
|
|
|
func (self *nntpArticle) Pubkey() string {
|
|
return self.headers.Get("X-PubKey-Ed25519", self.headers.Get("X-Pubkey-Ed25519", ""))
|
|
}
|
|
|
|
func (self *nntpArticle) MessageID() (msgid string) {
|
|
for _, h := range []string{"Message-ID", "Message-Id"} {
|
|
mid := self.headers.Get(h, "")
|
|
if mid != "" {
|
|
msgid = string(mid)
|
|
return
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (self *nntpArticle) Pack() {
|
|
if len(self.attachments) > 0 {
|
|
if len(self.boundary) == 0 {
|
|
// we have no boundry, set it
|
|
self.boundary = randStr(24)
|
|
// set headers
|
|
self.headers.Set("Mime-Version", "1.0")
|
|
self.headers.Set("Content-Type", fmt.Sprintf("multipart/mixed; boundary=%s", self.boundary))
|
|
}
|
|
} else if self.signedPart == nil {
|
|
self.headers.Set("Content-Type", "text/plain; charset=utf-8")
|
|
}
|
|
}
|
|
|
|
func (self *nntpArticle) Reference() string {
|
|
return self.headers.Get("Reference", self.headers.Get("References", ""))
|
|
}
|
|
|
|
func (self *nntpArticle) Newsgroup() string {
|
|
return self.headers.Get("Newsgroups", "")
|
|
}
|
|
|
|
func (self *nntpArticle) Name() string {
|
|
const defname = "Anonymous"
|
|
|
|
from := strings.TrimSpace(self.headers.Get("From", ""))
|
|
if from == "" {
|
|
return defname
|
|
}
|
|
|
|
a, e := mail.ParseAddress(from)
|
|
var name string
|
|
if e != nil {
|
|
// try older method - some nodes generate non-compliant stuff
|
|
if i := strings.IndexByte(from, '<'); i > 1 {
|
|
name = from[:i]
|
|
} else {
|
|
return "[Invalid From header]"
|
|
}
|
|
} else {
|
|
name = a.Name
|
|
}
|
|
|
|
name = safeHeader(name)
|
|
if name == "" {
|
|
return defname
|
|
}
|
|
|
|
return name
|
|
}
|
|
|
|
func (self *nntpArticle) Addr() (addr string) {
|
|
addr = self.headers.Get("X-Encrypted-Ip", "")
|
|
if addr != "" {
|
|
return
|
|
}
|
|
|
|
addr = self.headers.Get("X-Encrypted-IP", "")
|
|
if addr != "" {
|
|
return
|
|
}
|
|
|
|
addr = self.headers.Get("X-I2P-DestHash", "")
|
|
if addr != "" {
|
|
if addr == "None" {
|
|
return ""
|
|
}
|
|
return
|
|
}
|
|
|
|
addr = self.headers.Get("X-I2p-Desthash", "")
|
|
return
|
|
}
|
|
|
|
func (self *nntpArticle) Email() string {
|
|
from := strings.TrimSpace(self.headers.Get("From", ""))
|
|
if from == "" {
|
|
return ""
|
|
}
|
|
a, e := mail.ParseAddress(from)
|
|
if e != nil {
|
|
return fmt.Sprintf("[Invalid From header: %v]", e)
|
|
}
|
|
return a.Address
|
|
}
|
|
|
|
func (self *nntpArticle) Subject() string {
|
|
return self.headers.Get("Subject", "")
|
|
}
|
|
|
|
func (self *nntpArticle) Posted() int64 {
|
|
posted := self.headers.Get("Date", "")
|
|
t, err := mail.ParseDate(posted)
|
|
if err == nil {
|
|
return t.Unix()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (self *nntpArticle) Message() string {
|
|
return strings.Trim(self.message, "\x00")
|
|
}
|
|
|
|
func (self *nntpArticle) Path() string {
|
|
return self.headers.Get("Path", "unspecified")
|
|
}
|
|
|
|
func (self *nntpArticle) Headers() ArticleHeaders {
|
|
return self.headers
|
|
}
|
|
|
|
func (self *nntpArticle) MIMEHeader() textproto.MIMEHeader {
|
|
h := make(textproto.MIMEHeader)
|
|
for k, v := range self.headers {
|
|
h[k] = v
|
|
}
|
|
return h
|
|
}
|
|
|
|
func (self *nntpArticle) AppendPath(part string) NNTPMessage {
|
|
if self.headers.Has("Path") {
|
|
self.headers.Set("Path", part+"!"+self.Path())
|
|
} else {
|
|
self.headers.Set("Path", part)
|
|
}
|
|
return self
|
|
}
|
|
func (self *nntpArticle) ContentType() string {
|
|
// assumes text/plain if unspecified
|
|
return self.headers.Get("Content-Type", "text/plain; charset=UTF-8")
|
|
}
|
|
|
|
func (self *nntpArticle) Sage() bool {
|
|
return self.headers.Get("X-Sage", "") == "1"
|
|
}
|
|
|
|
func (self *nntpArticle) OP() bool {
|
|
ref := self.Reference()
|
|
return ref == "" || ref == self.MessageID()
|
|
}
|
|
|
|
func (self *nntpArticle) Attachments() []NNTPAttachment {
|
|
return self.attachments
|
|
}
|
|
|
|
func (self *nntpArticle) Attach(att NNTPAttachment) {
|
|
self.attachments = append(self.attachments, att)
|
|
}
|
|
|
|
func (self *nntpArticle) WriteBody(wr io.Writer, limit int64) (err error) {
|
|
// this is a signed message, don't treat it special
|
|
if self.signedPart != nil {
|
|
_, err = wr.Write(self.signedPart.Bytes())
|
|
return
|
|
}
|
|
self.Pack()
|
|
content_type := self.ContentType()
|
|
_, params, err := mime.ParseMediaType(content_type)
|
|
if err != nil {
|
|
log.Println("failed to parse media type", err)
|
|
return err
|
|
}
|
|
|
|
boundary, ok := params["boundary"]
|
|
if ok {
|
|
nlw := NewLineWriter(wr, limit)
|
|
w := multipart.NewWriter(nlw)
|
|
|
|
err = w.SetBoundary(boundary)
|
|
if err == nil {
|
|
attachments := []NNTPAttachment{createPlaintextAttachment([]byte(self.message))}
|
|
attachments = append(attachments, self.attachments...)
|
|
for _, att := range attachments {
|
|
if att == nil {
|
|
continue
|
|
}
|
|
hdr := att.Header()
|
|
hdr.Set("Content-Transfer-Encoding", "base64")
|
|
part, err := w.CreatePart(hdr)
|
|
if err != nil {
|
|
log.Println("failed to create part?", err)
|
|
}
|
|
var buff [1024]byte
|
|
var b io.ReadCloser
|
|
b, err = att.OpenBody()
|
|
if err == nil {
|
|
enc := base64.NewEncoder(base64.StdEncoding, part)
|
|
_, err = io.CopyBuffer(enc, b, buff[:])
|
|
b.Close()
|
|
enc.Close()
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
part = nil
|
|
}
|
|
}
|
|
if err != nil {
|
|
log.Println("error writing part", err)
|
|
}
|
|
err = w.Close()
|
|
w = nil
|
|
} else {
|
|
nlw := NewLineWriter(wr, limit)
|
|
// write out message
|
|
_, err = io.WriteString(nlw, self.message)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// verify a signed message's body
|
|
// innerHandler must close reader when done
|
|
// returns error if one happens while verifying article
|
|
func verifyMessageSHA512(pk, sig string, body io.Reader, innerHandler func(ArticleHeaders, io.Reader)) (err error) {
|
|
log.Println("unwrapping signed message from", pk)
|
|
pk_bytes := unhex(pk)
|
|
sig_bytes := unhex(sig)
|
|
h := sha512.New()
|
|
pr, pw := io.Pipe()
|
|
// read header
|
|
// handle inner body
|
|
go func(hdr_reader *io.PipeReader) {
|
|
r := bufio.NewReader(hdr_reader)
|
|
msg, err := readMIMEHeader(r)
|
|
if err == nil {
|
|
innerHandler(ArticleHeaders(msg.Header), msg.Body)
|
|
}
|
|
hdr_reader.Close()
|
|
}(pr)
|
|
body = io.TeeReader(body, pw)
|
|
// copy body 128 bytes at a time
|
|
var buff [128]byte
|
|
_, err = io.CopyBuffer(h, body, buff[:])
|
|
if err == nil {
|
|
hash := h.Sum(nil)
|
|
log.Printf("hash=%s", hexify(hash))
|
|
log.Printf("sig=%s", hexify(sig_bytes))
|
|
if naclCryptoVerifyFucky(hash, sig_bytes, pk_bytes) {
|
|
log.Println("signature is valid :^)")
|
|
} else {
|
|
err = errors.New("invalid signature")
|
|
}
|
|
}
|
|
// flush pipe
|
|
pw.Close()
|
|
return
|
|
}
|
|
|
|
func verifyMessageBLAKE2B(pk, sig string, body io.Reader, innerHandler func(ArticleHeaders, io.Reader)) (err error) {
|
|
log.Println("unwrapping signed message from", pk)
|
|
pk_bytes := unhex(pk)
|
|
sig_bytes := unhex(sig)
|
|
h := blake2b.New512()
|
|
pr, pw := io.Pipe()
|
|
// read header
|
|
// handle inner body
|
|
go func(hdr_reader *io.PipeReader) {
|
|
r := bufio.NewReader(hdr_reader)
|
|
msg, err := readMIMEHeader(r)
|
|
if err == nil {
|
|
innerHandler(ArticleHeaders(msg.Header), msg.Body)
|
|
}
|
|
hdr_reader.Close()
|
|
}(pr)
|
|
body = io.TeeReader(body, pw)
|
|
// copy body 128 bytes at a time
|
|
var buff [128]byte
|
|
_, err = io.CopyBuffer(h, body, buff[:])
|
|
if err == nil {
|
|
hash := h.Sum(nil)
|
|
log.Printf("hash=%s", hexify(hash))
|
|
log.Printf("sig=%s", hexify(sig_bytes))
|
|
if naclCryptoVerifyFucky(hash, sig_bytes, pk_bytes) {
|
|
log.Println("signature is valid :^)")
|
|
} else {
|
|
err = errors.New("invalid signature")
|
|
}
|
|
}
|
|
// flush pipe
|
|
pw.Close()
|
|
return
|
|
}
|