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 and SHOULD contain (MUST contain // 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: // // // // ... // // // ... // // // V1 (Omemo2, current) renamed content->envelope and payload->content // (XEP-0420 0.4.0+, urn:xmpp:sce:1): // // // // ... // // // ... // 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- 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 }