//
// message.go
//
package srnd
import (
"bufio"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"github.com/dchest/blake2b"
"io"
"log"
"mime"
"mime/multipart"
"strings"
"time"
)
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
// all headers
Headers() ArticleHeaders
// 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()
}
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", fmt.Sprintf("%s <%s>", 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; charset=UTF-8")
} else {
v := h[k][0]
signed.headers.Set(k, v)
}
}
sha := sha512.New()
blake := blake2b.New256()
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) 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 {
// write body
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", "Messageid", "MessageID", "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 {
from := self.headers.Get("From", "anonymous ")
idx := strings.Index(from, "<")
if idx > 1 {
return from[:idx]
}
return "[Invalid From header]"
}
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 := self.headers.Get("From", "anonymous ")
idx := strings.Index(from, "<")
if idx > 2 {
return from[:idx-2]
}
return "[Invalid From header]"
}
func (self *nntpArticle) Subject() string {
return self.headers.Get("Subject", "")
}
func (self *nntpArticle) Posted() int64 {
posted := self.headers.Get("Date", "")
t, err := time.Parse(time.RFC1123Z, 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) 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
var n int
if self.signedPart != nil {
n, err = wr.Write(self.signedPart.Bytes())
limit -= int64(n)
if limit <= 0 {
err = ErrOversizedMessage
}
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.Add("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
if nlw.Left <= 0 {
err = ErrOversizedMessage
}
} 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.LimitedReader, innerHandler func(map[string][]string, 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(msg.Header, msg.Body)
}
hdr_reader.Close()
}(pr)
body = &io.LimitedReader{
R: io.TeeReader(body, pw),
N: body.N,
}
// 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.LimitedReader, innerHandler func(map[string][]string, io.Reader)) (err error) {
log.Println("unwrapping signed message from", pk)
pk_bytes := unhex(pk)
sig_bytes := unhex(sig)
h := blake2b.New256()
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(msg.Header, msg.Body)
}
hdr_reader.Close()
}(pr)
body = &io.LimitedReader{
R: io.TeeReader(body, pw),
N: body.N,
}
// 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
}