telegabber/e2ee/omemo/bundle.go
2026-07-28 23:14:32 -04:00

205 lines
6.6 KiB
Go

package omemo
import (
"encoding/base64"
"encoding/xml"
"fmt"
"dev.narayana.im/narayana/telegabber/e2ee/omemo/libsignal"
)
// Bundle document parsing/encoding for all three OMEMO versions. As with
// devicelist.go, Omemo1 and Omemo2 share an identical element shape (spk/
// spks/ik/prekeys/pk) - only the namespace differs, per the same
// changelog-silence reasoning - so one xepBundle struct serves both,
// namespace-agnostic on Unmarshal, explicit on Marshal.
//
// Omemo0's element names (signedPreKeyPublic/signedPreKeySignature/
// identityKey/prekeys/preKeyPublic) do NOT carry over to Omemo1/Omemo2 -
// confirmed against the XEP-0384 XML schema directly, not assumed.
//
// None of the three bundle formats carry a registration id (unlike
// vanilla Signal protocol's own prekey-bundle API, which does) - OMEMO
// simply doesn't use it (XEP-0384's OMEMOKeyExchange has no such field
// either). parseBundle leaves RegistrationID as 0; callers pass that
// straight through to libsignal.RemoteBundle, which is the standard,
// harmless practice for this field in OMEMO implementations - device_id
// already does what registration_id would otherwise disambiguate.
type omemo0Bundle struct {
XMLName xml.Name `xml:"eu.siacs.conversations.axolotl bundle"`
SignedPreKeyPublic omemo0IDText `xml:"signedPreKeyPublic"`
SignedPreKeySignature string `xml:"signedPreKeySignature"`
IdentityKey string `xml:"identityKey"`
PreKeys omemo0PreKeysWrap `xml:"prekeys"`
}
type omemo0IDText struct {
ID uint32 `xml:"signedPreKeyId,attr"`
Text string `xml:",chardata"`
}
type omemo0PreKeysWrap struct {
List []omemo0PreKey `xml:"preKeyPublic"`
}
type omemo0PreKey struct {
ID uint32 `xml:"preKeyId,attr"`
Text string `xml:",chardata"`
}
type xepBundle struct {
XMLName xml.Name `xml:"bundle"`
SPK xepIDText `xml:"spk"`
SPKS string `xml:"spks"`
IK string `xml:"ik"`
PreKeys xepPreKeysWrap `xml:"prekeys"`
}
type xepIDText struct {
ID uint32 `xml:"id,attr"`
Text string `xml:",chardata"`
}
type xepPreKeysWrap struct {
List []xepIDText `xml:"pk"`
}
// parsedBundle is the common, namespace-agnostic result of parsing any of
// the three bundle formats, in the raw (still base64-decoded) bytes
// libsignal expects.
type parsedBundle struct {
Version Version // which of the three OMEMO versions this bundle was actually fetched/published under - determines both which libsignal protocol version (V3 vs V4) a session built from it must use, AND (Omemo1 vs Omemo2 specifically) what to record as the chat's negotiated version, since that's a property of the bundle actually fetched, not of this backend's own prior state (see IngestRemoteBundle)
SignedPreKeyID uint32
SignedPreKeyPublic []byte
SignedPreKeySignature []byte
IdentityKeyPublic []byte
PreKeys []parsedPreKey
}
type parsedPreKey struct {
ID uint32
Public []byte
}
func parseBundle(raw []byte) (*parsedBundle, error) {
var omemo0 omemo0Bundle
if err := xml.Unmarshal(raw, &omemo0); err == nil {
return decodeOmemo0Bundle(omemo0)
}
var xep xepBundle
if err := xml.Unmarshal(raw, &xep); err != nil {
return nil, err
}
return decodeXepBundle(xep)
}
func decodeOmemo0Bundle(b omemo0Bundle) (*parsedBundle, error) {
spk, err := b64Decode(b.SignedPreKeyPublic.Text)
if err != nil {
return nil, fmt.Errorf("omemo: omemo0 bundle signedPreKeyPublic: %w", err)
}
sig, err := b64Decode(b.SignedPreKeySignature)
if err != nil {
return nil, fmt.Errorf("omemo: omemo0 bundle signedPreKeySignature: %w", err)
}
ik, err := b64Decode(b.IdentityKey)
if err != nil {
return nil, fmt.Errorf("omemo: omemo0 bundle identityKey: %w", err)
}
result := &parsedBundle{
Version: Omemo0,
SignedPreKeyID: b.SignedPreKeyPublic.ID,
SignedPreKeyPublic: spk,
SignedPreKeySignature: sig,
IdentityKeyPublic: ik,
}
for _, pk := range b.PreKeys.List {
data, err := b64Decode(pk.Text)
if err != nil {
return nil, fmt.Errorf("omemo: omemo0 bundle preKeyPublic %d: %w", pk.ID, err)
}
result.PreKeys = append(result.PreKeys, parsedPreKey{ID: pk.ID, Public: data})
}
return result, nil
}
func decodeXepBundle(b xepBundle) (*parsedBundle, error) {
spk, err := b64Decode(b.SPK.Text)
if err != nil {
return nil, fmt.Errorf("omemo: bundle spk: %w", err)
}
sig, err := b64Decode(b.SPKS)
if err != nil {
return nil, fmt.Errorf("omemo: bundle spks: %w", err)
}
ik, err := b64Decode(b.IK)
if err != nil {
return nil, fmt.Errorf("omemo: bundle ik: %w", err)
}
version := Omemo2
if b.XMLName.Space == omemo1NS {
version = Omemo1
}
result := &parsedBundle{
Version: version,
SignedPreKeyID: b.SPK.ID,
SignedPreKeyPublic: spk,
SignedPreKeySignature: sig,
IdentityKeyPublic: ik,
}
for _, pk := range b.PreKeys.List {
data, err := b64Decode(pk.Text)
if err != nil {
return nil, fmt.Errorf("omemo: bundle pk %d: %w", pk.ID, err)
}
result.PreKeys = append(result.PreKeys, parsedPreKey{ID: pk.ID, Public: data})
}
return result, nil
}
// encodeBundle produces a bundle document for one of this gateway's own
// published devices, in the namespace matching version. Public keys are
// expected in the form DecodeSignedPreKey/DecodePreKey/
// DecodeIdentityPublicKey return - i.e. already picking the signature form
// (Omemo0 vs Omemo1/Omemo2) matching version, per the rule documented on
// libsignal.SignedPreKeyInfo.
func encodeBundle(version Version, identityKeyPublic []byte, spkID uint32, spkPublic, spkSignature []byte, preKeys []libsignal.PreKeyInfo) ([]byte, error) {
if version == Omemo0 {
b := omemo0Bundle{
SignedPreKeyPublic: omemo0IDText{ID: spkID, Text: b64Encode(spkPublic)},
SignedPreKeySignature: b64Encode(spkSignature),
IdentityKey: b64Encode(identityKeyPublic),
}
for _, pk := range preKeys {
b.PreKeys.List = append(b.PreKeys.List, omemo0PreKey{ID: pk.ID, Text: b64Encode(pk.PublicKey)})
}
return xml.Marshal(b)
}
ns := omemo1NS
if version == Omemo2 {
ns = omemo2NS
}
b := xepBundle{
XMLName: xml.Name{Space: ns, Local: "bundle"},
SPK: xepIDText{ID: spkID, Text: b64Encode(spkPublic)},
SPKS: b64Encode(spkSignature),
IK: b64Encode(identityKeyPublic),
}
for _, pk := range preKeys {
b.PreKeys.List = append(b.PreKeys.List, xepIDText{ID: pk.ID, Text: b64Encode(pk.PublicKey)})
}
return xml.Marshal(b)
}
func b64Encode(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
func b64Decode(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}