mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
518 lines
18 KiB
Go
518 lines
18 KiB
Go
package omemo
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"dev.narayana.im/narayana/telegabber/e2ee"
|
|
"dev.narayana.im/narayana/telegabber/e2ee/omemo/libsignal"
|
|
"dev.narayana.im/narayana/telegabber/e2ee/store/badgerstore"
|
|
)
|
|
|
|
// Name identifies this backend in the e2ee registry and config.
|
|
const Name = "omemo"
|
|
|
|
// initialPreKeyCount is how many one-time prekeys EnsureIdentity generates
|
|
// up front - a conventional pool size (matches common OMEMO implementation
|
|
// practice) balancing bundle-refill frequency against bundle size.
|
|
// Replenishing a depleted pool (prekeys are removed automatically as
|
|
// they're consumed - see ListPreKeyIDs's doc comment) is a future
|
|
// enhancement, not handled yet.
|
|
const initialPreKeyCount = 100
|
|
|
|
// ownDeviceIDNum is the sole device id this gateway ever publishes for an
|
|
// owned identity - there is exactly one telegabber process encrypting on
|
|
// behalf of any given bridged chat pseudo-JID, so unlike a real multi-device
|
|
// XMPP account, no more than one device id is ever needed.
|
|
const ownDeviceIDNum uint32 = 1
|
|
|
|
// OwnDeviceID is ownDeviceIDNum in e2ee.DeviceID form, for callers that
|
|
// need to reference "the" device of an owned identity (e.g. PublishedBundle).
|
|
var OwnDeviceID = e2ee.DeviceID(strconv.FormatUint(uint64(ownDeviceIDNum), 10))
|
|
|
|
// Backend is the OMEMO (XEP-0384) implementation of e2ee.Backend. It always
|
|
// supports every Version this build is capable of (see Variants) - a
|
|
// single identity/prekey set serves all of them simultaneously (XEP-0384's
|
|
// signed prekeys carry both a legacy and an OMEMO signature precisely so
|
|
// this works, see libsignal.SignedPreKeyInfo) - rather than being
|
|
// configured for one fixed version. Which version is actually used for a
|
|
// given chat's outgoing content is negotiated per chat (see
|
|
// NegotiatedVariant/negotiatedVersion), not chosen at construction time.
|
|
type Backend struct {
|
|
libctx *libsignal.Context
|
|
db *badgerstore.DB
|
|
|
|
// libsignal is not internally thread-safe (see its package doc) - one
|
|
// mutex serializes every call into it, across every identity this
|
|
// backend manages. Simple and correct; per-identity striping would
|
|
// allow more concurrency but isn't needed at telegabber's message
|
|
// volume.
|
|
mu sync.Mutex
|
|
}
|
|
|
|
// New creates an OMEMO backend backed by db.
|
|
func New(libctx *libsignal.Context, db *badgerstore.DB) *Backend {
|
|
return &Backend{libctx: libctx, db: db}
|
|
}
|
|
|
|
var _ e2ee.Backend = (*Backend)(nil)
|
|
|
|
func (b *Backend) Name() string { return Name }
|
|
|
|
// Close implements e2ee.Backend.
|
|
func (b *Backend) Close() error { return b.db.Close() }
|
|
|
|
func (b *Backend) Namespaces() e2ee.Namespaces {
|
|
disco := []string{omemo0NS + ".devicelist+notify"}
|
|
if libsignal.ProtocolV4Supported {
|
|
disco = append(disco, omemo1NS+":devices+notify", omemo2NS+":devices+notify")
|
|
}
|
|
return e2ee.Namespaces{Disco: disco}
|
|
}
|
|
|
|
// Variants implements e2ee.Backend. Omemo1/Omemo2 (both libsignal.ProtocolVersionV4)
|
|
// are only listed when this build's crypto library actually supports
|
|
// protocol v4 (see libsignal.ProtocolV4Supported - false for the
|
|
// signal_legacy build tag, vanilla libsignal-protocol-c).
|
|
func (b *Backend) Variants() []e2ee.Variant {
|
|
variants := []e2ee.Variant{e2ee.Variant(Omemo0.String())}
|
|
if libsignal.ProtocolV4Supported {
|
|
variants = append(variants, e2ee.Variant(Omemo1.String()), e2ee.Variant(Omemo2.String()))
|
|
}
|
|
return variants
|
|
}
|
|
|
|
// DefaultVariant implements e2ee.Backend: Omemo0, the most widely deployed
|
|
// version, is always what a brand new chat starts out using.
|
|
func (b *Backend) DefaultVariant() e2ee.Variant {
|
|
return e2ee.Variant(Omemo0.String())
|
|
}
|
|
|
|
// NegotiatedVariant implements e2ee.Backend.
|
|
//
|
|
// KNOWN LIMITATION: this only ever reflects whatever variant a chat's
|
|
// session was ESTABLISHED at (via IngestRemoteBundle, or reactively via
|
|
// Decrypt processing a peer's first PreKeyMessage) - there is currently no
|
|
// way to re-negotiate an EXISTING chat to a different variant later.
|
|
// Reading libomemo-c's session_builder.c/ratchet.c directly shows that
|
|
// session_builder_process_pre_key_bundle/process_pre_key_signal_message
|
|
// always archive the OLD session state and have the replacement INHERIT
|
|
// its version (session_record_archive_current_state literally copies
|
|
// session_record_get_version(record) onto the fresh state) whenever a
|
|
// session record already exists for that address - only a genuinely fresh
|
|
// (never-before-seen) address gets its version set from what's actually
|
|
// requested. So encrypting/decrypting with a new variant against an
|
|
// address that already has a session silently keeps using the OLD
|
|
// session's version regardless of what's asked for - it does not, and
|
|
// cannot, upgrade in place. A future enhancement wanting real
|
|
// re-negotiation would need to explicitly delete the existing session
|
|
// (Store.DeleteSession/DeleteAllSessions) before re-ingesting a bundle
|
|
// under the new variant.
|
|
func (b *Backend) NegotiatedVariant(owner e2ee.PeerID) (e2ee.Variant, error) {
|
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
|
if !ok {
|
|
return "", fmt.Errorf("omemo: NegotiatedVariant: %q is not an OwnedPeer", owner)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
version, err := b.negotiatedVersion(b.db.Store(b.libctx, login, ownerJID))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return e2ee.Variant(version.String()), nil
|
|
}
|
|
|
|
// negotiatedVersion is NegotiatedVariant's internal counterpart, returning
|
|
// a Version directly for Encrypt's own use - store must already be scoped
|
|
// to the right (login, owner) and b.mu must already be held.
|
|
func (b *Backend) negotiatedVersion(store *badgerstore.Store) (Version, error) {
|
|
raw, ok, err := store.NegotiatedVersion()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if !ok {
|
|
return Omemo0, nil
|
|
}
|
|
return Version(raw), nil
|
|
}
|
|
|
|
// DeviceListNode implements e2ee.Backend. Node names follow each version's
|
|
// own established convention: Omemo0 uses siacs' original
|
|
// "<ns>.devicelist"; Omemo1/Omemo2 use XEP-0384's "<ns>:devices" (confirmed
|
|
// against the archived 0.4.0 spec text directly, not assumed).
|
|
func (b *Backend) DeviceListNode(variant e2ee.Variant) string {
|
|
version, err := ParseVersion(string(variant))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
switch version {
|
|
case Omemo0:
|
|
return omemo0NS + ".devicelist"
|
|
case Omemo1:
|
|
return omemo1NS + ":devices"
|
|
default:
|
|
return omemo2NS + ":devices"
|
|
}
|
|
}
|
|
|
|
// BundleNode implements e2ee.Backend. Omemo0 addresses each device's bundle
|
|
// under its own node ("<ns>.bundles:<device>"); Omemo1/Omemo2 instead use
|
|
// one shared node per XEP-0384 0.4.0's bundle-storage redesign ("Each
|
|
// bundle MUST be stored in a se[p]arate item. The item id MUST be set to
|
|
// the device id.") - confirmed against the archived 0.4.0 spec text, not
|
|
// assumed. Either way, e2ee/fetch.go only ever needs the most recent item
|
|
// on whatever node this returns, so callers don't need to know which shape
|
|
// applies.
|
|
func (b *Backend) BundleNode(variant e2ee.Variant, device e2ee.DeviceID) string {
|
|
version, err := ParseVersion(string(variant))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
if version == Omemo0 {
|
|
return fmt.Sprintf("%s.bundles:%s", omemo0NS, device)
|
|
}
|
|
ns := omemo1NS
|
|
if version == Omemo2 {
|
|
ns = omemo2NS
|
|
}
|
|
return ns + ":bundles"
|
|
}
|
|
|
|
func (b *Backend) EnsureIdentity(peer e2ee.PeerID) error {
|
|
login, owner, ok := e2ee.SplitOwnedPeer(peer)
|
|
if !ok {
|
|
return fmt.Errorf("omemo: EnsureIdentity: %q is not an OwnedPeer", peer)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, owner)
|
|
|
|
has, err := store.HasIdentityKeyPair()
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: HasIdentityKeyPair: %w", err)
|
|
}
|
|
if has {
|
|
return nil // idempotent - already set up
|
|
}
|
|
|
|
idKeyPair, err := libsignal.GenerateIdentityKeyPair(b.libctx)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: GenerateIdentityKeyPair: %w", err)
|
|
}
|
|
if err := store.SaveIdentityKeyPair(idKeyPair.Record); err != nil {
|
|
return fmt.Errorf("omemo: SaveIdentityKeyPair: %w", err)
|
|
}
|
|
|
|
regID, err := libsignal.GenerateRegistrationID(b.libctx)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: GenerateRegistrationID: %w", err)
|
|
}
|
|
if err := store.SaveRegistrationID(regID); err != nil {
|
|
return fmt.Errorf("omemo: SaveRegistrationID: %w", err)
|
|
}
|
|
|
|
signedPreKey, err := libsignal.GenerateSignedPreKey(b.libctx, idKeyPair, 1)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: GenerateSignedPreKey: %w", err)
|
|
}
|
|
if err := store.StoreSignedPreKey(signedPreKey.ID, signedPreKey.Record); err != nil {
|
|
return fmt.Errorf("omemo: StoreSignedPreKey: %w", err)
|
|
}
|
|
|
|
start, err := store.NextPreKeyIDs(initialPreKeyCount)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: NextPreKeyIDs: %w", err)
|
|
}
|
|
preKeys, err := libsignal.GeneratePreKeys(b.libctx, start, initialPreKeyCount)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: GeneratePreKeys: %w", err)
|
|
}
|
|
for _, pk := range preKeys {
|
|
if err := store.StorePreKey(pk.ID, pk.Record); err != nil {
|
|
return fmt.Errorf("omemo: StorePreKey %d: %w", pk.ID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) PublishedIdentity(peer e2ee.PeerID, variant e2ee.Variant) (e2ee.DeviceListDoc, error) {
|
|
login, owner, ok := e2ee.SplitOwnedPeer(peer)
|
|
if !ok {
|
|
return e2ee.DeviceListDoc{}, fmt.Errorf("omemo: PublishedIdentity: %q is not an OwnedPeer", peer)
|
|
}
|
|
version, err := ParseVersion(string(variant))
|
|
if err != nil {
|
|
return e2ee.DeviceListDoc{}, fmt.Errorf("omemo: PublishedIdentity: %w", err)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, owner)
|
|
if has, err := store.HasIdentityKeyPair(); err != nil {
|
|
return e2ee.DeviceListDoc{}, err
|
|
} else if !has {
|
|
return e2ee.DeviceListDoc{}, errors.New("omemo: PublishedIdentity: EnsureIdentity was not called")
|
|
}
|
|
|
|
raw, err := encodeDeviceList(version, ownDeviceIDNum)
|
|
if err != nil {
|
|
return e2ee.DeviceListDoc{}, err
|
|
}
|
|
return e2ee.DeviceListDoc{Raw: raw}, nil
|
|
}
|
|
|
|
func (b *Backend) PublishedBundle(peer e2ee.PeerID, device e2ee.DeviceID, variant e2ee.Variant) (e2ee.BundleDoc, error) {
|
|
login, owner, ok := e2ee.SplitOwnedPeer(peer)
|
|
if !ok {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: PublishedBundle: %q is not an OwnedPeer", peer)
|
|
}
|
|
if device != OwnDeviceID {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: PublishedBundle: unknown device %q", device)
|
|
}
|
|
version, err := ParseVersion(string(variant))
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: PublishedBundle: %w", err)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, owner)
|
|
|
|
identityPublic, _, err := store.GetIdentityKeyPair()
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: GetIdentityKeyPair: %w", err)
|
|
}
|
|
|
|
// EnsureIdentity always creates signed prekey id 1 and never rotates it
|
|
// yet (a future enhancement), so id 1 is the only one that will exist.
|
|
spkRecord, found, err := store.LoadSignedPreKey(1)
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: LoadSignedPreKey: %w", err)
|
|
}
|
|
if !found {
|
|
return e2ee.BundleDoc{}, errors.New("omemo: PublishedBundle: EnsureIdentity was not called")
|
|
}
|
|
spkInfo, err := libsignal.DecodeSignedPreKey(b.libctx, spkRecord)
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: DecodeSignedPreKey: %w", err)
|
|
}
|
|
// See the rule documented on libsignal.SignedPreKeyInfo: the signature
|
|
// must match the serialization form the verifier will re-derive, which
|
|
// depends on the requested variant (Omemo0 -> legacy form, Omemo1/Omemo2
|
|
// -> OMEMO form, since both share libsignal.ProtocolVersionV4) - the
|
|
// same signed prekey serves every variant, just re-signed per request.
|
|
signature := spkInfo.Signature
|
|
if version != Omemo0 {
|
|
signature = spkInfo.SignatureOMEMO
|
|
}
|
|
|
|
preKeyIDs, err := store.ListPreKeyIDs()
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: ListPreKeyIDs: %w", err)
|
|
}
|
|
preKeys := make([]libsignal.PreKeyInfo, 0, len(preKeyIDs))
|
|
for _, id := range preKeyIDs {
|
|
record, found, err := store.LoadPreKey(id)
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: LoadPreKey %d: %w", id, err)
|
|
}
|
|
if !found {
|
|
continue // consumed between ListPreKeyIDs and here - fine, just skip it
|
|
}
|
|
info, err := libsignal.DecodePreKey(b.libctx, record)
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, fmt.Errorf("omemo: DecodePreKey %d: %w", id, err)
|
|
}
|
|
preKeys = append(preKeys, *info)
|
|
}
|
|
|
|
raw, err := encodeBundle(version, identityPublic, spkInfo.ID, spkInfo.PublicKey, signature, preKeys)
|
|
if err != nil {
|
|
return e2ee.BundleDoc{}, err
|
|
}
|
|
return e2ee.BundleDoc{Raw: raw}, nil
|
|
}
|
|
|
|
func (b *Backend) IngestRemoteDeviceList(owner, peer e2ee.PeerID, doc e2ee.DeviceListDoc) ([]e2ee.DeviceID, error) {
|
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
|
if !ok {
|
|
return nil, fmt.Errorf("omemo: IngestRemoteDeviceList: %q is not an OwnedPeer", owner)
|
|
}
|
|
ids, err := parseDeviceList(doc.Raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: parseDeviceList: %w", err)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, ownerJID)
|
|
if err := store.SaveRemoteDeviceListCache(string(peer), doc.Raw); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
deviceIDs := make([]e2ee.DeviceID, len(ids))
|
|
for i, id := range ids {
|
|
deviceIDs[i] = e2ee.DeviceID(strconv.FormatUint(uint64(id), 10))
|
|
}
|
|
return deviceIDs, nil
|
|
}
|
|
|
|
func (b *Backend) IngestRemoteBundle(owner, peer e2ee.PeerID, device e2ee.DeviceID, doc e2ee.BundleDoc) error {
|
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
|
if !ok {
|
|
return fmt.Errorf("omemo: IngestRemoteBundle: %q is not an OwnedPeer", owner)
|
|
}
|
|
deviceID, err := parseDeviceID(device)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
parsed, err := parseBundle(doc.Raw)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: parseBundle: %w", err)
|
|
}
|
|
|
|
var preKeyID uint32
|
|
var preKeyPublic []byte
|
|
if len(parsed.PreKeys) > 0 {
|
|
idx, err := cryptoRandIndex(len(parsed.PreKeys))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
preKeyID = parsed.PreKeys[idx].ID
|
|
preKeyPublic = parsed.PreKeys[idx].Public
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, ownerJID)
|
|
storeCtx, err := libsignal.NewStoreContext(b.libctx, store)
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: NewStoreContext: %w", err)
|
|
}
|
|
defer storeCtx.Close()
|
|
|
|
// The session's protocol version must match what the fetched bundle
|
|
// itself actually is (parsed.Version), derived from doc.Raw's own shape
|
|
// rather than assumed from whatever variant fetch.go asked for -
|
|
// picking the wrong one here fails bundle signature verification with
|
|
// SG_ERR_INVALID_KEY (the same class of bug already hit and fixed once
|
|
// for PublishedBundle/Decrypt - see their comments).
|
|
builder, err := libsignal.NewSessionBuilder(b.libctx, storeCtx, libsignal.Address{Name: string(peer), DeviceID: deviceID}, parsed.Version.libsignalVersion())
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: NewSessionBuilder: %w", err)
|
|
}
|
|
defer builder.Close()
|
|
|
|
err = builder.ProcessPreKeyBundle(b.libctx, libsignal.RemoteBundle{
|
|
// OMEMO's wire format (neither legacy nor OMEMOKeyExchange) ever
|
|
// transmits a registration id - device id already does what it
|
|
// would otherwise disambiguate. 0 is standard, harmless practice.
|
|
RegistrationID: 0,
|
|
DeviceID: deviceID,
|
|
PreKeyID: preKeyID,
|
|
PreKeyPublic: preKeyPublic,
|
|
SignedPreKeyID: parsed.SignedPreKeyID,
|
|
SignedPreKeyPublic: parsed.SignedPreKeyPublic,
|
|
SignedPreKeySignature: parsed.SignedPreKeySignature,
|
|
IdentityKeyPublic: parsed.IdentityKeyPublic,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("omemo: ProcessPreKeyBundle: %w", err)
|
|
}
|
|
|
|
// Keep the chat's negotiated version in sync with whatever session we
|
|
// just established/replaced - libomemo-c's session_cipher_encrypt
|
|
// always serializes using the session's own stored version
|
|
// (session_state_get_session_version), NOT whatever version Encrypt
|
|
// asks for, so a stale negotiated-version record here would make
|
|
// Encrypt produce a WireEnvelope whose declared Version doesn't match
|
|
// the ACTUAL bytes libsignal ends up emitting - confirmed by reading
|
|
// session_cipher.c's session_cipher_encrypt/decrypt directly, not
|
|
// assumed (this is exactly the SG_ERR_INVALID_PROTO_BUF failure the
|
|
// old, purely Decrypt-driven negotiation tracking could hit as soon as
|
|
// a chat's session got established/replaced at a version other than
|
|
// its previously-recorded negotiated one).
|
|
if err := store.SetNegotiatedVersion(byte(parsed.Version)); err != nil {
|
|
return fmt.Errorf("omemo: SetNegotiatedVersion: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) Devices(owner, peer e2ee.PeerID) ([]e2ee.DeviceInfo, error) {
|
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
|
if !ok {
|
|
return nil, fmt.Errorf("omemo: Devices: %q is not an OwnedPeer", owner)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, ownerJID)
|
|
deviceIDs, err := store.GetSubDeviceSessions(string(peer))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
infos := make([]e2ee.DeviceInfo, 0, len(deviceIDs))
|
|
for _, id := range deviceIDs {
|
|
// A stored session implies IsTrustedIdentity already accepted this
|
|
// device's key (TOFU) - finer-grained trust states (manually
|
|
// verified/revoked) are a documented future enhancement, not
|
|
// needed for this phase.
|
|
infos = append(infos, e2ee.DeviceInfo{ID: e2ee.DeviceID(strconv.FormatUint(uint64(id), 10)), Trusted: true})
|
|
}
|
|
return infos, nil
|
|
}
|
|
|
|
func (b *Backend) Enabled(owner e2ee.PeerID) (bool, error) {
|
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
|
if !ok {
|
|
return false, fmt.Errorf("omemo: Enabled: %q is not an OwnedPeer", owner)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b.db.Store(b.libctx, login, ownerJID).Enabled()
|
|
}
|
|
|
|
func (b *Backend) SetEnabled(owner e2ee.PeerID, enabled bool) error {
|
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
|
if !ok {
|
|
return fmt.Errorf("omemo: SetEnabled: %q is not an OwnedPeer", owner)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
return b.db.Store(b.libctx, login, ownerJID).SetEnabled(enabled)
|
|
}
|
|
|
|
func parseDeviceID(device e2ee.DeviceID) (uint32, error) {
|
|
id, err := strconv.ParseUint(string(device), 10, 32)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("omemo: invalid device id %q: %w", device, err)
|
|
}
|
|
return uint32(id), nil
|
|
}
|
|
|
|
func cryptoRandIndex(n int) (int, error) {
|
|
i, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return int(i.Int64()), nil
|
|
}
|