telegabber/e2ee/omemo/sce.go
2026-07-28 20:32:56 -04:00

133 lines
3.2 KiB
Go

package omemo
import (
"crypto/rand"
"encoding/base64"
"encoding/xml"
"math/big"
)
// Two dialects of XEP-0420 Stanza Content Encryption, matching what each
// OMEMO version actually wraps its plaintext in - just enough of each to
// carry a plain message body, per each dialect's requirement that the
// envelope MUST contain <rpad/> and SHOULD contain <from/> (MUST contain
// <to/> for MUC, which this gateway doesn't do OMEMO for - see the plan's
// scope note on MUC).
//
// V0 (Omemo1, XEP-0384 0.4.0-0.7.x) is the OLD XEP-0420 shape, confirmed
// by reading the archived 0.3.2 spec directly:
//
// <content xmlns='urn:xmpp:sce:0'>
// <payload>
// <body xmlns='jabber:client'>...</body>
// </payload>
// <from jid='...'/>
// <rpad>...</rpad>
// </content>
//
// V1 (Omemo2, current) renamed content->envelope and payload->content
// (XEP-0420 0.4.0+, urn:xmpp:sce:1):
//
// <envelope xmlns='urn:xmpp:sce:1'>
// <content>
// <body xmlns='jabber:client'>...</body>
// </content>
// <from jid='...'/>
// <rpad>...</rpad>
// </envelope>
type sceBody struct {
XMLName xml.Name `xml:"jabber:client body"`
Text string `xml:",chardata"`
}
type sceJIDAttr struct {
JID string `xml:"jid,attr"`
}
// --- V0 (Omemo1) ---
type sceContentV0 struct {
XMLName xml.Name `xml:"urn:xmpp:sce:0 content"`
Payload scePayloadV0 `xml:"payload"`
From *sceJIDAttr `xml:"from"`
RPad string `xml:"rpad"`
}
type scePayloadV0 struct {
Body sceBody `xml:"body"`
}
func sceEncodeV0(plaintext []byte, fromJID string) ([]byte, error) {
rpad, err := randomPadding()
if err != nil {
return nil, err
}
env := sceContentV0{
Payload: scePayloadV0{Body: sceBody{Text: string(plaintext)}},
RPad: rpad,
}
if fromJID != "" {
env.From = &sceJIDAttr{JID: fromJID}
}
return xml.Marshal(env)
}
func sceDecodeV0(data []byte) ([]byte, error) {
var env sceContentV0
if err := xml.Unmarshal(data, &env); err != nil {
return nil, err
}
return []byte(env.Payload.Body.Text), nil
}
// --- V1 (Omemo2) ---
type sceEnvelopeV1 struct {
XMLName xml.Name `xml:"urn:xmpp:sce:1 envelope"`
Content sceContentV1 `xml:"content"`
From *sceJIDAttr `xml:"from"`
RPad string `xml:"rpad"`
}
type sceContentV1 struct {
Body sceBody `xml:"body"`
}
func sceEncodeV1(plaintext []byte, fromJID string) ([]byte, error) {
rpad, err := randomPadding()
if err != nil {
return nil, err
}
env := sceEnvelopeV1{
Content: sceContentV1{Body: sceBody{Text: string(plaintext)}},
RPad: rpad,
}
if fromJID != "" {
env.From = &sceJIDAttr{JID: fromJID}
}
return xml.Marshal(env)
}
func sceDecodeV1(data []byte) ([]byte, error) {
var env sceEnvelopeV1
if err := xml.Unmarshal(data, &env); err != nil {
return nil, err
}
return []byte(env.Content.Body.Text), nil
}
// randomPadding returns base64-encoded random padding of a random length
// (1-63 bytes before encoding) to mask plaintext length, per XEP-0420's
// MUST-contain-<rpad/> requirement (both dialects).
func randomPadding() (string, error) {
n, err := rand.Int(rand.Reader, big.NewInt(63))
if err != nil {
return "", err
}
buf := make([]byte, n.Int64()+1)
if _, err := rand.Read(buf); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(buf), nil
}