go-sendxmpp/ox.go
2022-04-24 00:33:17 +02:00

542 lines
15 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 (
"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 oxImportPrivKey(jid string, privKeyLocation string, client *xmpp.Client) error {
xmppUri := "xmpp:" + jid
buffer, err := readFile(privKeyLocation)
if err != nil {
return err
}
key, err := crypto.NewKey(buffer.Bytes())
if err != nil {
key, err = crypto.NewKeyFromArmored(buffer.String())
if err != nil {
return err
}
}
entity := key.GetEntity()
if entity.Identities[xmppUri] == nil {
return errors.New("Key identity is not " + xmppUri)
}
pk, err := key.GetPublicKey()
if err != nil {
return err
}
pubKey, err := crypto.NewKey(pk)
if err != nil {
return err
}
fingerprint := strings.ToUpper(pubKey.GetFingerprint())
_, err = oxRecvPublicKeys(client, jid, fingerprint)
if err != nil {
return errors.New("Key not found in pubsub: " + fingerprint)
}
location, err := oxGetPrivKeyLoc(jid)
if err != nil {
return err
}
keySerialized, err := key.Serialize()
if err != nil {
return err
}
err = oxStoreKey(location,
base64.StdEncoding.EncodeToString(keySerialized))
if err != nil {
log.Fatal(err)
}
return nil
}
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 oxGetPubKeyLoc(fingerprint 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/oxpubkeys/"
if _, err = os.Stat(dataDir); os.IsNotExist(err) {
err = os.MkdirAll(dataDir, 0700)
if err != nil {
return "error", err
}
}
dataFile := dataDir + fingerprint
return dataFile, nil
}
func oxGetPrivKey(jid string, passphrase string) (*crypto.Key, error) {
dataFile, err := oxGetPrivKeyLoc(jid)
if err != nil {
log.Fatal(err)
}
keyBuffer, err := readFile(dataFile)
if err != nil {
log.Fatal(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 oxStoreKey(location string, key string) error {
var file *os.File
if _, err := os.Stat(location); os.IsNotExist(err) {
file, err = os.Create(location)
if err != nil {
return err
}
} else {
file, err = os.OpenFile(location, os.O_RDWR, os.FileMode(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(key))
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
}
location, err := oxGetPrivKeyLoc(jid)
if err != nil {
return err
}
err = oxStoreKey(location,
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.")
}
ownPubKeyRingFromPubsub, err := oxRecvPublicKeys(client, jid, fingerprint)
if err != nil {
return errors.New("Couldn't successfully verify public key upload.")
}
ownPubKeyFromPubsub := ownPubKeyRingFromPubsub.GetKeys()[0]
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 oxRecvPublicKeys(client *xmpp.Client, recipient string,
fingerprint string) (*crypto.KeyRing, error) {
opkr := etree.NewDocument()
opkrPs := opkr.CreateElement("pubsub")
opkrPs.CreateAttr("xmlns", nsPubsub)
opkrPsItems := opkrPs.CreateElement("items")
opkrPsItems.CreateAttr("node", nsOxPubKeys+":"+fingerprint)
opkrPsItems.CreateAttr("max_items", "1")
opkrString, err := opkr.WriteToString()
if err != nil {
return nil, err
}
oxPublicKey, err := sendIQ(client, recipient, "get", opkrString)
if err != nil {
return nil, err
}
if oxPublicKey.Type != "result" {
return nil, errors.New("Error while requesting public key for " +
recipient)
}
oxPublicKeyXML := etree.NewDocument()
err = oxPublicKeyXML.ReadFromBytes(oxPublicKey.Query)
if err != nil {
return nil, err
}
keyring, err := crypto.NewKeyRing(nil)
if err != nil {
return nil, err
}
oxPublicKeyXMLPubsub := oxPublicKeyXML.SelectElement("pubsub")
oxPublicKeyXMLItems := oxPublicKeyXMLPubsub.SelectElement("items")
oxPublicKeyXMLItem := oxPublicKeyXMLItems.SelectElement("item")
oxPublicKeyXMLPubkeys := oxPublicKeyXMLItem.SelectElements("pubkey")
for _, r := range oxPublicKeyXMLPubkeys {
data := r.SelectElement("data")
decodedPubKey, err := base64.StdEncoding.DecodeString(data.Text())
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)
}
err = keyring.AddKey(key)
if err != nil {
return nil, err
}
}
return keyring, nil
}
func oxGetPublicKeyRing(client *xmpp.Client, recipient string) (*crypto.KeyRing, error) {
var oxPublicKeyListXML OxPublicKeysList
publicKeyRing, err := crypto.NewKeyRing(nil)
if err != nil {
return nil, err
}
oxPubKeyListReq := etree.NewDocument()
oxPubKeyListReqPs := oxPubKeyListReq.CreateElement("pubsub")
oxPubKeyListReqPs.CreateAttr("xmlns", nsPubsub)
oxPubKeyListReqPsItems := oxPubKeyListReqPs.CreateElement("items")
oxPubKeyListReqPsItems.CreateAttr("node", nsOxPubKeys)
oxPubKeyListReqPsItems.CreateAttr("max_items", "1")
opkl, err := oxPubKeyListReq.WriteToString()
if err != nil {
log.Fatal(err)
}
oxPublicKeyList, err := sendIQ(client, recipient, "get", 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
}
pubKeyRingId := "none"
newestKey, err := time.Parse(time.RFC3339, "1900-01-01T00:00:00Z")
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
pubKeyRingId = r.V4Fingerprint
}
}
if pubKeyRingId == "none" {
return nil, errors.New("server didn't provide public key fingerprints for " + recipient)
}
pubKeyRingLocation, err := oxGetPubKeyLoc(pubKeyRingId)
if err != nil {
return nil, err
}
pubKeyReadXML := etree.NewDocument()
err = pubKeyReadXML.ReadFromFile(pubKeyRingLocation)
if err == nil {
date := pubKeyReadXML.SelectElement("date")
if date != nil {
savedKeysDate, err := time.Parse(time.RFC3339, date.Text())
if err != nil {
return nil, err
}
if !savedKeysDate.Before(newestKey) {
pubKeys := pubKeyReadXML.SelectElements("pubkey")
if pubKeys == nil {
return nil, errors.New("Couldn't read public keys from cache.")
}
for _, r := range pubKeys {
keyByte, err := base64.StdEncoding.DecodeString(r.Text())
if err != nil {
return nil, err
}
key, err := crypto.NewKey(keyByte)
if err != nil {
return nil, err
}
if !key.IsExpired() {
err = publicKeyRing.AddKey(key)
if err != nil {
return nil, err
}
}
}
if publicKeyRing.CanEncrypt() {
return publicKeyRing, nil
}
}
}
}
pubKeyRing, err := oxRecvPublicKeys(client, recipient, pubKeyRingId)
if err != nil {
return nil, err
}
pubKeySaveXML := etree.NewDocument()
date := pubKeySaveXML.CreateElement("date")
date.SetText(newestKey.Format(time.RFC3339))
for _, key := range pubKeyRing.GetKeys() {
keySerialized, err := key.Serialize()
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
saveKey := pubKeySaveXML.CreateElement("pubkey")
saveKey.SetText(base64.StdEncoding.EncodeToString(keySerialized))
}
err = pubKeySaveXML.WriteToFile(pubKeyRingLocation)
if err != nil {
return nil, err
}
return pubKeyRing, nil
}
func oxEncrypt(client *xmpp.Client, oxPrivKey *crypto.Key, recipient string,
keyRing *crypto.KeyRing, message string) (string, error) {
var oxMessage OxMessageElement
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 := etree.NewDocument()
oxCryptMessageSc := oxCryptMessage.CreateElement("signcrypt")
oxCryptMessageSc.CreateAttr("xmlns", nsOx)
oxCryptMessageScTo := oxCryptMessageSc.CreateElement("to")
oxCryptMessageScTo.CreateAttr("jid", recipient)
oxCryptMessageScTime := oxCryptMessageSc.CreateElement("time")
oxCryptMessageScTime.CreateAttr("stamp", time.Now().UTC().Format("2006-01-02T15:04:05Z"))
oxCryptMessageScRpad := oxCryptMessageSc.CreateElement("rpad")
oxCryptMessageScRpad.CreateText(getRpad())
oxCryptMessageScPayload := oxCryptMessageSc.CreateElement("payload")
oxCryptMessageScPayloadBody := oxCryptMessageScPayload.CreateElement("body")
oxCryptMessageScPayloadBody.CreateAttr("xmlns", nsJabberClient)
oxCryptMessageScPayloadBody.CreateText(message)
ocm, err := oxCryptMessage.WriteToString()
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.Store.Xmlns = nsHints
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
}