package omemo import ( "crypto/rand" "encoding/base64" "encoding/xml" "math/big" ) // Minimal XEP-0420 Stanza Content Encryption envelope, used only as the // plaintext OMEMO 2 (modern) content encryption operates on - just enough // to carry a plain message body, per the spec'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). type sceEnvelope struct { XMLName xml.Name `xml:"urn:xmpp:sce:1 envelope"` Content sceContent `xml:"content"` RPad string `xml:"rpad"` From *sceJIDAttr `xml:"from"` } type sceContent struct { Body sceBody `xml:"body"` } type sceBody struct { XMLName xml.Name `xml:"jabber:client body"` Text string `xml:",chardata"` } type sceJIDAttr struct { JID string `xml:"jid,attr"` } // sceEncode wraps plaintext in an SCE envelope and serializes it - the // "plaintext" that modern OMEMO's outer content encryption actually // operates on. fromJID becomes the binding (empty to omit it). func sceEncode(plaintext []byte, fromJID string) ([]byte, error) { rpad, err := randomPadding() if err != nil { return nil, err } env := sceEnvelope{ Content: sceContent{Body: sceBody{Text: string(plaintext)}}, RPad: rpad, } if fromJID != "" { env.From = &sceJIDAttr{JID: fromJID} } return xml.Marshal(env) } // sceDecode reverses sceEncode, extracting the plain body text. func sceDecode(data []byte) ([]byte, error) { var env sceEnvelope 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. 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 }