diff --git a/crypto/account.go b/crypto/account.go index 64d6425e..72c24bd8 100644 --- a/crypto/account.go +++ b/crypto/account.go @@ -13,8 +13,10 @@ import ( ) type OlmAccount struct { - Internal olm.Account - Shared bool + Internal olm.Account + signingKey id.Curve25519 + identityKey id.Ed25519 + Shared bool } func NewOlmAccount() *OlmAccount { @@ -23,15 +25,35 @@ func NewOlmAccount() *OlmAccount { } } +func (account *OlmAccount) Keys() (id.Ed25519, id.Curve25519) { + if len(account.signingKey) == 0 || len(account.identityKey) == 0 { + account.identityKey, account.signingKey = account.Internal.IdentityKeys() + } + return account.identityKey, account.signingKey +} + +func (account *OlmAccount) SigningKey() id.Curve25519 { + if len(account.signingKey) == 0 { + account.identityKey, account.signingKey = account.Internal.IdentityKeys() + } + return account.signingKey +} + +func (account *OlmAccount) IdentityKey() id.Ed25519 { + if len(account.identityKey) == 0 { + account.identityKey, account.signingKey = account.Internal.IdentityKeys() + } + return account.identityKey +} + func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID) *mautrix.DeviceKeys { - ed, curve := account.Internal.IdentityKeys() deviceKeys := &mautrix.DeviceKeys{ UserID: userID, DeviceID: deviceID, Algorithms: []id.Algorithm{id.AlgorithmMegolmV1, id.AlgorithmOlmV1}, Keys: map[id.DeviceKeyID]string{ - id.NewDeviceKeyID(id.KeyAlgorithmCurve25519, deviceID): string(curve), - id.NewDeviceKeyID(id.KeyAlgorithmEd25519, deviceID): string(ed), + id.NewDeviceKeyID(id.KeyAlgorithmCurve25519, deviceID): string(account.SigningKey()), + id.NewDeviceKeyID(id.KeyAlgorithmEd25519, deviceID): string(account.IdentityKey()), }, } @@ -49,7 +71,7 @@ func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID } func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID, currentOTKCount int) map[id.KeyID]mautrix.OneTimeKey { - newCount := int(account.Internal.MaxNumberOfOneTimeKeys() / 2) - currentOTKCount + newCount := int(account.Internal.MaxNumberOfOneTimeKeys()/2) - currentOTKCount if newCount > 0 { account.Internal.GenOneTimeKeys(uint(newCount)) } diff --git a/crypto/decryptmegolm.go b/crypto/decryptmegolm.go index 47d24884..77af9c96 100644 --- a/crypto/decryptmegolm.go +++ b/crypto/decryptmegolm.go @@ -23,12 +23,13 @@ var ( DeviceKeyMismatch = errors.New("device keys in event and verified device info do not match") ) -type MegolmEvent struct { +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 { @@ -51,7 +52,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro } var verified bool - ownSigningKey, ownIdentityKey := mach.account.Internal.IdentityKeys() + ownSigningKey, ownIdentityKey := mach.account.Keys() if content.DeviceID == mach.Client.DeviceID && sess.SigningKey == ownSigningKey && content.SenderKey == ownIdentityKey { verified = true } else { @@ -68,7 +69,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro } } - megolmEvt := &MegolmEvent{} + megolmEvt := &megolmEvent{} err = json.Unmarshal(plaintext, &megolmEvt) if err != nil { return nil, errors.Wrap(err, "failed to parse megolm payload") diff --git a/crypto/decryptolm.go b/crypto/decryptolm.go index bfab2dba..5b2eb2e2 100644 --- a/crypto/decryptolm.go +++ b/crypto/decryptolm.go @@ -26,7 +26,8 @@ var ( RecipientKeyMismatch = errors.New("mismatched recipient key in olm payload") ) -type OlmEvent struct { +// DecryptedOlmEvent represents an event that was decrypted from an event encrypted with the m.olm.v1.curve25519-aes-sha2 algorithm. +type DecryptedOlmEvent struct { Source *event.Event `json:"-"` SenderKey id.SenderKey `json:"-"` @@ -41,15 +42,14 @@ type OlmEvent struct { Content event.Content `json:"content"` } -func (mach *OlmMachine) decryptOlmEvent(evt *event.Event) (*OlmEvent, error) { +func (mach *OlmMachine) decryptOlmEvent(evt *event.Event) (*DecryptedOlmEvent, error) { content, ok := evt.Content.Parsed.(*event.EncryptedEventContent) if !ok { return nil, IncorrectEncryptedContentType } else if content.Algorithm != id.AlgorithmOlmV1 { return nil, UnsupportedAlgorithm } - _, ownKey := mach.account.Internal.IdentityKeys() - ownContent, ok := content.OlmCiphertext[ownKey] + ownContent, ok := content.OlmCiphertext[mach.account.SigningKey()] if !ok { return nil, NotEncryptedForMe } @@ -65,7 +65,7 @@ type OlmEventKeys struct { Ed25519 id.Ed25519 `json:"ed25519"` } -func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.DeviceID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*OlmEvent, error) { +func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.DeviceID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*DecryptedOlmEvent, error) { if olmType != id.OlmMsgTypePreKey && olmType != id.OlmMsgTypeMsg { return nil, UnsupportedOlmMessageType } @@ -107,7 +107,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic } } - var olmEvt OlmEvent + var olmEvt DecryptedOlmEvent err = json.Unmarshal(plaintext, &olmEvt) if err != nil { return nil, errors.Wrap(err, "failed to parse olm payload") @@ -116,7 +116,7 @@ func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, deviceID id.Devic return nil, SenderMismatch } else if mach.Client.UserID != olmEvt.Recipient { return nil, RecipientMismatch - } else if ed25519, _ := mach.account.Internal.IdentityKeys(); ed25519 != olmEvt.RecipientKeys.Ed25519 { + } else if mach.account.IdentityKey() != olmEvt.RecipientKeys.Ed25519 { return nil, RecipientKeyMismatch } @@ -165,7 +165,7 @@ func (mach *OlmMachine) createInboundSession(senderKey id.SenderKey, ciphertext if err != nil { return nil, err } - mach.SaveAccount() + mach.saveAccount() err = mach.CryptoStore.AddSession(senderKey, session) if err != nil { mach.Log.Error("Failed to store created inbound session: %v", err) diff --git a/crypto/devicelist.go b/crypto/devicelist.go index 38a009ca..2c10f30a 100644 --- a/crypto/devicelist.go +++ b/crypto/devicelist.go @@ -92,6 +92,10 @@ func (mach *OlmMachine) fetchKeys(users []id.UserID, sinceToken string, includeU return data } +// OnDevicesChanged finds all shared rooms with the given user and invalidates outbound sessions in those rooms. +// +// This is called automatically whenever a device list change is noticed in ProcessSyncResponse and usually does +// not need to be called manually. func (mach *OlmMachine) OnDevicesChanged(userID id.UserID) { for _, roomID := range mach.StateStore.FindSharedRooms(userID) { mach.Log.Debug("Devices of %s changed, invalidating group session for %s", userID, roomID) diff --git a/crypto/encryptmegolm.go b/crypto/encryptmegolm.go index 8d589b5a..db539ceb 100644 --- a/crypto/encryptmegolm.go +++ b/crypto/encryptmegolm.go @@ -39,6 +39,15 @@ type rawMegolmEvent struct { Content interface{} `json:"content"` } +// IsShareError returns true if the error is caused by the lack of an outgoing megolm session and can be solved with OlmMachine.ShareGroupSession +func IsShareError(err error) bool { + return err == SessionExpired || err == SessionNotShared || err == NoGroupSession +} + +// EncryptMegolmEvent encrypts data with the m.megolm.v1.aes-sha2 algorithm. +// +// If you use the event.Content struct, make sure you pass a pointer to the struct, +// as JSON serialization will not work correctly otherwise. func (mach *OlmMachine) EncryptMegolmEvent(roomID id.RoomID, evtType event.Type, content interface{}) (*event.EncryptedEventContent, error) { mach.Log.Trace("Encrypting event of type %s for %s", evtType.Type, roomID) session, err := mach.CryptoStore.GetOutboundGroupSession(roomID) @@ -63,10 +72,9 @@ func (mach *OlmMachine) EncryptMegolmEvent(roomID id.RoomID, evtType event.Type, if err != nil { mach.Log.Warn("Failed to update megolm session in crypto store after encrypting: %v", err) } - _, idKey := mach.account.Internal.IdentityKeys() return &event.EncryptedEventContent{ Algorithm: id.AlgorithmMegolmV1, - SenderKey: idKey, + SenderKey: mach.account.SigningKey(), DeviceID: mach.Client.DeviceID, SessionID: session.ID(), MegolmCiphertext: ciphertext, @@ -76,11 +84,15 @@ func (mach *OlmMachine) EncryptMegolmEvent(roomID id.RoomID, evtType event.Type, func (mach *OlmMachine) newOutboundGroupSession(roomID id.RoomID) *OutboundGroupSession { session := NewOutboundGroupSession(roomID) - signingKey, idKey := mach.account.Internal.IdentityKeys() + signingKey, idKey := mach.account.Keys() mach.createGroupSession(idKey, signingKey, roomID, session.ID(), session.Internal.Key()) return session } +// ShareGroupSession shares a group session for a specific room with all the devices of the given user list. +// +// For devices with TrustStateBlacklisted, a m.room_key.withheld event with code=m.blacklisted is sent. +// If AllowUnverifiedDevices is false, a similar event with code=m.unverified is sent to devices with TrustStateUnset func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) error { mach.Log.Trace("Sharing group session for room %s to %v", roomID, users) session, err := mach.CryptoStore.GetOutboundGroupSession(roomID) @@ -159,18 +171,33 @@ func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) e func (mach *OlmMachine) encryptGroupSessionForUser(session *OutboundGroupSession, userID id.UserID, devices map[id.DeviceID]*DeviceIdentity, output map[id.DeviceID]*event.Content, missingOutput map[id.DeviceID]*DeviceIdentity) { for deviceID, device := range devices { userKey := UserDevice{UserID: userID, DeviceID: deviceID} - if userID == mach.Client.UserID && deviceID == mach.Client.DeviceID { - session.Users[userKey] = OGSIgnored - } - - // TODO blacklisting and verification checking should be done around here - if state := session.Users[userKey]; state != OGSNotShared { continue - } - - deviceSession, err := mach.CryptoStore.GetLatestSession(device.IdentityKey) - if err != nil { + } else if userID == mach.Client.UserID && deviceID == mach.Client.DeviceID { + session.Users[userKey] = OGSIgnored + } else if device.Trust == TrustStateBlacklisted { + mach.Log.Debug("Not encrypting group session %s for %s of %s: device is blacklisted", session.ID(), deviceID, userID) + output[deviceID] = &event.Content{Parsed: event.RoomKeyWithheldEventContent{ + RoomID: session.RoomID, + Algorithm: id.AlgorithmMegolmV1, + SessionID: session.ID(), + SenderKey: mach.account.SigningKey(), + Code: event.RoomKeyWithheldBlacklisted, + Reason: "Device is blacklisted", + }} + session.Users[userKey] = OGSIgnored + } else if !mach.AllowUnverifiedDevices && device.Trust == TrustStateUnset { + mach.Log.Debug("Not encrypting group session %s for %s of %s: device is not verified", session.ID(), deviceID, userID) + output[deviceID] = &event.Content{Parsed: event.RoomKeyWithheldEventContent{ + RoomID: session.RoomID, + Algorithm: id.AlgorithmMegolmV1, + SessionID: session.ID(), + SenderKey: mach.account.SigningKey(), + Code: event.RoomKeyWithheldUnverified, + Reason: "Device is not verified", + }} + session.Users[userKey] = OGSIgnored + } else if deviceSession, err := mach.CryptoStore.GetLatestSession(device.IdentityKey); err != nil { mach.Log.Error("Failed to get session for %s of %s: %v", deviceID, userID, err) } else if deviceSession == nil { mach.Log.Warn("Didn't find a session for %s of %s", deviceID, userID) diff --git a/crypto/encryptolm.go b/crypto/encryptolm.go index 3d4893c1..76c26d91 100644 --- a/crypto/encryptolm.go +++ b/crypto/encryptolm.go @@ -18,11 +18,10 @@ import ( ) func (mach *OlmMachine) encryptOlmEvent(session *OlmSession, recipient *DeviceIdentity, evtType event.Type, content event.Content) *event.EncryptedEventContent { - selfSigningKey, selfIdentityKey := mach.account.Internal.IdentityKeys() - evt := &OlmEvent{ + evt := &DecryptedOlmEvent{ Sender: mach.Client.UserID, SenderDevice: mach.Client.DeviceID, - Keys: OlmEventKeys{Ed25519: selfSigningKey}, + Keys: OlmEventKeys{Ed25519: mach.account.IdentityKey()}, Recipient: recipient.UserID, RecipientKeys: OlmEventKeys{Ed25519: recipient.SigningKey}, Type: evtType, @@ -39,7 +38,7 @@ func (mach *OlmMachine) encryptOlmEvent(session *OlmSession, recipient *DeviceId } return &event.EncryptedEventContent{ Algorithm: id.AlgorithmOlmV1, - SenderKey: selfIdentityKey, + SenderKey: mach.account.SigningKey(), OlmCiphertext: event.OlmCiphertexts{ recipient.IdentityKey: { Type: msgType, diff --git a/crypto/machine.go b/crypto/machine.go index 5d74eddd..40634eee 100644 --- a/crypto/machine.go +++ b/crypto/machine.go @@ -14,6 +14,8 @@ import ( "maunium.net/go/mautrix/event" ) +// Logger is a simple logging struct for OlmMachine. +// Implementations are recommended to use fmt.Sprintf and manually add a newline after the message. type Logger interface { Error(message string, args ...interface{}) Warn(message string, args ...interface{}) @@ -21,6 +23,7 @@ type Logger interface { Trace(message string, args ...interface{}) } +// OlmMachine is the main struct for handling Matrix end-to-end encryption. type OlmMachine struct { Client *mautrix.Client Log Logger @@ -28,23 +31,31 @@ type OlmMachine struct { CryptoStore Store StateStore StateStore + AllowUnverifiedDevices bool + account *OlmAccount } +// StateStore is used by OlmMachine to get room state information that's needed for encryption. type StateStore interface { IsEncrypted(id.RoomID) bool FindSharedRooms(id.UserID) []id.RoomID } +// NewOlmMachine creates an OlmMachine with the given client, logger and stores. func NewOlmMachine(client *mautrix.Client, log Logger, cryptoStore Store, stateStore StateStore) *OlmMachine { return &OlmMachine{ Client: client, Log: log, CryptoStore: cryptoStore, StateStore: stateStore, + + AllowUnverifiedDevices: true, } } +// Load loads the Olm account information from the crypto store. If there's no olm account, a new one is created. +// This must be called before using the machine. func (mach *OlmMachine) Load() (err error) { mach.account, err = mach.CryptoStore.GetAccount() if err != nil { @@ -58,19 +69,21 @@ func (mach *OlmMachine) Load() (err error) { return nil } -func (mach *OlmMachine) SaveAccount() { +func (mach *OlmMachine) saveAccount() { err := mach.CryptoStore.PutAccount(mach.account) if err != nil { mach.Log.Error("Failed to save account: %v", err) } } +// FlushStore calls the Flush method of the CryptoStore. func (mach *OlmMachine) FlushStore() error { return mach.CryptoStore.Flush() } +// Fingerprint returns the fingerprint of the Olm account that can be used for non-interactive verification. func (mach *OlmMachine) Fingerprint() string { - signingKey, _ := mach.account.Internal.IdentityKeys() + signingKey := mach.account.SigningKey() spacedSigningKey := make([]byte, len(signingKey)+(len(signingKey)-1)/4) var ptr = 0 for i, chr := range signingKey { @@ -84,6 +97,11 @@ func (mach *OlmMachine) Fingerprint() string { return string(spacedSigningKey) } +// ProcessSyncResponse processes a single /sync response. +// +// This can be easily registered into a mautrix client using .OnSync(): +// +// client.Syncer.(*mautrix.DefaultSyncer).OnSync(c.crypto.ProcessSyncResponse) func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync, since string) { if len(resp.DeviceLists.Changed) > 0 { mach.Log.Trace("Device list changes in /sync: %v", resp.DeviceLists.Changed) @@ -110,6 +128,11 @@ func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync, since string } } +// HandleMemberEvent handles a single membership event. +// +// Currently this is not automatically called, so you must add a listener yourself: +// +// client.Syncer.(*mautrix.DefaultSyncer).OnSync(c.crypto.ProcessSyncResponse) func (mach *OlmMachine) HandleMemberEvent(evt *event.Event) { if !mach.StateStore.IsEncrypted(evt.RoomID) { return @@ -139,6 +162,8 @@ func (mach *OlmMachine) HandleMemberEvent(evt *event.Event) { } } +// HandleToDeviceEvent handles a single to-device event. This is automatically called by ProcessSyncResponse, so you +// don't need to add any custom handlers if you use that method. func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) { switch content := evt.Content.Parsed.(type) { case *event.EncryptedEventContent: @@ -176,7 +201,7 @@ func (mach *OlmMachine) createGroupSession(senderKey id.SenderKey, signingKey id mach.Log.Trace("Created inbound group session %s/%s/%s", roomID, senderKey, sessionID) } -func (mach *OlmMachine) receiveRoomKey(evt *OlmEvent, content *event.RoomKeyEventContent) { +func (mach *OlmMachine) receiveRoomKey(evt *DecryptedOlmEvent, content *event.RoomKeyEventContent) { // TODO nio had a comment saying "handle this better" for the case where evt.Keys.Ed25519 is none? if content.Algorithm != id.AlgorithmMegolmV1 || evt.Keys.Ed25519 == "" { return @@ -185,7 +210,11 @@ func (mach *OlmMachine) receiveRoomKey(evt *OlmEvent, content *event.RoomKeyEven mach.createGroupSession(evt.SenderKey, evt.Keys.Ed25519, content.RoomID, content.SessionID, content.SessionKey) } -// ShareKeys returns a key upload request. +// ShareKeys uploads necessary keys to the server. +// +// If the Olm account hasn't been shared, the account keys will be uploaded. +// If currentOTKCount is less than half of the limit (100 / 2 = 50), enough one-time keys will be uploaded so exactly +// half of the limit is filled. func (mach *OlmMachine) ShareKeys(currentOTKCount int) error { var deviceKeys *mautrix.DeviceKeys if !mach.account.Shared { @@ -207,7 +236,7 @@ func (mach *OlmMachine) ShareKeys(currentOTKCount int) error { return err } mach.account.Shared = true - mach.SaveAccount() + mach.saveAccount() mach.Log.Trace("Shared keys and saved account") return nil } diff --git a/crypto/store.go b/crypto/store.go index 204da48f..629b248d 100644 --- a/crypto/store.go +++ b/crypto/store.go @@ -15,6 +15,7 @@ import ( "maunium.net/go/mautrix/id" ) +// TrustState determines how trusted a device is. type TrustState int const ( @@ -24,6 +25,7 @@ const ( TrustStateIgnored ) +// DeviceIdentity contains the identity details of a device and some additional info. type DeviceIdentity struct { UserID id.UserID DeviceID id.DeviceID @@ -35,31 +37,68 @@ type DeviceIdentity struct { Name string } +// Store is used by OlmMachine to store Olm and Megolm sessions, user device lists and message indices. +// +// General implementation details: +// * Get methods should not return errors if the requested data does not exist in the store, they should simply return nil. +// * Update methods may assume that the pointer is the same as what has earlier been added to or fetched from the store. type Store interface { + // Flush ensures that everything in the store is persisted to disk. + // This doesn't have to do anything, e.g. for database-backed implementations that persist everything immediately. Flush() error + // PutAccount updates the OlmAccount in the store. PutAccount(*OlmAccount) error + // GetAccount returns the OlmAccount in the store that was previously inserted with PutAccount. GetAccount() (*OlmAccount, error) - HasSession(id.SenderKey) bool - GetSessions(id.SenderKey) (OlmSessionList, error) - GetLatestSession(id.SenderKey) (*OlmSession, error) + // AddSession inserts an Olm session into the store. AddSession(id.SenderKey, *OlmSession) error + // HasSession returns whether or not the store has an Olm session with the given sender key. + HasSession(id.SenderKey) bool + // GetSessions returns all Olm sessions in the store with the given sender key. + GetSessions(id.SenderKey) (OlmSessionList, error) + // GetLatestSession returns the session with the highest session ID (lexiographically sorting). + // It's usually safe to return the most recently added session if sorting by session ID is too difficult. + GetLatestSession(id.SenderKey) (*OlmSession, error) + // UpdateSession updates a session that has previously been inserted with AddSession. UpdateSession(id.SenderKey, *OlmSession) error + // PutGroupSession inserts an inbound Megolm session into the store. PutGroupSession(id.RoomID, id.SenderKey, id.SessionID, *InboundGroupSession) error + // GetGroupSession gets an inbound Megolm session from the store. GetGroupSession(id.RoomID, id.SenderKey, id.SessionID) (*InboundGroupSession, error) + // AddOutboundGroupSession inserts the given outbound Megolm session into the store. + // + // The store should index inserted sessions by the RoomID field to support getting and removing sessions. + // There will only be one outbound session per room ID at a time. AddOutboundGroupSession(*OutboundGroupSession) error + // UpdateOutboundGroupSession updates the given outbound Megolm session in the store. UpdateOutboundGroupSession(*OutboundGroupSession) error + // GetOutboundGroupSession gets the stored outbound Megolm session for the given room ID from the store. GetOutboundGroupSession(id.RoomID) (*OutboundGroupSession, error) + // RemoveOutboundGroupSession removes the stored outbound Megolm session for the given room ID. RemoveOutboundGroupSession(id.RoomID) error + // ValidateMessageIndex validates that the given message details aren't from a replay attack. + // + // Implementations should store a map from (senderKey, sessionID, index) to (eventID, timestamp), then use that map + // to check whether or not the message index is valid: + // + // * If the map key doesn't exist, the given values should be stored and this should return true. + // * If the map key exists and the stored values match the given values, this should return true. + // * If the map key exists, but the stored values do not match the given values, this should return false. ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) bool + // GetDevices returns a map from device ID to DeviceIdentity containing all devices of a given user. GetDevices(id.UserID) (map[id.DeviceID]*DeviceIdentity, error) + // GetDevice returns a specific device of a given user. GetDevice(id.UserID, id.DeviceID) (*DeviceIdentity, error) + // PutDevices overrides the stored device list for the given user with the given list. PutDevices(id.UserID, map[id.DeviceID]*DeviceIdentity) error + // FilterTrackedUsers returns a filtered version of the given list that only includes user IDs whose device lists + // have been stored with PutDevices. A user is considered tracked even if the PutDevices list was empty. FilterTrackedUsers([]id.UserID) []id.UserID } @@ -74,6 +113,7 @@ type messageIndexValue struct { Timestamp int64 } +// GobStore is a simple Store implementation that dumps everything into a .gob file. type GobStore struct { lock sync.RWMutex path string @@ -88,6 +128,7 @@ type GobStore struct { var _ Store = (*GobStore)(nil) +// NewGobStore creates a new GobStore that saves everything to the given file. func NewGobStore(path string) (*GobStore, error) { gs := &GobStore{ path: path, diff --git a/event/content.go b/event/content.go index d7f49c47..ab840837 100644 --- a/event/content.go +++ b/event/content.go @@ -50,6 +50,7 @@ var TypeMap = map[Type]reflect.Type{ ToDeviceForwardedRoomKey: reflect.TypeOf(ForwardedRoomKeyEventContent{}), ToDeviceRoomKeyRequest: reflect.TypeOf(RoomKeyRequestEventContent{}), ToDeviceEncrypted: reflect.TypeOf(EncryptedEventContent{}), + ToDeviceRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}), } // Content stores the content of a Matrix event. @@ -174,6 +175,7 @@ func init() { gob.Register(&RoomKeyEventContent{}) gob.Register(&ForwardedRoomKeyEventContent{}) gob.Register(&RoomKeyRequestEventContent{}) + gob.Register(&RoomKeyWithheldEventContent{}) } // Helper cast functions below @@ -367,3 +369,10 @@ func (content *Content) AsRoomKeyRequest() *RoomKeyRequestEventContent { } return casted } +func (content *Content) AsRoomKeyWithheld() *RoomKeyWithheldEventContent { + casted, ok := content.Parsed.(*RoomKeyWithheldEventContent) + if !ok { + return &RoomKeyWithheldEventContent{} + } + return casted +} diff --git a/event/encryption.go b/event/encryption.go index 1575231f..4920ba12 100644 --- a/event/encryption.go +++ b/event/encryption.go @@ -120,3 +120,22 @@ type RequestedKeyInfo struct { SenderKey id.SenderKey `json:"sender_key"` SessionID id.SessionID `json:"session_id"` } + +type RoomKeyWithheldCode string + +const ( + RoomKeyWithheldBlacklisted RoomKeyWithheldCode = "m.blacklisted" + RoomKeyWithheldUnverified RoomKeyWithheldCode = "m.unverified" + RoomKeyWithheldUnauthorized RoomKeyWithheldCode = "m.unauthorized" + RoomKeyWithheldUnavailable RoomKeyWithheldCode = "m.unavailable" + RoomKeyWithheldNoOlmSession RoomKeyWithheldCode = "m.no_olm" +) + +type RoomKeyWithheldEventContent struct { + RoomID id.RoomID `json:"room_id,omitempty"` + Algorithm id.Algorithm `json:"algorithm"` + SessionID id.SessionID `json:"session_id,omitempty"` + SenderKey id.SenderKey `json:"sender_key"` + Code RoomKeyWithheldCode `json:"code"` + Reason string `json:"reason,omitempty"` +} diff --git a/event/type.go b/event/type.go index 2191987a..5700afb1 100644 --- a/event/type.go +++ b/event/type.go @@ -89,7 +89,7 @@ func (et *Type) GuessClass() TypeClass { return AccountDataEventType case EventRedaction.Type, EventMessage.Type, EventEncrypted.Type, EventReaction.Type, EventSticker.Type: return MessageEventType - case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type: + case ToDeviceRoomKey.Type, ToDeviceRoomKeyRequest.Type, ToDeviceForwardedRoomKey.Type, ToDeviceRoomKeyWithheld.Type: return ToDeviceEventType default: return UnknownEventType @@ -176,4 +176,5 @@ var ( ToDeviceRoomKeyRequest = Type{"m.room_key_request", ToDeviceEventType} ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType} ToDeviceEncrypted = Type{"m.room.encrypted", ToDeviceEventType} + ToDeviceRoomKeyWithheld = Type{"m.room_key.withheld", ToDeviceEventType} ) diff --git a/version.go b/version.go index 7c864d3f..bf19fea9 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package mautrix -const Version = "v0.5.2" +const Version = "v0.5.3"