Add more stuff

This commit is contained in:
Tulir Asokan 2020-04-21 02:58:20 +03:00
commit ffc8d4de8f
8 changed files with 494 additions and 81 deletions

63
crypto/account.go Normal file
View file

@ -0,0 +1,63 @@
// 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 (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/olm"
)
type OlmAccount struct {
*olm.Account
Shared bool
}
func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID) *mautrix.DeviceKeys {
ed, curve := account.IdentityKeys()
deviceKeys := &mautrix.DeviceKeys{
UserID: userID,
DeviceID: deviceID,
Algorithms: []string{string(olm.AlgorithmMegolmV1)},
Keys: map[id.DeviceKeyID]string{
id.NewDeviceKeyID("curve25519", deviceID): string(curve),
id.NewDeviceKeyID("ed25519", deviceID): string(ed),
},
}
signature, err := account.SignJSON(deviceKeys)
if err != nil {
panic(err)
}
deviceKeys.Signatures = mautrix.Signatures{
userID: {
id.NewDeviceKeyID("ed25519", deviceID): signature,
},
}
return deviceKeys
}
func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID) map[id.KeyID]mautrix.OneTimeKey {
account.GenOneTimeKeys(account.MaxNumberOfOneTimeKeys() / 3 * 2)
oneTimeKeys := make(map[id.KeyID]mautrix.OneTimeKey)
// TODO do we need unsigned curve25519 one-time keys at all?
// this just signs all of them
for keyID, key := range account.OneTimeKeys().Curve25519 {
key := mautrix.OneTimeKey{Key: string(key)}
signature, _ := account.SignJSON(key)
key.Signatures = mautrix.Signatures{
userID: {
id.NewDeviceKeyID("ed25519", deviceID): signature,
},
}
key.IsSigned = true
oneTimeKeys[id.NewKeyID("signed_curve25519", keyID)] = key
}
account.MarkKeysAsPublished()
return oneTimeKeys
}

View file

@ -7,83 +7,159 @@
package crypto
import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
"maunium.net/go/olm"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
)
type Logger interface {
Debugfln(message string, args ...interface{})
}
type OlmMachine struct {
UserID id.UserID
DeviceID id.DeviceID
Store Store
client *mautrix.Client
store Store
account *olm.Account
account *OlmAccount
sessions map[string][]*OlmSession
groupSessions map[id.RoomID]map[string]map[string]*InboundGroupSession
log Logger
}
func NewOlmMachine(userID id.UserID, deviceID id.DeviceID, store Store) *OlmMachine {
func NewOlmMachine(client *mautrix.Client, store Store) *OlmMachine {
return &OlmMachine{
UserID: userID,
DeviceID: deviceID,
Store: store,
client: client,
store: store,
}
}
func (mach *OlmMachine) Load() {
mach.account = mach.Store.LoadAccount()
func (mach *OlmMachine) Load() (err error) {
mach.account, err = mach.store.LoadAccount()
if err != nil {
return
}
if mach.account == nil {
mach.account = olm.NewAccount()
mach.account = &OlmAccount{
Account: olm.NewAccount(),
}
}
return nil
}
func (mach *OlmMachine) SaveAccount() {
err := mach.store.SaveAccount(mach.account)
if err != nil {
mach.log.Debugfln("Failed to save account: %v", err)
}
}
// NewOneTimeKeys generates new one-time keys and returns a key upload request.
// If no new one-time keys are needed, this returns nil. In that case, the upload request should not be made.
func (mach *OlmMachine) NewOneTimeKeys() *mautrix.ReqUploadKeys {
otks := mach.getOneTimeKeys()
if len(otks) == 0 {
func (mach *OlmMachine) GetSessions(senderKey string) []*OlmSession {
sessions, ok := mach.sessions[senderKey]
if !ok {
sessions, err := mach.store.LoadSessions(senderKey)
if err != nil {
mach.log.Debugfln("Failed to load sessions for %s: %v", senderKey, err)
sessions = make([]*OlmSession, 0)
}
mach.sessions[senderKey] = sessions
}
return sessions
}
func (mach *OlmMachine) SaveSession(senderKey string, session *OlmSession) {
mach.sessions[senderKey] = append(mach.sessions[senderKey], session)
err := mach.store.SaveSessions(senderKey, mach.sessions[senderKey])
if err != nil {
mach.log.Debugfln("Failed to save sessions for %s: %v", senderKey, err)
}
}
func (mach *OlmMachine) ProcessSyncResponse(resp *mautrix.RespSync) {
for _, evt := range resp.ToDevice.Events {
evt.Type.Class = event.ToDeviceEventType
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
continue
}
mach.HandleToDeviceEvent(evt)
}
min := mach.account.MaxNumberOfOneTimeKeys() / 2
if resp.DeviceOneTimeKeysCount.SignedCurve25519 <= int(min) {
err := mach.ShareKeys()
if err != nil {
mach.log.Debugfln("Failed to share keys: %v", err)
}
}
}
func (mach *OlmMachine) HandleToDeviceEvent(evt *event.Event) {
switch evt.Content.Parsed.(type) {
case *event.EncryptedEventContent:
decryptedEvt, err := mach.DecryptOlmEvent(evt)
if err != nil {
mach.log.Debugfln("Failed to decrypt to-device event:", err)
return
}
switch content := decryptedEvt.Content.Parsed.(type) {
case *event.RoomKeyEventContent:
mach.receiveRoomKey(decryptedEvt, content)
}
// TODO unencrypted to-device events should be handled here. At least m.room_key_request and m.verification.start
}
}
func (mach *OlmMachine) getGroupSessions(roomID id.RoomID, senderKey string) map[string]*InboundGroupSession {
roomGroupSessions, ok := mach.groupSessions[roomID]
if !ok {
roomGroupSessions = make(map[string]map[string]*InboundGroupSession)
mach.groupSessions[roomID] = roomGroupSessions
}
senderGroupSessions, ok := roomGroupSessions[senderKey]
if !ok {
senderGroupSessions = make(map[string]*InboundGroupSession)
roomGroupSessions[senderKey] = senderGroupSessions
}
return senderGroupSessions
}
func (mach *OlmMachine) createGroupSession(senderKey, signingKey string, roomID id.RoomID, sessionID, sessionKey string) {
igs, err := NewInboundGroupSession(senderKey, signingKey, roomID, sessionID, sessionKey)
if err != nil {
mach.log.Debugfln("Failed to create inbound group session: %v", err)
} else if string(igs.ID()) != sessionID {
mach.log.Debugfln("Mismatched session ID while creating inbound group session")
} else {
mach.getGroupSessions(roomID, senderKey)[sessionID] = igs
// TODO save mach.groupSessions
}
}
func (mach *OlmMachine) receiveRoomKey(evt *OlmEvent, content *event.RoomKeyEventContent) {
// TODO nio had a comment saying "handle this better" for the case where evt.Keys.Ed25519 is none?
if content.Algorithm != event.AlgorithmMegolmV1 || evt.Keys.Ed25519 == "" {
return
}
mach.createGroupSession(evt.SenderKey, evt.Keys.Ed25519, content.RoomID, content.SessionID, content.SessionKey)
}
// ShareKeys returns a key upload request.
func (mach *OlmMachine) ShareKeys() error {
var deviceKeys *mautrix.DeviceKeys
if !mach.account.Shared {
deviceKeys = mach.account.getInitialKeys(mach.client.UserID, mach.client.DeviceID)
}
oneTimeKeys := mach.account.getOneTimeKeys(mach.client.UserID, mach.client.DeviceID)
if len(oneTimeKeys) == 0 && deviceKeys == nil {
return nil
}
return &mautrix.ReqUploadKeys{
OneTimeKeys: otks,
}
}
// InitialKeys returns the initial key upload request, including signed device keys and unsigned one-time keys.
func (mach *OlmMachine) InitialKeys() (*mautrix.ReqUploadKeys, error) {
ed, curve := mach.account.IdentityKeys()
deviceKeys := &mautrix.DeviceKeys{
UserID: mach.UserID,
DeviceID: mach.DeviceID,
Algorithms: []string{string(olm.AlgorithmMegolmV1)},
Keys: map[id.DeviceKeyID]string{
id.NewDeviceKeyID("curve25519", mach.DeviceID): string(curve),
id.NewDeviceKeyID("ed25519", mach.DeviceID): string(ed),
},
Signatures: map[id.UserID]map[id.DeviceKeyID]string{
mach.UserID: {
// This is filled below.
},
},
}
signature, err := mach.account.SignJSON(deviceKeys)
if err != nil {
return nil, err
}
deviceKeys.Signatures[mach.UserID][id.NewDeviceKeyID("ed25519", mach.DeviceID)] = signature
return &mautrix.ReqUploadKeys{
req := &mautrix.ReqUploadKeys{
DeviceKeys: deviceKeys,
OneTimeKeys: mach.getOneTimeKeys(),
}, nil
}
func (mach *OlmMachine) getOneTimeKeys() map[id.KeyID]string {
mach.account.GenOneTimeKeys(mach.account.MaxNumberOfOneTimeKeys() / 2)
oneTimeKeys := make(map[id.KeyID]string)
for keyID, key := range mach.account.OneTimeKeys().Curve25519 {
oneTimeKeys[id.NewKeyID("curve25519", keyID)] = string(key)
OneTimeKeys: oneTimeKeys,
}
mach.account.MarkKeysAsPublished()
return oneTimeKeys
_, err := mach.client.UploadKeys(req)
return err
}

170
crypto/decrypt.go Normal file
View file

@ -0,0 +1,170 @@
// 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"
"fmt"
"github.com/pkg/errors"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"maunium.net/go/olm"
)
var (
IncorrectEncryptedContentType = errors.New("event content is not instance of *event.EncryptedEventContent")
UnsupportedAlgorithm = errors.New("unsupported event encryption algorithm")
NotEncryptedForMe = errors.New("olm event doesn't contain ciphertext for this device")
UnsupportedOlmMessageType = errors.New("unsupported olm message type")
DecryptionFailedWithMatchingSession = errors.New("decryption failed with matching session")
DecryptionFailedForNormalMessage = errors.New("decryption failed for normal message")
SenderMismatch = errors.New("mismatched sender in olm payload")
RecipientMismatch = errors.New("mismatched recipient in olm payload")
RecipientKeyMismatch = errors.New("mismatched recipient key in olm payload")
)
func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, error) {
content, ok := evt.Content.Parsed.(*event.EncryptedEventContent)
if !ok {
return nil, IncorrectEncryptedContentType
}
fmt.Println(content.Algorithm)
// TODO
return nil, nil
}
type OlmEventKeys struct {
Ed25519 string `json:"ed25519"`
}
type OlmEvent struct {
Source *event.Event `json:"-"`
SenderKey string `json:"-"`
Sender id.UserID `json:"sender"`
SenderDevice id.DeviceID `json:"sender_device"`
Keys OlmEventKeys `json:"keys"`
Recipient id.UserID `json:"recipient"`
RecipientKeys OlmEventKeys `json:"recipient_keys"`
Type event.Type `json:"type"`
Content event.Content `json:"content"`
}
func (mach *OlmMachine) createInboundSession(senderKey, ciphertext string) (*OlmSession, error) {
session, err := mach.account.NewInboundSessionFrom(senderKey, ciphertext)
if err != nil {
return nil, err
}
mach.SaveAccount()
mach.SaveSession(senderKey, session)
return session, nil
}
func (mach *OlmMachine) markDeviceForUnwedging(sender id.UserID, senderKey string) {
// TODO implement
}
func (mach *OlmMachine) DecryptOlmEvent(evt *event.Event) (*OlmEvent, error) {
content, ok := evt.Content.Parsed.(*event.EncryptedEventContent)
if !ok {
return nil, IncorrectEncryptedContentType
} else if content.Algorithm != event.AlgorithmOlmV1 {
return nil, UnsupportedAlgorithm
}
_, ownKey := mach.account.IdentityKeys()
ownContent, ok := content.OlmCiphertext[string(ownKey)]
if !ok {
return nil, NotEncryptedForMe
}
return mach.decryptOlmEvent(evt, content.SenderKey, ownContent.Type, ownContent.Body)
}
func (mach *OlmMachine) decryptOlmEvent(evt *event.Event, senderKey string, olmType event.OlmMessageType, ciphertext string) (*OlmEvent, error) {
if olmType != event.OlmPreKeyMessage && olmType != event.OlmNormalMessage {
return nil, UnsupportedOlmMessageType
}
plaintext, err := mach.tryDecryptOlmEvent(senderKey, olmType, ciphertext)
if err != nil {
if err == DecryptionFailedWithMatchingSession {
mach.log.Debugfln("Found matching session yet decryption failed for sender %s with key %s", evt.Sender, senderKey)
mach.markDeviceForUnwedging(evt.Sender, senderKey)
}
return nil, err
}
// Decryption failed with every known session or no known sessions, let's try to create a new session.
if plaintext == nil {
// 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 != event.OlmNormalMessage {
mach.markDeviceForUnwedging(evt.Sender, senderKey)
return nil, DecryptionFailedForNormalMessage
}
session, err := mach.createInboundSession(senderKey, ciphertext)
if err != nil {
mach.markDeviceForUnwedging(evt.Sender, senderKey)
return nil, errors.Wrap(err, "failed to create new session from prekey message")
}
plaintext, err = session.Decrypt(ciphertext, olm.MsgType(olmType))
if err != nil {
return nil, errors.Wrap(err, "failed to decrypt message with session created from prekey message")
}
}
var olmEvt OlmEvent
err = json.Unmarshal(plaintext, &olmEvt)
if err != nil {
return nil, errors.Wrap(err, "failed to parse olm payload")
}
if evt.Sender != olmEvt.Sender {
return nil, SenderMismatch
} else if mach.client.UserID != olmEvt.Recipient {
return nil, RecipientMismatch
} else if ed25519, _ := mach.account.IdentityKeys(); string(ed25519) != olmEvt.RecipientKeys.Ed25519 {
return nil, RecipientKeyMismatch
}
err = olmEvt.Content.ParseRaw(olmEvt.Type)
if err != nil && !event.IsUnsupportedContentType(err) {
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 string, olmType event.OlmMessageType, ciphertext string) ([]byte, error) {
for _, session := range mach.GetSessions(senderKey) {
if olmType == event.OlmPreKeyMessage {
matches, err := session.MatchesInboundSession(ciphertext)
if err != nil {
return nil, err
} else if !matches {
continue
}
}
plaintext, err := session.Decrypt(ciphertext, olm.MsgType(olmType))
if err != nil {
if olmType == event.OlmPreKeyMessage {
return nil, DecryptionFailedWithMatchingSession
}
} else {
return plaintext, nil
}
}
return nil, nil
}

View file

@ -7,11 +7,13 @@
package crypto
import (
"errors"
"time"
"maunium.net/go/mautrix/id"
"github.com/pkg/errors"
"maunium.net/go/olm"
"maunium.net/go/mautrix/id"
)
var (
@ -24,40 +26,43 @@ type UserDevice struct {
DeviceID id.DeviceID
}
type OlmAccount struct {
*olm.Account
Shared bool
}
type OlmSession struct {
*olm.Session
ExpirationMixin
}
func wrapSession(session *olm.Session) *OlmSession {
return &OlmSession{
Session: session,
ExpirationMixin: ExpirationMixin{
TimeMixin: TimeMixin{
CreationTime: time.Now(),
UseTime: time.Now(),
},
MaxAge: 7 * 24 * time.Hour,
},
}
}
func (account *OlmAccount) NewInboundSessionFrom(senderKey, ciphertext string) (*OlmSession, error) {
session, err := account.Account.NewInboundSessionFrom(olm.Curve25519(senderKey), ciphertext)
if err != nil {
return nil, err
}
_ = account.RemoveOneTimeKeys(session)
return wrapSession(session), nil
}
func (session *OlmSession) Encrypt(plaintext string) (olm.MsgType, string) {
session.UseTime = time.Now()
return session.Session.Encrypt(plaintext)
}
func (session *OlmSession) Decrypt(ciphertext string, msgType olm.MsgType) (string, error) {
func (session *OlmSession) Decrypt(ciphertext string, msgType olm.MsgType) ([]byte, error) {
session.UseTime = time.Now()
return session.Session.Decrypt(ciphertext, msgType)
}
type TimeMixin struct {
CreationTime time.Time
UseTime time.Time
}
type ExpirationMixin struct {
TimeMixin
MaxAge time.Duration
}
func (exp *ExpirationMixin) Expired() bool {
return exp.CreationTime.Add(exp.MaxAge).Before(time.Now())
}
type InboundGroupSession struct {
*olm.InboundGroupSession
@ -68,6 +73,17 @@ type InboundGroupSession struct {
ForwardingChains []string
}
func NewInboundGroupSession(senderKey, signingKey string, roomID id.RoomID, sessionID, sessionKey string) (*InboundGroupSession, error) {
igs, err := olm.NewInboundGroupSession([]byte(sessionKey))
return &InboundGroupSession{
InboundGroupSession: igs,
SigningKey: signingKey,
SenderKey: senderKey,
RoomID: roomID,
ForwardingChains: nil,
}, err
}
type OutboundGroupSession struct {
*olm.OutboundGroupSession

View file

@ -7,15 +7,74 @@
package crypto
import (
"maunium.net/go/olm"
"encoding/gob"
"os"
"path/filepath"
"strings"
)
type Store interface {
Key() []byte
SaveAccount(*OlmAccount) error
LoadAccount() (*OlmAccount, error)
SaveAccount(*olm.Account)
LoadAccount() *olm.Account
LoadSessions() []*olm.Session
SaveSession(string, *olm.Session)
SaveSessions(string, []*OlmSession) error
LoadSessions(string) ([]*OlmSession, error)
}
type GobStore struct {
Path string
}
func (gs *GobStore) LoadAccount() (*OlmAccount, error) {
file, err := os.Open(filepath.Join(gs.Path, "account.gob"))
if err != nil {
if os.IsNotExist(err) {
err = nil
}
return nil, err
}
dec := gob.NewDecoder(file)
var account OlmAccount
err = dec.Decode(&account)
_ = file.Close()
return &account, err
}
func (gs *GobStore) SaveAccount(account *OlmAccount) error {
file, err := os.OpenFile(filepath.Join(gs.Path, "account.gob"), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
err = gob.NewEncoder(file).Encode(account)
_ = file.Close()
return err
}
func pathSafe(val string) string {
return strings.ReplaceAll(val, "/", "-")
}
func (gs *GobStore) LoadSessions(senderKey string) ([]*OlmSession, error) {
file, err := os.Open(filepath.Join(gs.Path, "sessions", pathSafe(senderKey) + ".gob"))
if err != nil {
if os.IsNotExist(err) {
return []*OlmSession{}, nil
}
return nil, err
}
dec := gob.NewDecoder(file)
var sessions []*OlmSession
err = dec.Decode(&sessions)
_ = file.Close()
return sessions, err
}
func (gs *GobStore) SaveSessions(senderKey string, sessions []*OlmSession) error {
file, err := os.OpenFile(filepath.Join(gs.Path, "sessions", pathSafe(senderKey) + ".gob"), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
err = gob.NewEncoder(file).Encode(sessions)
_ = file.Close()
return err
}

25
crypto/time.go Normal file
View file

@ -0,0 +1,25 @@
// 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 (
"time"
)
type TimeMixin struct {
CreationTime time.Time
UseTime time.Time
}
type ExpirationMixin struct {
TimeMixin
MaxAge time.Duration
}
func (exp *ExpirationMixin) Expired() bool {
return exp.CreationTime.Add(exp.MaxAge).Before(time.Now())
}

3
go.mod
View file

@ -4,8 +4,9 @@ go 1.14
require (
github.com/fatih/structs v1.1.0
github.com/pkg/errors v0.9.1
github.com/russross/blackfriday/v2 v2.0.1
github.com/stretchr/testify v1.5.1
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
maunium.net/go/olm v0.0.0-20200419222421-d050af0532a1
maunium.net/go/olm v0.0.0-20200420235207-35b7cc0d340c
)

3
go.sum
View file

@ -4,6 +4,8 @@ github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4=
github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -33,3 +35,4 @@ maunium.net/go/canonicaljson v0.1.1 h1:5G+jhiFG/wHPPcZ79tSxbZgiE577vPz0nVfXfKnG0
maunium.net/go/canonicaljson v0.1.1/go.mod h1:0X1niXxzCoKAURvqbYmimuu6IQpbqd8fIOiwPU5JwRg=
maunium.net/go/olm v0.0.0-20200419222421-d050af0532a1 h1:6EHqKE4e/osCwhri1lr9u8hzTGFOjei6fGry/cEkA9s=
maunium.net/go/olm v0.0.0-20200419222421-d050af0532a1/go.mod h1:SaLfmFDzrmqovMPeXoVJOZqij88nuJI9ZWR4/hV03v4=
maunium.net/go/olm v0.0.0-20200420235207-35b7cc0d340c/go.mod h1:SaLfmFDzrmqovMPeXoVJOZqij88nuJI9ZWR4/hV03v4=