mirror of
https://salsa.debian.org/mdosch/go-sendxmpp
synced 2024-11-18 21:25:31 +00:00
385 lines
10 KiB
Go
385 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("Err 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
|
||
|
}
|