147 lines
6 KiB
Go
147 lines
6 KiB
Go
|
// Copyright (c) 2020 Tulir Asokan
|
||
|
//
|
||
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
||
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
|
|
||
|
package crypto
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strings"
|
||
|
|
||
|
"maunium.net/go/mautrix/event"
|
||
|
"maunium.net/go/mautrix/id"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
IncorrectEncryptedContentType = errors.New("event content is not instance of *event.EncryptedEventContent")
|
||
|
NoSessionFound = errors.New("failed to decrypt megolm event: no session with given ID found")
|
||
|
DuplicateMessageIndex = errors.New("duplicate megolm message index")
|
||
|
WrongRoom = errors.New("encrypted megolm event is not intended for this room")
|
||
|
DeviceKeyMismatch = errors.New("device keys in event and verified device info do not match")
|
||
|
SenderKeyMismatch = errors.New("sender keys in content and megolm session do not match")
|
||
|
)
|
||
|
|
||
|
type megolmEvent struct {
|
||
|
RoomID id.RoomID `json:"room_id"`
|
||
|
Type event.Type `json:"type"`
|
||
|
Content event.Content `json:"content"`
|
||
|
}
|
||
|
|
||
|
// DecryptMegolmEvent decrypts an m.room.encrypted event where the algorithm is m.megolm.v1.aes-sha2
|
||
|
func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, error) {
|
||
|
content, ok := evt.Content.Parsed.(*event.EncryptedEventContent)
|
||
|
if !ok {
|
||
|
return nil, IncorrectEncryptedContentType
|
||
|
} else if content.Algorithm != id.AlgorithmMegolmV1 {
|
||
|
return nil, UnsupportedAlgorithm
|
||
|
}
|
||
|
encryptionRoomID := evt.RoomID
|
||
|
// Allow the server to move encrypted events between rooms if both the real room and target room are on a non-federatable .local domain.
|
||
|
// The message index checks to prevent replay attacks still apply and aren't based on the room ID,
|
||
|
// so the event ID and timestamp must remain the same when the event is moved to a different room.
|
||
|
if origRoomID, ok := evt.Content.Raw["com.beeper.original_room_id"].(string); ok && strings.HasSuffix(origRoomID, ".local") && strings.HasSuffix(evt.RoomID.String(), ".local") {
|
||
|
encryptionRoomID = id.RoomID(origRoomID)
|
||
|
}
|
||
|
sess, err := mach.CryptoStore.GetGroupSession(encryptionRoomID, content.SenderKey, content.SessionID)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to get group session: %w", err)
|
||
|
} else if sess == nil {
|
||
|
return nil, fmt.Errorf("%w (ID %s)", NoSessionFound, content.SessionID)
|
||
|
} else if content.SenderKey != "" && content.SenderKey != sess.SenderKey {
|
||
|
return nil, SenderKeyMismatch
|
||
|
}
|
||
|
plaintext, messageIndex, err := sess.Internal.Decrypt(content.MegolmCiphertext)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to decrypt megolm event: %w", err)
|
||
|
} else if ok, err = mach.CryptoStore.ValidateMessageIndex(sess.SenderKey, content.SessionID, evt.ID, messageIndex, evt.Timestamp); err != nil {
|
||
|
return nil, fmt.Errorf("failed to check if message index is duplicate: %w", err)
|
||
|
} else if !ok {
|
||
|
return nil, DuplicateMessageIndex
|
||
|
}
|
||
|
|
||
|
var trustLevel id.TrustState
|
||
|
var forwardedKeys bool
|
||
|
var device *id.Device
|
||
|
ownSigningKey, ownIdentityKey := mach.account.Keys()
|
||
|
if sess.SigningKey == ownSigningKey && sess.SenderKey == ownIdentityKey && len(sess.ForwardingChains) == 0 {
|
||
|
trustLevel = id.TrustStateVerified
|
||
|
} else {
|
||
|
device, err = mach.GetOrFetchDeviceByKey(evt.Sender, sess.SenderKey)
|
||
|
if err != nil {
|
||
|
// We don't want to throw these errors as the message can still be decrypted.
|
||
|
mach.Log.Debug("Failed to get device %s/%s to verify session %s: %v", evt.Sender, sess.SenderKey, sess.ID(), err)
|
||
|
trustLevel = id.TrustStateUnknownDevice
|
||
|
} else if len(sess.ForwardingChains) == 0 || (len(sess.ForwardingChains) == 1 && sess.ForwardingChains[0] == sess.SenderKey.String()) {
|
||
|
if device == nil {
|
||
|
mach.Log.Debug("Couldn't resolve trust level of session %s: sent by unknown device %s/%s", sess.ID(), evt.Sender, sess.SenderKey)
|
||
|
trustLevel = id.TrustStateUnknownDevice
|
||
|
} else if device.SigningKey != sess.SigningKey || device.IdentityKey != sess.SenderKey {
|
||
|
return nil, DeviceKeyMismatch
|
||
|
} else {
|
||
|
trustLevel = mach.ResolveTrust(device)
|
||
|
}
|
||
|
} else {
|
||
|
forwardedKeys = true
|
||
|
lastChainItem := sess.ForwardingChains[len(sess.ForwardingChains)-1]
|
||
|
device, _ = mach.CryptoStore.FindDeviceByKey(evt.Sender, id.IdentityKey(lastChainItem))
|
||
|
if device != nil {
|
||
|
trustLevel = mach.ResolveTrust(device)
|
||
|
} else {
|
||
|
mach.Log.Debug("Couldn't resolve trust level of session %s: forwarding chain ends with unknown device %s", sess.ID(), lastChainItem)
|
||
|
trustLevel = id.TrustStateForwarded
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
megolmEvt := &megolmEvent{}
|
||
|
err = json.Unmarshal(plaintext, &megolmEvt)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to parse megolm payload: %w", err)
|
||
|
} else if megolmEvt.RoomID != encryptionRoomID {
|
||
|
return nil, WrongRoom
|
||
|
}
|
||
|
megolmEvt.Type.Class = evt.Type.Class
|
||
|
err = megolmEvt.Content.ParseRaw(megolmEvt.Type)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, event.ErrUnsupportedContentType) {
|
||
|
mach.Log.Warn("Unsupported event type %s in encrypted event %s", megolmEvt.Type.Repr(), evt.ID)
|
||
|
} else {
|
||
|
return nil, fmt.Errorf("failed to parse content of megolm payload event: %w", err)
|
||
|
}
|
||
|
}
|
||
|
if content.RelatesTo != nil {
|
||
|
relatable, ok := megolmEvt.Content.Parsed.(event.Relatable)
|
||
|
if ok {
|
||
|
if relatable.OptionalGetRelatesTo() == nil {
|
||
|
relatable.SetRelatesTo(content.RelatesTo)
|
||
|
} else {
|
||
|
mach.Log.Trace("Not overriding relation data in %s, as encrypted payload already has it", evt.ID)
|
||
|
}
|
||
|
}
|
||
|
if _, hasRelation := megolmEvt.Content.Raw["m.relates_to"]; !hasRelation {
|
||
|
megolmEvt.Content.Raw["m.relates_to"] = evt.Content.Raw["m.relates_to"]
|
||
|
}
|
||
|
}
|
||
|
megolmEvt.Type.Class = evt.Type.Class
|
||
|
return &event.Event{
|
||
|
Sender: evt.Sender,
|
||
|
Type: megolmEvt.Type,
|
||
|
Timestamp: evt.Timestamp,
|
||
|
ID: evt.ID,
|
||
|
RoomID: evt.RoomID,
|
||
|
Content: megolmEvt.Content,
|
||
|
Unsigned: evt.Unsigned,
|
||
|
Mautrix: event.MautrixInfo{
|
||
|
TrustState: trustLevel,
|
||
|
TrustSource: device,
|
||
|
ForwardedKeys: forwardedKeys,
|
||
|
WasEncrypted: true,
|
||
|
ReceivedAt: evt.Mautrix.ReceivedAt,
|
||
|
},
|
||
|
}, nil
|
||
|
}
|