From ffc8d4de8f7d0dfbc554f17151fa4252af2f5ee5 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 21 Apr 2020 02:58:20 +0300 Subject: [PATCH] Add more stuff --- crypto/account.go | 63 +++++++++++++++ crypto/crypto.go | 194 +++++++++++++++++++++++++++++++-------------- crypto/decrypt.go | 170 +++++++++++++++++++++++++++++++++++++++ crypto/sessions.go | 60 +++++++++----- crypto/store.go | 73 +++++++++++++++-- crypto/time.go | 25 ++++++ go.mod | 3 +- go.sum | 3 + 8 files changed, 502 insertions(+), 89 deletions(-) create mode 100644 crypto/account.go create mode 100644 crypto/decrypt.go create mode 100644 crypto/time.go diff --git a/crypto/account.go b/crypto/account.go new file mode 100644 index 00000000..f3f9ee1d --- /dev/null +++ b/crypto/account.go @@ -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 +} diff --git a/crypto/crypto.go b/crypto/crypto.go index 8ce957d9..c499ebc6 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -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 } diff --git a/crypto/decrypt.go b/crypto/decrypt.go new file mode 100644 index 00000000..611090dc --- /dev/null +++ b/crypto/decrypt.go @@ -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 +} diff --git a/crypto/sessions.go b/crypto/sessions.go index 9612ca11..cd2b9543 100644 --- a/crypto/sessions.go +++ b/crypto/sessions.go @@ -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 diff --git a/crypto/store.go b/crypto/store.go index b0f99d27..d3d6ae8d 100644 --- a/crypto/store.go +++ b/crypto/store.go @@ -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 } diff --git a/crypto/time.go b/crypto/time.go new file mode 100644 index 00000000..3dc8823d --- /dev/null +++ b/crypto/time.go @@ -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()) +} diff --git a/go.mod b/go.mod index d46e9cb2..32b19ba2 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 7b25a4f0..4c30f494 100644 --- a/go.sum +++ b/go.sum @@ -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=