Add initial outbound encryption

This commit is contained in:
Tulir Asokan 2020-04-28 00:57:04 +03:00
commit fe82e2b914
22 changed files with 599 additions and 77 deletions

View file

@ -752,7 +752,7 @@ func (cli *Client) UserTyping(roomID id.RoomID, typing bool, timeout int64) (res
return
}
func (cli *Client) SetPresence(status string) (err error) {
func (cli *Client) SetPresence(status event.Presence) (err error) {
req := ReqPresence{Presence: status}
u := cli.BuildURL("presence", cli.UserID, "status")
_, err = cli.MakeRequest("PUT", u, req, nil)
@ -998,6 +998,12 @@ func (cli *Client) GetKeyChanges(from, to string) (resp *RespKeyChanges, err err
return
}
func (cli *Client) SendToDevice(eventType event.Type, req *ReqSendToDevice) (resp *RespSendToDevice, err error) {
urlPath := cli.BuildURL("sendToDevice", eventType.String(), cli.TxnID())
_, err = cli.MakeRequest("PUT", urlPath, req, &resp)
return
}
// GetPushRules returns the push notification rules for the global scope.
func (cli *Client) GetPushRules() (*pushrules.PushRuleset, error) {
return cli.GetScopedPushRules("global")

View file

@ -48,7 +48,7 @@ func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID
// TODO do we need unsigned curve25519 one-time keys at all?
// this just signs all of them
for keyID, key := range account.OneTimeKeys() {
key := mautrix.OneTimeKey{Key: string(key)}
key := mautrix.OneTimeKey{Key: key}
signature, _ := account.SignJSON(key)
key.Signatures = mautrix.Signatures{
userID: {

View file

@ -19,9 +19,11 @@ 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 message index")
WrongRoom = errors.New("encrypted megolm event is not intended for this room")
)
type MegolmEvent struct {
RoomID id.RoomID `json:"room_id"`
Type event.Type `json:"type"`
Content event.Content `json:"content"`
}
@ -52,6 +54,8 @@ func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, erro
err = json.Unmarshal(plaintext, &megolmEvt)
if err != nil {
return nil, errors.Wrap(err, "failed to parse megolm payload")
} else if megolmEvt.RoomID != evt.RoomID {
return nil, WrongRoom
}
err = megolmEvt.Content.ParseRaw(megolmEvt.Type)
if err != nil && !event.IsUnsupportedContentType(err) {

View file

@ -41,7 +41,7 @@ type OlmEvent struct {
Content event.Content `json:"content"`
}
func (mach *OlmMachine) DecryptOlmEvent(evt *event.Event) (*OlmEvent, error) {
func (mach *OlmMachine) decryptOlmEvent(evt *event.Event) (*OlmEvent, error) {
content, ok := evt.Content.Parsed.(*event.EncryptedEventContent)
if !ok {
return nil, IncorrectEncryptedContentType
@ -49,27 +49,32 @@ func (mach *OlmMachine) DecryptOlmEvent(evt *event.Event) (*OlmEvent, error) {
return nil, UnsupportedAlgorithm
}
_, ownKey := mach.account.IdentityKeys()
ownContent, ok := content.OlmCiphertext[string(ownKey)]
ownContent, ok := content.OlmCiphertext[ownKey]
if !ok {
return nil, NotEncryptedForMe
}
return mach.decryptOlmEvent(evt, content.SenderKey, ownContent.Type, ownContent.Body)
decrypted, err := mach.decryptOlmCiphertext(evt.Sender, content.SenderKey, ownContent.Type, ownContent.Body)
if err != nil {
return nil, err
}
decrypted.Source = evt
return decrypted, nil
}
type OlmEventKeys struct {
Ed25519 id.Ed25519 `json:"ed25519"`
}
func (mach *OlmMachine) decryptOlmEvent(evt *event.Event, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*OlmEvent, error) {
func (mach *OlmMachine) decryptOlmCiphertext(sender id.UserID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*OlmEvent, error) {
if olmType != id.OlmMsgTypePreKey && olmType != id.OlmMsgTypeMsg {
return nil, UnsupportedOlmMessageType
}
plaintext, err := mach.tryDecryptOlmEvent(senderKey, olmType, ciphertext)
plaintext, err := mach.tryDecryptOlmCiphertext(senderKey, olmType, ciphertext)
if err != nil {
if err == DecryptionFailedWithMatchingSession {
mach.log.Warn("Found matching session yet decryption failed for sender %s with key %s", evt.Sender, senderKey)
mach.markDeviceForUnwedging(evt.Sender, senderKey)
mach.log.Warn("Found matching session yet decryption failed for sender %s with key %s", sender, senderKey)
mach.markDeviceForUnwedging(sender, senderKey)
}
return nil, errors.Wrap(err, "failed to decrypt olm event")
}
@ -79,13 +84,13 @@ func (mach *OlmMachine) decryptOlmEvent(evt *event.Event, senderKey id.SenderKey
// New sessions can only be created if it's a prekey message, we can't decrypt the message
// if it isn't one at this point in time anymore, so return early.
if olmType != id.OlmMsgTypePreKey {
mach.markDeviceForUnwedging(evt.Sender, senderKey)
mach.markDeviceForUnwedging(sender, senderKey)
return nil, DecryptionFailedForNormalMessage
}
session, err := mach.createInboundSession(senderKey, ciphertext)
if err != nil {
mach.markDeviceForUnwedging(evt.Sender, senderKey)
mach.markDeviceForUnwedging(sender, senderKey)
return nil, errors.Wrap(err, "failed to create new session from prekey message")
}
@ -100,7 +105,7 @@ func (mach *OlmMachine) decryptOlmEvent(evt *event.Event, senderKey id.SenderKey
if err != nil {
return nil, errors.Wrap(err, "failed to parse olm payload")
}
if evt.Sender != olmEvt.Sender {
if sender != olmEvt.Sender {
return nil, SenderMismatch
} else if mach.client.UserID != olmEvt.Recipient {
return nil, RecipientMismatch
@ -113,13 +118,12 @@ func (mach *OlmMachine) decryptOlmEvent(evt *event.Event, senderKey id.SenderKey
return nil, errors.Wrap(err, "failed to parse content of olm payload event")
}
olmEvt.Source = evt
olmEvt.SenderKey = senderKey
return &olmEvt, nil
}
func (mach *OlmMachine) tryDecryptOlmEvent(senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) ([]byte, error) {
func (mach *OlmMachine) tryDecryptOlmCiphertext(senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) ([]byte, error) {
sessions, err := mach.store.GetSessions(senderKey)
if err != nil {
return nil, errors.Wrapf(err, "failed to get session for %s", senderKey)

116
crypto/devicelist.go Normal file
View file

@ -0,0 +1,116 @@
// 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 (
"github.com/pkg/errors"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/id"
)
func (mach *OlmMachine) FetchKeys(users []id.UserID, sinceToken string) {
req := &mautrix.ReqQueryKeys{
DeviceKeys: mautrix.DeviceKeysRequest{},
Timeout: 10 * 1000,
Token: sinceToken,
}
for _, userID := range users {
req.DeviceKeys[userID] = mautrix.DeviceIDList{}
}
mach.log.Trace("Querying keys for %v", users)
resp, err := mach.client.QueryKeys(req)
if err != nil {
mach.log.Warn("Failed to query keys: %v", err)
return
}
for server, err := range resp.Failures {
mach.log.Warn("Query keys failure for %s: %v", server, err)
}
mach.log.Trace("Query key result received with %d users", len(resp.DeviceKeys))
for userID, devices := range resp.DeviceKeys {
delete(req.DeviceKeys, userID)
newDevices := make(map[id.DeviceID]*DeviceIdentity)
existingDevices, err := mach.store.GetDevices(userID)
if err != nil {
mach.log.Warn("Failed to get existing devices for %s: %v", userID, err)
existingDevices = make(map[id.DeviceID]*DeviceIdentity)
}
mach.log.Trace("Updating devices for %s, got %d devices, have %d in store", userID, len(devices), len(existingDevices))
for deviceID, deviceKeys := range devices {
existing := existingDevices[deviceID]
mach.log.Trace("Validating device %s of %s", deviceID, userID)
newDevice, err := mach.validateDevice(userID, deviceID, deviceKeys, existing)
if err != nil {
mach.log.Error("Failed to validate device %s of %s: %v", deviceID, userID, err)
} else if newDevice != nil {
newDevices[deviceID] = newDevice
}
}
mach.log.Trace("Storing new device list for %s containing %d devices", userID, len(newDevices))
err = mach.store.PutDevices(userID, newDevices)
if err != nil {
mach.log.Warn("Failed to update device list for %s: %v", userID, err)
}
}
for userID := range req.DeviceKeys {
mach.log.Warn("Didn't get any keys for user %s", userID)
}
}
var (
MismatchingDeviceID = errors.New("mismatching device ID in parameter and keys object")
MismatchingUserID = errors.New("mismatching user ID in parameter and keys object")
MismatchingSigningKey = errors.New("received update for device with different signing key")
NoSigningKeyFound = errors.New("didn't find ed25519 signing key")
NoIdentityKeyFound = errors.New("didn't find curve25519 identity key")
InvalidKeySignature = errors.New("invalid signature on device keys")
)
func (mach *OlmMachine) validateDevice(userID id.UserID, deviceID id.DeviceID, deviceKeys mautrix.DeviceKeys, existing *DeviceIdentity) (*DeviceIdentity, error) {
if deviceID != deviceKeys.DeviceID {
return nil, MismatchingDeviceID
} else if userID != deviceKeys.UserID {
return nil, MismatchingUserID
}
signingKey := deviceKeys.Keys.GetEd25519(deviceID)
identityKey := deviceKeys.Keys.GetCurve25519(deviceID)
if signingKey == "" {
return nil, NoSigningKeyFound
} else if identityKey == "" {
return nil, NoIdentityKeyFound
}
if existing != nil && existing.SigningKey != signingKey {
return existing, MismatchingSigningKey
}
ok, err := olm.VerifySignatureJSON(deviceKeys, userID, deviceID, signingKey)
if err != nil {
return existing, errors.Wrap(err, "failed to verify signature")
} else if !ok {
return existing, InvalidKeySignature
}
name, ok := deviceKeys.Unsigned["device_display_name"].(string)
if !ok {
name = string(deviceID)
}
return &DeviceIdentity{
UserID: userID,
DeviceID: deviceID,
IdentityKey: identityKey,
SigningKey: signingKey,
Trust: TrustStateUnset,
Name: name,
Deleted: false,
}, nil
}

136
crypto/encryptmegolm.go Normal file
View file

@ -0,0 +1,136 @@
// 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"
"github.com/pkg/errors"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var (
AlreadyShared = errors.New("group session already shared")
NoGroupSession = errors.New("no group session created")
)
func (mach *OlmMachine) EncryptMegolmEvent(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) {
session, err := mach.store.GetOutboundGroupSession(roomID)
if err != nil {
return nil, errors.Wrap(err, "failed to get outbound group session")
} else if session == nil {
return nil, NoGroupSession
}
plaintext, err := json.Marshal(&MegolmEvent{
RoomID: roomID,
Type: evtType,
Content: content,
})
if err != nil {
return nil, err
}
ciphertext, err := session.Encrypt(plaintext)
if err != nil {
return nil, err
}
_, idKey := mach.account.IdentityKeys()
return &event.EncryptedEventContent{
Algorithm: id.AlgorithmMegolmV1,
SenderKey: idKey,
DeviceID: mach.client.DeviceID,
SessionID: session.ID(),
MegolmCiphertext: ciphertext,
}, nil
}
func (mach *OlmMachine) newOutboundGroupSession(roomID id.RoomID) *OutboundGroupSession {
session := NewOutboundGroupSession()
signingKey, idKey := mach.account.IdentityKeys()
mach.createGroupSession(idKey, signingKey, roomID, session.ID(), session.Key())
return session
}
func (mach *OlmMachine) ShareGroupSession(roomID id.RoomID, users []id.UserID) error {
mach.log.Trace("Sharing group session for room %s", roomID)
session, err := mach.store.GetOutboundGroupSession(roomID)
if err != nil {
return errors.Wrap(err, "failed to get previous outbound group session")
}
if session == nil || session.Expired() {
session = mach.newOutboundGroupSession(roomID)
} else if session.Shared {
return AlreadyShared
}
keyContent := event.Content{Parsed: &event.RoomKeyEventContent{
Algorithm: id.AlgorithmMegolmV1,
RoomID: roomID,
SessionID: session.ID(),
SessionKey: session.Key(),
}}
toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)}
for _, userID := range users {
devices, err := mach.store.GetDevices(userID)
if err != nil {
mach.log.Warn("Failed to get devices of %s", userID)
continue
}
if len(devices) == 0 {
mach.FetchKeys([]id.UserID{userID}, "")
devices, err = mach.store.GetDevices(userID)
if err != nil {
mach.log.Warn("Failed to get devices of %s", userID)
continue
}
}
toDeviceMessages := make(map[id.DeviceID]*event.Content)
toDevice.Messages[userID] = toDeviceMessages
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.store.GetLatestSession(device.IdentityKey)
if err != nil {
mach.log.Warn("Failed to get session for %s of %s: %v", deviceID, userID, err)
continue
} else if deviceSession == nil {
// TODO we should probably be creating these sessions somewhere
deviceSession, err = mach.createOutboundSession(userID, deviceID, device.IdentityKey, device.SigningKey)
if err != nil {
mach.log.Warn("Failed to create session for %s of %s: %v", deviceID, userID, err)
continue
}
}
content := mach.encryptOlmEvent(deviceSession, device, event.ToDeviceRoomKey, keyContent)
toDeviceMessages[deviceID] = &event.Content{Parsed: content}
session.Users[userKey] = OGSAlreadyShared
}
}
_, err = mach.client.SendToDevice(event.ToDeviceEncrypted, toDevice)
if err != nil {
return errors.Wrap(err, "failed to share group session")
}
session.Shared = true
return mach.store.PutOutboundGroupSession(roomID, session)
}

89
crypto/encryptolm.go Normal file
View file

@ -0,0 +1,89 @@
// 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"
"github.com/pkg/errors"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
var (
NoOneTimeKeyReceived = errors.New("no one-time key received")
InvalidOTKSignature = errors.New("invalid signature on one-time key")
)
func (mach *OlmMachine) encryptOlmEvent(session *OlmSession, recipient *DeviceIdentity, evtType event.Type, content event.Content) *event.EncryptedEventContent {
selfSigningKey, selfIdentityKey := mach.account.IdentityKeys()
evt := &OlmEvent{
Sender: mach.client.UserID,
SenderDevice: mach.client.DeviceID,
Keys: OlmEventKeys{Ed25519: selfSigningKey},
Recipient: recipient.UserID,
RecipientKeys: OlmEventKeys{Ed25519: recipient.SigningKey},
Type: evtType,
Content: content,
}
plaintext, err := json.Marshal(evt)
if err != nil {
panic(err)
}
msgType, ciphertext := session.Encrypt(plaintext)
return &event.EncryptedEventContent{
Algorithm: id.AlgorithmOlmV1,
SenderKey: selfIdentityKey,
OlmCiphertext: event.OlmCiphertexts{
recipient.IdentityKey: {
Type: msgType,
Body: string(ciphertext),
},
},
}
// TODO this probably needs to be done somewhere
//sess, err := mach.store.GetLatestSession(recipientKey)
//if err != nil {
// return nil, errors.Wrap(err, "failed to get session")
//}
//if sess == nil {
// sess, err = mach.createOutboundSession(recipient, recipientKey)
// if err != nil {
// return nil, errors.Wrap(err, "failed to create session")
// }
//}
}
func (mach *OlmMachine) createOutboundSession(userID id.UserID, deviceID id.DeviceID, identityKey id.Curve25519, signingKey id.Ed25519) (*OlmSession, error) {
resp, err := mach.client.ClaimKeys(&mautrix.ReqClaimKeys{
OneTimeKeys: mautrix.OneTimeKeysRequest{userID: {deviceID: id.KeyAlgorithmSignedCurve25519}},
Timeout: 10 * 1000,
})
if err != nil {
return nil, errors.Wrap(err, "failed to claim keys")
}
deviceKeyID := id.NewDeviceKeyID(id.KeyAlgorithmSignedCurve25519, deviceID)
oneTimeKey, ok := resp.OneTimeKeys[userID][deviceKeyID]
if !ok {
return nil, NoOneTimeKeyReceived
}
ok, err = olm.VerifySignatureJSON(oneTimeKey, userID, deviceID, signingKey)
if err != nil {
return nil, errors.Wrap(err, "failed to verify signature")
} else if !ok {
return nil, InvalidOTKSignature
}
sess, err := mach.account.NewOutboundSession(identityKey, oneTimeKey.Key)
if err != nil {
return nil, err
}
wrapped := wrapSession(sess)
return wrapped, mach.store.AddSession(identityKey, wrapped)
}

View file

@ -57,7 +57,11 @@ func (mach *OlmMachine) SaveAccount() {
}
}
func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync) {
func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync, since string) {
if len(resp.DeviceLists.Changed) > 0 {
mach.FetchKeys(resp.DeviceLists.Changed, since)
}
for _, evt := range resp.ToDevice.Events {
mach.log.Trace("Got to-device event %s from %s", evt.Type.Type, evt.Sender)
evt.Type.Class = event.ToDeviceEventType
@ -82,7 +86,7 @@ func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync) {
func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) {
switch evt.Content.Parsed.(type) {
case *event.EncryptedEventContent:
decryptedEvt, err := mach.DecryptOlmEvent(evt)
decryptedEvt, err := mach.decryptOlmEvent(evt)
if err != nil {
mach.log.Error("Failed to decrypt to-device event: %v", err)
return

View file

@ -183,7 +183,7 @@ func (a *Account) MarshalJSON() ([]byte, error) {
}
func (a *Account) UnmarshalJSON(data []byte) error {
if data[0] != '"' || data[len(data)-1] != '"' {
if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
return InputNotJSONString
}
if a.int == nil {

View file

@ -34,7 +34,7 @@ func InboundGroupSessionFromPickled(pickled, key []byte) (*InboundGroupSession,
}
// NewInboundGroupSession creates a new inbound group session from a key
// exported from OutboundGroupSession.SessionKey(). Returns error on failure.
// exported from OutboundGroupSession.Key(). Returns error on failure.
// If the sessionKey is not valid base64 the error will be
// "OLM_INVALID_BASE64". If the session_key is invalid the error will be
// "OLM_BAD_SESSION_KEY".
@ -173,7 +173,7 @@ func (s *InboundGroupSession) MarshalJSON() ([]byte, error) {
}
func (s *InboundGroupSession) UnmarshalJSON(data []byte) error {
if data[0] != '"' || data[len(data)-1] != '"' {
if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
return InputNotJSONString
}
if s.int == nil {

View file

@ -150,7 +150,7 @@ func (s *OutboundGroupSession) MarshalJSON() ([]byte, error) {
}
func (s *OutboundGroupSession) UnmarshalJSON(data []byte) error {
if data[0] != '"' || data[len(data)-1] != '"' {
if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
return InputNotJSONString
}
if s.int == nil {
@ -219,8 +219,8 @@ func (s *OutboundGroupSession) sessionKeyLen() uint {
return uint(C.olm_outbound_group_session_key_length((*C.OlmOutboundGroupSession)(s.int)))
}
// SessionKey returns the base64-encoded current ratchet key for this session.
func (s *OutboundGroupSession) SessionKey() string {
// Key returns the base64-encoded current ratchet key for this session.
func (s *OutboundGroupSession) Key() string {
sessionKey := make([]byte, s.sessionKeyLen())
r := C.olm_outbound_group_session_key(
(*C.OlmOutboundGroupSession)(s.int),

View file

@ -173,7 +173,7 @@ func (s *Session) MarshalJSON() ([]byte, error) {
}
func (s *Session) UnmarshalJSON(data []byte) error {
if data[0] != '"' || data[len(data)-1] != '"' {
if len(data) == 0 || len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
return InputNotJSONString
}
if s == nil {
@ -275,9 +275,9 @@ func (s *Session) EncryptMsgType() id.OlmMsgType {
// Encrypt encrypts a message using the Session. Returns the encrypted message
// as base64.
func (s *Session) Encrypt(plaintext string) (id.OlmMsgType, string) {
func (s *Session) Encrypt(plaintext []byte) (id.OlmMsgType, []byte) {
if len(plaintext) == 0 {
plaintext = " "
panic(EmptyInput)
}
// Make the slice be at least length 1
random := make([]byte, s.encryptRandomLen()+1)
@ -289,7 +289,7 @@ func (s *Session) Encrypt(plaintext string) (id.OlmMsgType, string) {
message := make([]byte, s.encryptMsgLen(len(plaintext)))
r := C.olm_encrypt(
(*C.OlmSession)(s.int),
unsafe.Pointer(&([]byte(plaintext))[0]),
unsafe.Pointer(&plaintext[0]),
C.size_t(len(plaintext)),
unsafe.Pointer(&random[0]),
C.size_t(len(random)),
@ -298,7 +298,7 @@ func (s *Session) Encrypt(plaintext string) (id.OlmMsgType, string) {
if r == errorVal() {
panic(s.lastError())
}
return messageType, string(message[:r])
return messageType, message[:r]
}
// Decrypt decrypts a message using the Session. Returns the the plain-text on

View file

@ -107,8 +107,11 @@ var gjsonEscaper = strings.NewReplacer(
func gjsonPath(path ...string) string {
var result strings.Builder
for _, part := range path {
for i, part := range path {
_, _ = gjsonEscaper.WriteString(&result, part)
if i < len(path) - 1 {
result.WriteRune('.')
}
}
return result.String()
}
@ -130,6 +133,10 @@ func (u *Utility) VerifySignatureJSON(obj interface{}, userID id.UserID, deviceI
if err != nil {
return false, err
}
objJSON, err = sjson.DeleteBytes(objJSON, "signatures")
if err != nil {
return false, err
}
objJSONString := string(canonicaljson.CanonicalJSONAssumeValid(objJSON))
return u.VerifySignature(objJSONString, key, sig.Str)
}

View file

@ -7,6 +7,7 @@
package crypto
import (
"strings"
"time"
"github.com/pkg/errors"
@ -21,14 +22,33 @@ var (
SessionExpired = errors.New("session has expired")
)
type UserDevice struct {
UserID id.UserID
DeviceID id.DeviceID
// OlmSessionList is a list of OlmSessions. It implements sort.Interface in a way that sorts items
// in reverse alphabetic order, which means the newest session is first.
type OlmSessionList []*OlmSession
func (o OlmSessionList) Len() int {
return len(o)
}
func (o OlmSessionList) Less(i, j int) bool {
return strings.Compare(string(o[i].ID()), string(o[j].ID())) > 0
}
func (o OlmSessionList) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
type OlmSession struct {
olm.Session
ExpirationMixin
id id.SessionID
}
func (session *OlmSession) ID() id.SessionID {
if session.id == "" {
session.id = session.Session.ID()
}
return session.id
}
func wrapSession(session *olm.Session) *OlmSession {
@ -53,7 +73,7 @@ func (account *OlmAccount) NewInboundSessionFrom(senderKey id.Curve25519, cipher
return wrapSession(session), nil
}
func (session *OlmSession) Encrypt(plaintext string) (id.OlmMsgType, string) {
func (session *OlmSession) Encrypt(plaintext []byte) (id.OlmMsgType, []byte) {
session.UseTime = time.Now()
return session.Session.Encrypt(plaintext)
}
@ -87,6 +107,19 @@ func NewInboundGroupSession(senderKey id.SenderKey, signingKey id.Ed25519, roomI
}, nil
}
type OGSState int
const (
OGSNotShared OGSState = iota
OGSAlreadyShared
OGSIgnored
)
type UserDevice struct {
UserID id.UserID
DeviceID id.DeviceID
}
type OutboundGroupSession struct {
olm.OutboundGroupSession
@ -94,9 +127,25 @@ type OutboundGroupSession struct {
MaxMessages int
MessageCount int
UsersSharedWith []UserDevice
UsersIgnored []UserDevice
Shared bool
Users map[UserDevice]OGSState
Shared bool
}
func NewOutboundGroupSession() *OutboundGroupSession {
return &OutboundGroupSession{
OutboundGroupSession: *olm.NewOutboundGroupSession(),
ExpirationMixin: ExpirationMixin{
TimeMixin: TimeMixin{
CreationTime: time.Now(),
UseTime: time.Now(),
},
MaxAge: 7 * 24 * time.Hour,
},
// TODO take MaxMessages and MaxAge from the m.room.create event
MaxMessages: 100,
Shared: false,
Users: make(map[UserDevice]OGSState),
}
}
func (ogs *OutboundGroupSession) Expired() bool {

View file

@ -9,22 +9,50 @@ package crypto
import (
"encoding/gob"
"os"
"sort"
"sync"
"maunium.net/go/mautrix/id"
)
type TrustState int
const (
TrustStateUnset TrustState = iota
TrustStateVerified
TrustStateBlacklisted
TrustStateIgnored
)
type DeviceIdentity struct {
UserID id.UserID
DeviceID id.DeviceID
IdentityKey id.Curve25519
SigningKey id.Ed25519
Trust TrustState
Deleted bool
Name string
}
type Store interface {
PutAccount(*OlmAccount) error
GetAccount() (*OlmAccount, error)
GetSessions(id.SenderKey) ([]*OlmSession, error)
GetSessions(id.SenderKey) (OlmSessionList, error)
GetLatestSession(id.SenderKey) (*OlmSession, error)
AddSession(id.SenderKey, *OlmSession) error
PutGroupSession(id.RoomID, id.SenderKey, id.SessionID, *InboundGroupSession) error
GetGroupSession(id.RoomID, id.SenderKey, id.SessionID) (*InboundGroupSession, error)
PutOutboundGroupSession(id.RoomID, *OutboundGroupSession) error
GetOutboundGroupSession(id.RoomID) (*OutboundGroupSession, error)
ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) bool
GetDevices(id.UserID) (map[id.DeviceID]*DeviceIdentity, error)
PutDevices(id.UserID, map[id.DeviceID]*DeviceIdentity) error
}
type messageIndexKey struct {
@ -39,21 +67,27 @@ type messageIndexValue struct {
}
type GobStore struct {
lock sync.Mutex
lock sync.RWMutex
path string
Account *OlmAccount
Sessions map[id.SenderKey][]*OlmSession
GroupSessions map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession
MessageIndices map[messageIndexKey]messageIndexValue
Account *OlmAccount
Sessions map[id.SenderKey]OlmSessionList
GroupSessions map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession
OutGroupSessions map[id.RoomID]*OutboundGroupSession
MessageIndices map[messageIndexKey]messageIndexValue
Devices map[id.UserID]map[id.DeviceID]*DeviceIdentity
}
var _ Store = &GobStore{}
func NewGobStore(path string) (*GobStore, error) {
gs := &GobStore{
path: path,
Sessions: make(map[id.SenderKey][]*OlmSession),
GroupSessions: make(map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession),
MessageIndices: make(map[messageIndexKey]messageIndexValue),
path: path,
Sessions: make(map[id.SenderKey]OlmSessionList),
GroupSessions: make(map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession),
OutGroupSessions: make(map[id.RoomID]*OutboundGroupSession),
MessageIndices: make(map[messageIndexKey]messageIndexValue),
Devices: make(map[id.UserID]map[id.DeviceID]*DeviceIdentity),
}
return gs, gs.load()
}
@ -93,7 +127,7 @@ func (gs *GobStore) PutAccount(account *OlmAccount) error {
return err
}
func (gs *GobStore) GetSessions(senderKey id.SenderKey) ([]*OlmSession, error) {
func (gs *GobStore) GetSessions(senderKey id.SenderKey) (OlmSessionList, error) {
gs.lock.Lock()
sessions, ok := gs.Sessions[senderKey]
if !ok {
@ -108,11 +142,22 @@ func (gs *GobStore) AddSession(senderKey id.SenderKey, session *OlmSession) erro
gs.lock.Lock()
sessions, _ := gs.Sessions[senderKey]
gs.Sessions[senderKey] = append(sessions, session)
sort.Sort(gs.Sessions[senderKey])
err := gs.save()
gs.lock.Unlock()
return err
}
func (gs *GobStore) GetLatestSession(senderKey id.SenderKey) (*OlmSession, error) {
gs.lock.Lock()
sessions, ok := gs.Sessions[senderKey]
gs.lock.Unlock()
if !ok || len(sessions) == 0 {
return nil, nil
}
return sessions[0], nil
}
func (gs *GobStore) getGroupSessions(roomID id.RoomID, senderKey id.SenderKey) map[id.SessionID]*InboundGroupSession {
room, ok := gs.GroupSessions[roomID]
if !ok {
@ -146,6 +191,24 @@ func (gs *GobStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, se
return session, nil
}
func (gs *GobStore) PutOutboundGroupSession(roomID id.RoomID, session *OutboundGroupSession) error {
gs.lock.Lock()
gs.OutGroupSessions[roomID] = session
err := gs.save()
gs.lock.Unlock()
return err
}
func (gs *GobStore) GetOutboundGroupSession(roomID id.RoomID) (*OutboundGroupSession, error) {
gs.lock.RLock()
session, ok := gs.OutGroupSessions[roomID]
gs.lock.RUnlock()
if !ok {
return nil, nil
}
return session, nil
}
func (gs *GobStore) ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) bool {
gs.lock.Lock()
defer gs.lock.Unlock()
@ -168,3 +231,21 @@ func (gs *GobStore) ValidateMessageIndex(senderKey id.SenderKey, sessionID id.Se
}
return true
}
func (gs *GobStore) GetDevices(userID id.UserID) (map[id.DeviceID]*DeviceIdentity, error) {
gs.lock.RLock()
devices, ok := gs.Devices[userID]
if !ok {
devices = make(map[id.DeviceID]*DeviceIdentity)
}
gs.lock.RUnlock()
return devices, nil
}
func (gs *GobStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*DeviceIdentity) error {
gs.lock.Lock()
gs.Devices[userID] = devices
err := gs.save()
gs.lock.Unlock()
return err
}

View file

@ -28,12 +28,6 @@ type Tag struct {
// https://matrix.org/docs/spec/client_server/r0.6.0#m-direct
type DirectChatsEventContent map[id.UserID][]id.RoomID
// PushRulesEventContent represents the content of a m.push_rules account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-push-rules
//type PushRulesEventContent struct {
// Global *pushrules.PushRuleset `json:"global"`
//}
// FullyReadEventContent represents the content of a m.fully_read account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-fully-read
type FullyReadEventContent struct {

View file

@ -31,6 +31,7 @@ var TypeMap = map[Type]reflect.Type{
StateHistoryVisibility: reflect.TypeOf(HistoryVisibilityEventContent{}),
StateGuestAccess: reflect.TypeOf(GuestAccessEventContent{}),
StatePinnedEvents: reflect.TypeOf(PinnedEventsEventContent{}),
StateEncryption: reflect.TypeOf(EncryptionEventContent{}),
EventMessage: reflect.TypeOf(MessageEventContent{}),
EventSticker: reflect.TypeOf(MessageEventContent{}),
@ -42,7 +43,6 @@ var TypeMap = map[Type]reflect.Type{
AccountDataDirectChats: reflect.TypeOf(DirectChatsEventContent{}),
AccountDataFullyRead: reflect.TypeOf(FullyReadEventContent{}),
AccountDataIgnoredUserList: reflect.TypeOf(IgnoredUserListEventContent{}),
//AccountDataPushRules: reflect.TypeOf(PushRulesEventContent{}),
EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}),
EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}),
@ -75,7 +75,7 @@ func (content *Content) MarshalJSON() ([]byte, error) {
if content.Raw == nil {
if content.Parsed == nil {
if content.VeryRaw == nil {
return []byte("null"), nil
return []byte("{}"), nil
}
return content.VeryRaw, nil
}

View file

@ -29,15 +29,15 @@ type EncryptionEventContent struct {
type EncryptedEventContent struct {
Algorithm id.Algorithm `json:"algorithm"`
SenderKey id.SenderKey `json:"sender_key"`
DeviceID id.DeviceID `json:"device_id"`
SessionID id.SessionID `json:"session_id"`
DeviceID id.DeviceID `json:"device_id,omitempty"`
SessionID id.SessionID `json:"session_id,omitempty"`
Ciphertext json.RawMessage `json:"ciphertext"`
MegolmCiphertext []byte `json:"-"`
OlmCiphertext OlmCiphertexts `json:"-"`
}
type OlmCiphertexts map[string]struct {
type OlmCiphertexts map[id.Curve25519]struct {
Body string `json:"body"`
Type id.OlmMsgType `json:"type"`
}
@ -54,10 +54,10 @@ func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error {
content.OlmCiphertext = make(OlmCiphertexts)
return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext)
case id.AlgorithmMegolmV1:
if content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' {
if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' {
return olm.InputNotJSONString
}
content.MegolmCiphertext = content.Ciphertext[1:len(content.Ciphertext)-1]
content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1]
}
return nil
}
@ -68,9 +68,9 @@ func (content *EncryptedEventContent) MarshalJSON() ([]byte, error) {
case id.AlgorithmOlmV1:
content.Ciphertext, err = json.Marshal(content.OlmCiphertext)
case id.AlgorithmMegolmV1:
content.Ciphertext = make([]byte, len(content.MegolmCiphertext) + 2)
content.Ciphertext = make([]byte, len(content.MegolmCiphertext)+2)
content.Ciphertext[0] = '"'
content.Ciphertext[len(content.Ciphertext) - 1] = '"'
content.Ciphertext[len(content.Ciphertext)-1] = '"'
copy(content.Ciphertext[1:len(content.Ciphertext)-1], content.MegolmCiphertext)
}
if err != nil {

View file

@ -30,9 +30,9 @@ const (
type KeyAlgorithm string
const (
KeyAlgorithmCurve25519 = "curve25519"
KeyAlgorithmEd25519 = "ed25519"
KeyAlgorithmSignedCurve25519 = "signed_curve25519"
KeyAlgorithmCurve25519 KeyAlgorithm = "curve25519"
KeyAlgorithmEd25519 KeyAlgorithm = "ed25519"
KeyAlgorithmSignedCurve25519 KeyAlgorithm = "signed_curve25519"
)
// A SessionID is an arbitrary string that identifies an Olm or Megolm session.

View file

@ -14,6 +14,8 @@ import (
"maunium.net/go/mautrix/event"
)
// EventContent represents the content of a m.push_rules account data event.
// https://matrix.org/docs/spec/client_server/r0.6.0#m-push-rules
type EventContent struct {
Ruleset *PushRuleset `json:"global"`
}

View file

@ -102,7 +102,7 @@ type ReqTyping struct {
}
type ReqPresence struct {
Presence string `json:"presence"`
Presence event.Presence `json:"presence"`
}
type ReqAliasCreate struct {
@ -110,7 +110,7 @@ type ReqAliasCreate struct {
}
type OneTimeKey struct {
Key string `json:"key"`
Key id.Curve25519 `json:"key"`
IsSigned bool `json:"-"`
Signatures Signatures `json:"signatures,omitempty"`
Unsigned map[string]interface{} `json:"unsigned,omitempty"`
@ -121,7 +121,7 @@ type serializableOTK OneTimeKey
func (otk *OneTimeKey) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, (*serializableOTK)(otk))
if err != nil {
var key string
var key id.Curve25519
err := json.Unmarshal(data, &key)
if err != nil {
return err
@ -150,25 +150,53 @@ type ReqUploadKeys struct {
}
type DeviceKeys struct {
UserID id.UserID `json:"user_id"`
DeviceID id.DeviceID `json:"device_id"`
Algorithms []id.Algorithm `json:"algorithms"`
Keys map[id.DeviceKeyID]string `json:"keys"`
Signatures Signatures `json:"signatures"`
Unsigned map[string]interface{} `json:"unsigned,omitempty"`
UserID id.UserID `json:"user_id"`
DeviceID id.DeviceID `json:"device_id"`
Algorithms []id.Algorithm `json:"algorithms"`
Keys KeyMap `json:"keys"`
Signatures Signatures `json:"signatures"`
Unsigned map[string]interface{} `json:"unsigned,omitempty"`
}
type KeyMap map[id.DeviceKeyID]string
func (km KeyMap) GetEd25519(deviceID id.DeviceID) id.Ed25519 {
val, ok := km[id.NewDeviceKeyID(id.KeyAlgorithmEd25519, deviceID)]
if !ok {
return ""
}
return id.Ed25519(val)
}
func (km KeyMap) GetCurve25519(deviceID id.DeviceID) id.Curve25519 {
val, ok := km[id.NewDeviceKeyID(id.KeyAlgorithmCurve25519, deviceID)]
if !ok {
return ""
}
return id.Curve25519(val)
}
type Signatures map[id.UserID]map[id.DeviceKeyID]string
type ReqQueryKeys struct {
DeviceKeys map[id.UserID][]id.DeviceID `json:"device_keys"`
DeviceKeys DeviceKeysRequest `json:"device_keys"`
Timeout int64 `json:"timeout,omitempty"`
Token string `json:"token,omitempty"`
}
type DeviceKeysRequest map[id.UserID]DeviceIDList
type DeviceIDList []id.DeviceID
type ReqClaimKeys struct {
OneTimeKeys map[id.UserID]map[id.DeviceID]string `json:"one_time_keys"`
OneTimeKeys OneTimeKeysRequest `json:"one_time_keys"`
Timeout int64 `json:"timeout,omitempty"`
}
type OneTimeKeysRequest map[id.UserID]map[id.DeviceID]id.KeyAlgorithm
type ReqSendToDevice struct {
Messages map[id.UserID]map[id.DeviceID]*event.Content `json:"messages"`
}

View file

@ -240,12 +240,12 @@ type RespUploadKeys struct {
}
type RespQueryKeys struct {
Failures map[string]map[string]interface{} `json:"failures"`
Failures map[string]interface{} `json:"failures"`
DeviceKeys map[id.UserID]map[id.DeviceID]DeviceKeys `json:"device_keys"`
}
type RespClaimKeys struct {
Failures map[string]map[string]interface{} `json:"failures"`
Failures map[string]interface{} `json:"failures"`
OneTimeKeys map[id.UserID]map[id.DeviceKeyID]OneTimeKey `json:"one_time_keys"`
}
@ -253,3 +253,5 @@ type RespKeyChanges struct {
Changed []id.UserID `json:"changed"`
Left []id.UserID `json:"left"`
}
type RespSendToDevice struct{}