go-sendxmpp/ox.go

386 lines
10 KiB
Go

// Copyright 2022 Martin Dosch.
// Use of this source code is governed by the BSD-2-clause
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"encoding/base64"
"encoding/xml"
"errors"
"log"
"os"
"runtime"
"strings"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto" // MIT License
"github.com/beevik/etree" // BSD-2-clause
"github.com/mattn/go-xmpp" // BSD-3-Clause
)
func oxGetPrivKeyLoc(jid string) (string, error) {
var err error
var homeDir, dataDir string
switch {
case os.Getenv("$XDG_DATA_HOME") != "":
dataDir = os.Getenv("$XDG_DATA_HOME")
case os.Getenv("$XDG_HOME") != "":
homeDir = os.Getenv("$XDG_HOME")
dataDir = homeDir + "/.local/share"
case os.Getenv("$HOME") != "":
homeDir = os.Getenv("$HOME")
dataDir = homeDir + "/.local/share"
default:
homeDir, err = os.UserHomeDir()
if err != nil {
return "error", err
}
if homeDir == "" {
return "error", err
}
dataDir = homeDir + "/.local/share"
}
dataDir = dataDir + "/go-sendxmpp/oxprivkeys/"
if _, err = os.Stat(dataDir); os.IsNotExist(err) {
err = os.MkdirAll(dataDir, 0700)
if err != nil {
return "error", err
}
}
dataFile := dataDir + base64.StdEncoding.EncodeToString([]byte(jid))
return dataFile, nil
}
func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) {
dataFile, err := oxGetPrivKeyLoc(jid)
if err != nil {
log.Fatal(err)
}
file, err := os.OpenFile(dataFile, os.O_RDWR, 0600)
if err != nil {
log.Fatal("Error: can't open private key file:", err)
}
defer file.Close()
keyBuffer := new(bytes.Buffer)
_, err = keyBuffer.ReadFrom(file)
if err != nil {
return nil, err
}
keyString := keyBuffer.String()
decodedPrivKey, err := base64.StdEncoding.DecodeString(keyString)
if err != nil {
return nil, err
}
key, err := crypto.NewKey(decodedPrivKey)
if err != nil {
return nil, err
}
if passphrase != "" {
key, err = key.Unlock([]byte(passphrase))
if err != nil {
log.Fatal("Couldn't unlock private key.")
}
}
isLocked, err := key.IsLocked()
if err != nil {
return nil, err
}
if isLocked {
log.Fatal("Private key is locked.")
}
if key.IsExpired() {
return nil, errors.New("Key is expired: " + key.GetFingerprint())
}
return key, nil
}
func oxStorePrivKey(jid string, privKey string) error {
dataFile, err := oxGetPrivKeyLoc(jid)
if err != nil {
log.Fatal(err)
}
var file *os.File
if _, err := os.Stat(dataFile); os.IsNotExist(err) {
file, err = os.Create(dataFile)
if err != nil {
return err
}
} else {
file, err = os.OpenFile(dataFile, os.O_RDWR, 0600)
if err != nil {
return err
}
}
defer file.Close()
if runtime.GOOS != "windows" {
_ = file.Chmod(os.FileMode(0600))
} else {
_ = file.Chmod(os.FileMode(0200))
}
_, err = file.Write([]byte(privKey))
if err != nil {
return err
}
return nil
}
func oxGenPrivKey(jid string, client *xmpp.Client, passphrase string) error {
xmppUri := "xmpp:" + jid
key, err := crypto.GenerateKey(xmppUri, xmppUri, "x25519", 0)
if err != nil {
return err
}
decodedPubKey, err := key.GetPublicKey()
if err != nil {
return err
}
pubKeyBase64 := base64.StdEncoding.EncodeToString(decodedPubKey)
if passphrase != "" {
key, err = key.Lock([]byte(passphrase))
if err != nil {
return err
}
}
keySerialized, err := key.Serialize()
if err != nil {
return err
}
err = oxStorePrivKey(jid,
base64.StdEncoding.EncodeToString(keySerialized))
if err != nil {
log.Fatal(err)
}
keyCreated := time.Now().UTC().Format("2006-01-02T15:04:05Z")
pubKey, err := crypto.NewKey(decodedPubKey)
if err != nil {
return err
}
fingerprint := strings.ToUpper(pubKey.GetFingerprint())
root := etree.NewDocument()
pubsub := root.CreateElement("pubsub")
pubsub.CreateAttr("xmlns", nsPubsub)
publish := pubsub.CreateElement("publish")
publish.CreateAttr("node", nsOxPubKeys+":"+fingerprint)
item := publish.CreateElement("item")
item.CreateAttr("id", keyCreated)
pubkey := item.CreateElement("pubkey")
pubkey.CreateAttr("xmlns", nsOx)
data := pubkey.CreateElement("data")
data.CreateText(pubKeyBase64)
publishoptions := pubsub.CreateElement("publish-options")
x := publishoptions.CreateElement("x")
x.CreateAttr("xmlns", nsJabberData)
x.CreateAttr("type", "submit")
field := x.CreateElement("field")
field.CreateAttr("var", "FORM_TYPE")
field.CreateAttr("type", "hidden")
value := field.CreateElement("value")
value.CreateText(pubsubPubOptions)
field = x.CreateElement("field")
field.CreateAttr("var", "pubsub#access_model")
value = field.CreateElement("value")
value.CreateText("open")
xmlstring, err := root.WriteToString()
if err != nil {
return err
}
iqReply, err := sendIQ(client, jid, "set", xmlstring)
if err != nil {
return err
}
if iqReply.Type != "result" {
return errors.New("Error while publishing public key.")
}
ownPubKeyFromPubsub, err := oxRecvPublicKey(client, jid, fingerprint)
if err != nil {
return errors.New("Couldn't successfully verify public key upload.")
}
ownPubKeyFromPubsubSerialized, err := ownPubKeyFromPubsub.Serialize()
if err != nil {
return errors.New("Couldn't successfully verify public key upload.")
}
if pubKeyBase64 != base64.StdEncoding.EncodeToString(ownPubKeyFromPubsubSerialized) {
return errors.New("Couldn't successfully verify public key upload.")
}
root = etree.NewDocument()
pubsub = root.CreateElement("pubsub")
pubsub.CreateAttr("xmlns", nsPubsub)
publish = pubsub.CreateElement("publish")
publish.CreateAttr("node", nsOxPubKeys)
item = publish.CreateElement("item")
pubkeyslist := item.CreateElement("public-keys-list")
pubkeyslist.CreateAttr("xmlns", nsOx)
pubkeymeta := pubkeyslist.CreateElement("pubkey-metadata")
pubkeymeta.CreateAttr("v4-fingerprint", fingerprint)
pubkeymeta.CreateAttr("date", keyCreated)
publishoptions = pubsub.CreateElement("publish-options")
x = publishoptions.CreateElement("x")
x.CreateAttr("xmlns", nsJabberData)
x.CreateAttr("type", "submit")
field = x.CreateElement("field")
field.CreateAttr("var", "FORM_TYPE")
field.CreateAttr("type", "hidden")
value = field.CreateElement("value")
value.CreateText(pubsubPubOptions)
field = x.CreateElement("field")
field.CreateAttr("var", "pubsub#access_model")
value = field.CreateElement("value")
value.CreateText("open")
xmlstring, err = root.WriteToString()
if err != nil {
return err
}
iqReply, err = sendIQ(client, jid, "set", xmlstring)
if err != nil {
return err
}
if iqReply.Type != "result" {
return errors.New("Couldn't publish public key list.")
}
return nil
}
func oxRecvPublicKey(client *xmpp.Client, recipient string,
fingerprint string) (*crypto.Key, error) {
var oxPublicKeyRequest IQPubsubRequest
var oxPublicKeyXML OxPublicKey
oxPublicKeyRequest.Xmlns = nsPubsub
oxPublicKeyRequest.Items.Node = nsOxPubKeys + ":" + fingerprint
oxPublicKeyRequest.Items.MaxItems = "1"
opk, err := xml.Marshal(oxPublicKeyRequest)
if err != nil {
return nil, err
}
oxPublicKey, err := sendIQ(client, recipient, "get", string(opk))
if err != nil {
return nil, err
}
if oxPublicKey.Type != "result" {
return nil, errors.New("Error while requesting public key for " +
recipient)
}
err = xml.Unmarshal(oxPublicKey.Query, &oxPublicKeyXML)
if err != nil {
return nil, err
}
decodedPubKey, err := base64.StdEncoding.DecodeString(oxPublicKeyXML.Items.Item.Pubkey.Data)
if err != nil {
return nil, err
}
key, err := crypto.NewKey(decodedPubKey)
if err != nil {
return nil, err
}
if key.IsExpired() {
return nil, errors.New("Key is expired: " + fingerprint)
}
return key, nil
}
func oxGetPublicKey(client *xmpp.Client, recipient string) (*crypto.Key, error) {
var oxPublicKeyListRequest IQPubsubRequest
var oxPublicKeyListXML OxPublicKeysList
oxPublicKeyListRequest.Xmlns = nsPubsub
oxPublicKeyListRequest.Items.Node = nsOxPubKeys
oxPublicKeyListRequest.Items.MaxItems = "100"
opkl, err := xml.Marshal(oxPublicKeyListRequest)
if err != nil {
log.Fatal(err)
}
oxPublicKeyList, err := sendIQ(client, recipient, "get", string(opkl))
if err != nil {
log.Fatal(err)
}
if oxPublicKeyList.Type != "result" {
return nil, errors.New("Error while requesting public openpgp keys for " +
recipient)
}
err = xml.Unmarshal(oxPublicKeyList.Query, &oxPublicKeyListXML)
if err != nil {
return nil, err
}
fingerprint := "none"
newestKey, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
if err != nil {
return nil, err
}
for _, r := range oxPublicKeyListXML.Items.Item.PublicKeysList.PubkeyMetadata {
keyDate, err := time.Parse(time.RFC3339, r.Date)
if err != nil {
return nil, err
}
if keyDate.After(newestKey) {
newestKey = keyDate
fingerprint = r.V4Fingerprint
}
}
if fingerprint == "none" {
return nil, errors.New("server didn't provide public key fingerprints for " + recipient)
}
key, err := oxRecvPublicKey(client, recipient, fingerprint)
if err != nil {
return nil, errors.New("Couldn't fetch public key.")
}
return key, nil
}
func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string,
recipientKey *crypto.Key, message string) (string, error) {
var oxCryptMessage OxCryptElement
var oxMessage OxMessageElement
keyRing, err := crypto.NewKeyRing(recipientKey)
if err != nil {
return "error", err
}
privKeyRing, err := crypto.NewKeyRing(oxPrivKey)
if err != nil {
return "error", err
}
ownJid := strings.Split(client.JID(), "/")[0]
if recipient != ownJid {
opk, err := oxPrivKey.GetPublicKey()
if err == nil {
ownKey, _ := crypto.NewKey(opk)
_ = keyRing.AddKey(ownKey)
}
}
oxCryptMessage.Xmlns = nsOx
oxCryptMessage.To.Jid = recipient
oxCryptMessage.Time.Stamp = time.Now().UTC().Format("2006-01-02T15:04:05Z")
oxCryptMessage.Rpad = getRpad()
oxCryptMessage.Payload.Body.Xmlns = nsJabberClient
oxCryptMessage.Payload.Body.Text = message
ocm, err := xml.Marshal(oxCryptMessage)
if err != nil {
return "error", err
}
plainMessage := crypto.NewPlainMessage([]byte(ocm))
pgpMessage, err := keyRing.Encrypt(plainMessage, privKeyRing)
if err != nil {
return "error", err
}
oxMessage.To = recipient
oxMessage.Id = getID()
oxMessage.From = client.JID()
oxMessage.Openpgp.Text = base64.StdEncoding.EncodeToString(pgpMessage.Data)
oxMessage.Openpgp.Xmlns = nsOx
oxMessage.Encryption.Xmlns = nsEme
oxMessage.Encryption.Namespace = nsOx
oxMessage.Body = oxAltBody
om, err := xml.Marshal(oxMessage)
if err != nil {
return "error", err
}
return string(om), nil
}