mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
196 lines
7 KiB
Go
196 lines
7 KiB
Go
package omemo
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"dev.narayana.im/narayana/telegabber/e2ee"
|
|
"dev.narayana.im/narayana/telegabber/e2ee/omemo/libsignal"
|
|
)
|
|
|
|
func (b *Backend) Encrypt(from e2ee.PeerID, to []e2ee.PeerID, plaintext []byte) (e2ee.Envelope, error) {
|
|
login, owner, ok := e2ee.SplitOwnedPeer(from)
|
|
if !ok {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: Encrypt: %q is not an OwnedPeer", from)
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, owner)
|
|
if err := b.ensureIdentity(store); err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: ensureIdentity: %w", err)
|
|
}
|
|
|
|
// Which variant to actually encrypt as is negotiated per chat, not
|
|
// fixed on the backend - see negotiatedVersion's doc comment.
|
|
version, err := b.negotiatedVersion(store)
|
|
if err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: negotiatedVersion: %w", err)
|
|
}
|
|
|
|
// Outer content encryption happens once per message, regardless of how
|
|
// many recipient devices it ends up going to - see envelope.go.
|
|
payload, err := EncryptOuter(version, plaintext, owner)
|
|
if err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: EncryptOuter: %w", err)
|
|
}
|
|
|
|
storeCtx, err := libsignal.NewStoreContext(b.libctx, store)
|
|
if err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: NewStoreContext: %w", err)
|
|
}
|
|
defer storeCtx.Close()
|
|
|
|
env := &WireEnvelope{
|
|
Version: version,
|
|
SenderSID: ownDeviceIDNum,
|
|
IV: payload.IV,
|
|
Payload: payload.Ciphertext,
|
|
}
|
|
|
|
for _, peer := range to {
|
|
deviceIDs, err := store.GetSubDeviceSessions(string(peer))
|
|
if err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: GetSubDeviceSessions(%s): %w", peer, err)
|
|
}
|
|
if len(deviceIDs) == 0 {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: Encrypt: no known devices/sessions for %s (IngestRemoteBundle was not called)", peer)
|
|
}
|
|
|
|
for _, deviceID := range deviceIDs {
|
|
// A manually blocked device (see Backend.BlockDevice) never
|
|
// gets a copy, regardless of any session already established
|
|
// with it - TOFU trust itself is untouched, this is purely the
|
|
// manual override layered on top.
|
|
if blocked, err := store.IsDeviceBlocked(string(peer), deviceID); err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: IsDeviceBlocked(%s:%d): %w", peer, deviceID, err)
|
|
} else if blocked {
|
|
continue
|
|
}
|
|
|
|
cipher, err := libsignal.NewSessionCipher(b.libctx, storeCtx, libsignal.Address{Name: string(peer), DeviceID: deviceID}, version.libsignalVersion())
|
|
if err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: NewSessionCipher(%s:%d): %w", peer, deviceID, err)
|
|
}
|
|
ct, err := cipher.Encrypt(payload.ContentKey)
|
|
cipher.Close()
|
|
if err != nil {
|
|
return e2ee.Envelope{}, fmt.Errorf("omemo: Encrypt(%s:%d): %w", peer, deviceID, err)
|
|
}
|
|
env.Keys = append(env.Keys, WireKey{
|
|
RecipientJID: string(peer),
|
|
DeviceID: deviceID,
|
|
IsPreKey: ct.Type == libsignal.CiphertextPreKeyType,
|
|
Ciphertext: ct.Serialized,
|
|
})
|
|
}
|
|
}
|
|
|
|
raw, err := env.Encode()
|
|
if err != nil {
|
|
return e2ee.Envelope{}, err
|
|
}
|
|
return e2ee.Envelope{Backend: Name, Raw: raw}, nil
|
|
}
|
|
|
|
func (b *Backend) Decrypt(from, to e2ee.PeerID, env e2ee.Envelope) ([]byte, error) {
|
|
if env.Backend != Name {
|
|
return nil, fmt.Errorf("omemo: Decrypt: envelope backend %q, want %q", env.Backend, Name)
|
|
}
|
|
login, owner, ok := e2ee.SplitOwnedPeer(to)
|
|
if !ok {
|
|
return nil, fmt.Errorf("omemo: Decrypt: %q is not an OwnedPeer", to)
|
|
}
|
|
|
|
wireEnv, err := DecodeWireEnvelope(env.Raw)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: DecodeWireEnvelope: %w", err)
|
|
}
|
|
|
|
var keyForUs *WireKey
|
|
for i := range wireEnv.Keys {
|
|
if wireEnv.Keys[i].DeviceID == ownDeviceIDNum {
|
|
keyForUs = &wireEnv.Keys[i]
|
|
break
|
|
}
|
|
}
|
|
if keyForUs == nil {
|
|
return nil, errors.New("omemo: Decrypt: no <key> addressed to our own device in this envelope")
|
|
}
|
|
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
|
|
store := b.db.Store(b.libctx, login, owner)
|
|
if err := b.ensureIdentity(store); err != nil {
|
|
return nil, fmt.Errorf("omemo: ensureIdentity: %w", err)
|
|
}
|
|
|
|
// A manually blocked sender device (see Backend.BlockDevice) is
|
|
// rejected outright, regardless of any session already established
|
|
// with it - TOFU trust itself is untouched, this is purely the manual
|
|
// override layered on top.
|
|
if blocked, err := store.IsDeviceBlocked(string(from), wireEnv.SenderSID); err != nil {
|
|
return nil, fmt.Errorf("omemo: IsDeviceBlocked: %w", err)
|
|
} else if blocked {
|
|
return nil, fmt.Errorf("omemo: Decrypt: sender device %s:%d is blocked", from, wireEnv.SenderSID)
|
|
}
|
|
|
|
regID, err := store.GetLocalRegistrationID()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: GetLocalRegistrationID: %w", err)
|
|
}
|
|
|
|
storeCtx, err := libsignal.NewStoreContext(b.libctx, store)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: NewStoreContext: %w", err)
|
|
}
|
|
defer storeCtx.Close()
|
|
|
|
// The cipher's configured version must match what the SENDER actually
|
|
// used (wireEnv.Version, derived from which XML namespace their stanza
|
|
// used) - a remote peer's message version isn't guaranteed to match
|
|
// whatever we've last negotiated for outgoing content (see
|
|
// negotiatedVersion), so this is deliberately read straight from the
|
|
// envelope rather than assumed.
|
|
cipher, err := libsignal.NewSessionCipher(b.libctx, storeCtx, libsignal.Address{Name: string(from), DeviceID: wireEnv.SenderSID}, wireEnv.Version.libsignalVersion())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: NewSessionCipher: %w", err)
|
|
}
|
|
defer cipher.Close()
|
|
|
|
msgType := libsignal.CiphertextSignalType
|
|
if keyForUs.IsPreKey {
|
|
msgType = libsignal.CiphertextPreKeyType
|
|
}
|
|
// Omemo1 and Omemo2 both use the OMEMOKeyExchange/OMEMOAuthenticatedMessage
|
|
// wire framing (libsignal.ProtocolVersionV4); only Omemo0 uses the
|
|
// original WhisperTextProtocol framing.
|
|
useOmemoFraming := wireEnv.Version != Omemo0
|
|
|
|
contentKey, err := cipher.Decrypt(b.libctx, libsignal.CiphertextMessage{Type: msgType, Serialized: keyForUs.Ciphertext}, useOmemoFraming, regID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: SessionCipher.Decrypt: %w", err)
|
|
}
|
|
|
|
plaintext, err := DecryptOuter(wireEnv.Version, &EncryptedPayload{Ciphertext: wireEnv.Payload, IV: wireEnv.IV, ContentKey: contentKey})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("omemo: DecryptOuter: %w", err)
|
|
}
|
|
|
|
// A successful decrypt is the negotiation signal: "to"'s outgoing
|
|
// content should now use whatever variant this message actually used.
|
|
// This only actually changes anything the first time a session is
|
|
// established for "from" (a regular message on an existing session can
|
|
// only ever arrive at wireEnv.Version already matching that session's
|
|
// own stored version - libomemo-c rejects anything else outright - so
|
|
// this is a harmless no-op then); see the KNOWN LIMITATION on
|
|
// e2ee.Backend.NegotiatedVariant's doc comment for why an
|
|
// already-established chat can't actually be moved to a different
|
|
// variant later.
|
|
if err := store.SetNegotiatedVersion(byte(wireEnv.Version)); err != nil {
|
|
return nil, fmt.Errorf("omemo: SetNegotiatedVersion: %w", err)
|
|
}
|
|
return plaintext, nil
|
|
}
|