add vendoring
This commit is contained in:
92
vendor/maunium.net/go/mautrix/crypto/account.go
generated
vendored
Normal file
92
vendor/maunium.net/go/mautrix/crypto/account.go
generated
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type OlmAccount struct {
|
||||
Internal olm.Account
|
||||
signingKey id.SigningKey
|
||||
identityKey id.IdentityKey
|
||||
Shared bool
|
||||
}
|
||||
|
||||
func NewOlmAccount() *OlmAccount {
|
||||
return &OlmAccount{
|
||||
Internal: *olm.NewAccount(),
|
||||
}
|
||||
}
|
||||
|
||||
func (account *OlmAccount) Keys() (id.SigningKey, id.IdentityKey) {
|
||||
if len(account.signingKey) == 0 || len(account.identityKey) == 0 {
|
||||
account.signingKey, account.identityKey = account.Internal.IdentityKeys()
|
||||
}
|
||||
return account.signingKey, account.identityKey
|
||||
}
|
||||
|
||||
func (account *OlmAccount) SigningKey() id.SigningKey {
|
||||
if len(account.signingKey) == 0 {
|
||||
account.signingKey, account.identityKey = account.Internal.IdentityKeys()
|
||||
}
|
||||
return account.signingKey
|
||||
}
|
||||
|
||||
func (account *OlmAccount) IdentityKey() id.IdentityKey {
|
||||
if len(account.identityKey) == 0 {
|
||||
account.signingKey, account.identityKey = account.Internal.IdentityKeys()
|
||||
}
|
||||
return account.identityKey
|
||||
}
|
||||
|
||||
func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID) *mautrix.DeviceKeys {
|
||||
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(account.IdentityKey()),
|
||||
id.NewDeviceKeyID(id.KeyAlgorithmEd25519, deviceID): string(account.SigningKey()),
|
||||
},
|
||||
}
|
||||
|
||||
signature, err := account.Internal.SignJSON(deviceKeys)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
deviceKeys.Signatures = mautrix.Signatures{
|
||||
userID: {
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
|
||||
},
|
||||
}
|
||||
return deviceKeys
|
||||
}
|
||||
|
||||
func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID, currentOTKCount int) map[id.KeyID]mautrix.OneTimeKey {
|
||||
newCount := int(account.Internal.MaxNumberOfOneTimeKeys()/2) - currentOTKCount
|
||||
if newCount > 0 {
|
||||
account.Internal.GenOneTimeKeys(uint(newCount))
|
||||
}
|
||||
oneTimeKeys := make(map[id.KeyID]mautrix.OneTimeKey)
|
||||
for keyID, key := range account.Internal.OneTimeKeys() {
|
||||
key := mautrix.OneTimeKey{Key: key}
|
||||
signature, _ := account.Internal.SignJSON(key)
|
||||
key.Signatures = mautrix.Signatures{
|
||||
userID: {
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
|
||||
},
|
||||
}
|
||||
key.IsSigned = true
|
||||
oneTimeKeys[id.NewKeyID(id.KeyAlgorithmSignedCurve25519, keyID)] = key
|
||||
}
|
||||
account.Internal.MarkKeysAsPublished()
|
||||
return oneTimeKeys
|
||||
}
|
||||
239
vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go
generated
vendored
Normal file
239
vendor/maunium.net/go/mautrix/crypto/attachment/attachments.go
generated
vendored
Normal file
@@ -0,0 +1,239 @@
|
||||
// Copyright (c) 2022 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 attachment
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
HashMismatch = errors.New("mismatching SHA-256 digest")
|
||||
UnsupportedVersion = errors.New("unsupported Matrix file encryption version")
|
||||
UnsupportedAlgorithm = errors.New("unsupported JWK encryption algorithm")
|
||||
InvalidKey = errors.New("failed to decode key")
|
||||
InvalidInitVector = errors.New("failed to decode initialization vector")
|
||||
InvalidHash = errors.New("failed to decode SHA-256 hash")
|
||||
ReaderClosed = errors.New("encrypting reader was already closed")
|
||||
)
|
||||
|
||||
var (
|
||||
keyBase64Length = base64.RawURLEncoding.EncodedLen(utils.AESCTRKeyLength)
|
||||
ivBase64Length = base64.RawStdEncoding.EncodedLen(utils.AESCTRIVLength)
|
||||
hashBase64Length = base64.RawStdEncoding.EncodedLen(utils.SHAHashLength)
|
||||
)
|
||||
|
||||
type JSONWebKey struct {
|
||||
Key string `json:"k"`
|
||||
Algorithm string `json:"alg"`
|
||||
Extractable bool `json:"ext"`
|
||||
KeyType string `json:"kty"`
|
||||
KeyOps []string `json:"key_ops"`
|
||||
}
|
||||
|
||||
type EncryptedFileHashes struct {
|
||||
SHA256 string `json:"sha256"`
|
||||
}
|
||||
|
||||
type decodedKeys struct {
|
||||
key [utils.AESCTRKeyLength]byte
|
||||
iv [utils.AESCTRIVLength]byte
|
||||
|
||||
sha256 [utils.SHAHashLength]byte
|
||||
}
|
||||
|
||||
type EncryptedFile struct {
|
||||
Key JSONWebKey `json:"key"`
|
||||
InitVector string `json:"iv"`
|
||||
Hashes EncryptedFileHashes `json:"hashes"`
|
||||
Version string `json:"v"`
|
||||
|
||||
decoded *decodedKeys
|
||||
}
|
||||
|
||||
func NewEncryptedFile() *EncryptedFile {
|
||||
key, iv := utils.GenAttachmentA256CTR()
|
||||
return &EncryptedFile{
|
||||
Key: JSONWebKey{
|
||||
Key: base64.RawURLEncoding.EncodeToString(key[:]),
|
||||
Algorithm: "A256CTR",
|
||||
Extractable: true,
|
||||
KeyType: "oct",
|
||||
KeyOps: []string{"encrypt", "decrypt"},
|
||||
},
|
||||
InitVector: base64.RawStdEncoding.EncodeToString(iv[:]),
|
||||
Version: "v2",
|
||||
|
||||
decoded: &decodedKeys{key: key, iv: iv},
|
||||
}
|
||||
}
|
||||
|
||||
func (ef *EncryptedFile) decodeKeys(includeHash bool) error {
|
||||
if ef.decoded != nil {
|
||||
return nil
|
||||
} else if len(ef.Key.Key) != keyBase64Length {
|
||||
return InvalidKey
|
||||
} else if len(ef.InitVector) != ivBase64Length {
|
||||
return InvalidInitVector
|
||||
} else if includeHash && len(ef.Hashes.SHA256) != hashBase64Length {
|
||||
return InvalidHash
|
||||
}
|
||||
ef.decoded = &decodedKeys{}
|
||||
_, err := base64.RawURLEncoding.Decode(ef.decoded.key[:], []byte(ef.Key.Key))
|
||||
if err != nil {
|
||||
return InvalidKey
|
||||
}
|
||||
_, err = base64.RawStdEncoding.Decode(ef.decoded.iv[:], []byte(ef.InitVector))
|
||||
if err != nil {
|
||||
return InvalidInitVector
|
||||
}
|
||||
if includeHash {
|
||||
_, err = base64.RawStdEncoding.Decode(ef.decoded.sha256[:], []byte(ef.Hashes.SHA256))
|
||||
if err != nil {
|
||||
return InvalidHash
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts the given data, updates the SHA256 hash in the EncryptedFile struct and returns the ciphertext.
|
||||
//
|
||||
// Deprecated: this makes a copy for the ciphertext, which means 2x memory usage. EncryptInPlace is recommended.
|
||||
func (ef *EncryptedFile) Encrypt(plaintext []byte) []byte {
|
||||
ciphertext := make([]byte, len(plaintext))
|
||||
copy(ciphertext, plaintext)
|
||||
ef.EncryptInPlace(ciphertext)
|
||||
return ciphertext
|
||||
}
|
||||
|
||||
// EncryptInPlace encrypts the given data in-place (i.e. the provided data is overridden with the ciphertext)
|
||||
// and updates the SHA256 hash in the EncryptedFile struct.
|
||||
func (ef *EncryptedFile) EncryptInPlace(data []byte) {
|
||||
ef.decodeKeys(false)
|
||||
utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv)
|
||||
checksum := sha256.Sum256(data)
|
||||
ef.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(checksum[:])
|
||||
}
|
||||
|
||||
type encryptingReader struct {
|
||||
stream cipher.Stream
|
||||
hash hash.Hash
|
||||
source io.Reader
|
||||
file *EncryptedFile
|
||||
closed bool
|
||||
|
||||
isDecrypting bool
|
||||
}
|
||||
|
||||
func (r *encryptingReader) Read(dst []byte) (n int, err error) {
|
||||
if r.closed {
|
||||
return 0, ReaderClosed
|
||||
} else if r.isDecrypting && r.file.decoded == nil {
|
||||
if err = r.file.PrepareForDecryption(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
n, err = r.source.Read(dst)
|
||||
r.stream.XORKeyStream(dst[:n], dst[:n])
|
||||
r.hash.Write(dst[:n])
|
||||
return
|
||||
}
|
||||
|
||||
func (r *encryptingReader) Close() (err error) {
|
||||
closer, ok := r.source.(io.ReadCloser)
|
||||
if ok {
|
||||
err = closer.Close()
|
||||
}
|
||||
if r.isDecrypting {
|
||||
var downloadedChecksum [utils.SHAHashLength]byte
|
||||
r.hash.Sum(downloadedChecksum[:])
|
||||
if downloadedChecksum != r.file.decoded.sha256 {
|
||||
return HashMismatch
|
||||
}
|
||||
} else {
|
||||
r.file.Hashes.SHA256 = base64.RawStdEncoding.EncodeToString(r.hash.Sum(nil))
|
||||
}
|
||||
r.closed = true
|
||||
return
|
||||
}
|
||||
|
||||
// EncryptStream wraps the given io.Reader in order to encrypt the data.
|
||||
//
|
||||
// The Close() method of the returned io.ReadCloser must be called for the SHA256 hash
|
||||
// in the EncryptedFile struct to be updated. The metadata is not valid before the hash
|
||||
// is filled.
|
||||
func (ef *EncryptedFile) EncryptStream(reader io.Reader) io.ReadCloser {
|
||||
ef.decodeKeys(false)
|
||||
block, _ := aes.NewCipher(ef.decoded.key[:])
|
||||
return &encryptingReader{
|
||||
stream: cipher.NewCTR(block, ef.decoded.iv[:]),
|
||||
hash: sha256.New(),
|
||||
source: reader,
|
||||
file: ef,
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt decrypts the given data and returns the plaintext.
|
||||
//
|
||||
// Deprecated: this makes a copy for the plaintext data, which means 2x memory usage. DecryptInPlace is recommended.
|
||||
func (ef *EncryptedFile) Decrypt(ciphertext []byte) ([]byte, error) {
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
copy(plaintext, ciphertext)
|
||||
return plaintext, ef.DecryptInPlace(plaintext)
|
||||
}
|
||||
|
||||
// PrepareForDecryption checks that the version and algorithm are supported and decodes the base64 keys
|
||||
//
|
||||
// DecryptStream will call this with the first Read() call if this hasn't been called manually.
|
||||
//
|
||||
// DecryptInPlace will always call this automatically, so calling this manually is not necessary when using that function.
|
||||
func (ef *EncryptedFile) PrepareForDecryption() error {
|
||||
if ef.Version != "v2" {
|
||||
return UnsupportedVersion
|
||||
} else if ef.Key.Algorithm != "A256CTR" {
|
||||
return UnsupportedAlgorithm
|
||||
} else if err := ef.decodeKeys(true); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecryptInPlace decrypts the given data in-place (i.e. the provided data is overridden with the plaintext).
|
||||
func (ef *EncryptedFile) DecryptInPlace(data []byte) error {
|
||||
if err := ef.PrepareForDecryption(); err != nil {
|
||||
return err
|
||||
} else if ef.decoded.sha256 != sha256.Sum256(data) {
|
||||
return HashMismatch
|
||||
} else {
|
||||
utils.XorA256CTR(data, ef.decoded.key, ef.decoded.iv)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DecryptStream wraps the given io.Reader in order to decrypt the data.
|
||||
//
|
||||
// The first Read call will check the algorithm and decode keys, so it might return an error before actually reading anything.
|
||||
// If you want to validate the file before opening the stream, call PrepareForDecryption manually and check for errors.
|
||||
//
|
||||
// The Close call will validate the hash and return an error if it doesn't match.
|
||||
// In this case, the written data should be considered compromised and should not be used further.
|
||||
func (ef *EncryptedFile) DecryptStream(reader io.Reader) io.ReadCloser {
|
||||
block, _ := aes.NewCipher(ef.decoded.key[:])
|
||||
return &encryptingReader{
|
||||
stream: cipher.NewCTR(block, ef.decoded.iv[:]),
|
||||
hash: sha256.New(),
|
||||
source: reader,
|
||||
file: ef,
|
||||
}
|
||||
}
|
||||
177
vendor/maunium.net/go/mautrix/crypto/canonicaljson/LICENSE
generated
vendored
Normal file
177
vendor/maunium.net/go/mautrix/crypto/canonicaljson/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
4
vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md
generated
vendored
Normal file
4
vendor/maunium.net/go/mautrix/crypto/canonicaljson/README.md
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# canonicaljson
|
||||
This is a Go package to produce Matrix [Canonical JSON](https://matrix.org/docs/spec/appendices#canonical-json).
|
||||
It is essentially just [json.go](https://github.com/matrix-org/gomatrixserverlib/blob/master/json.go)
|
||||
from gomatrixserverlib without all the other files that are completely useless for non-server use cases.
|
||||
257
vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go
generated
vendored
Normal file
257
vendor/maunium.net/go/mautrix/crypto/canonicaljson/json.go
generated
vendored
Normal file
@@ -0,0 +1,257 @@
|
||||
/* Copyright 2016-2017 Vector Creations Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package canonicaljson
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// CanonicalJSON re-encodes the JSON in a canonical encoding. The encoding is
|
||||
// the shortest possible encoding using integer values with sorted object keys.
|
||||
// https://matrix.org/docs/spec/appendices#canonical-json
|
||||
func CanonicalJSON(input []byte) ([]byte, error) {
|
||||
if !gjson.Valid(string(input)) {
|
||||
return nil, fmt.Errorf("invalid json")
|
||||
}
|
||||
|
||||
return CanonicalJSONAssumeValid(input), nil
|
||||
}
|
||||
|
||||
// CanonicalJSONAssumeValid is the same as CanonicalJSON, but assumes the
|
||||
// input is valid JSON
|
||||
func CanonicalJSONAssumeValid(input []byte) []byte {
|
||||
input = CompactJSON(input, make([]byte, 0, len(input)))
|
||||
return SortJSON(input, make([]byte, 0, len(input)))
|
||||
}
|
||||
|
||||
// SortJSON reencodes the JSON with the object keys sorted by lexicographically
|
||||
// by codepoint. The input must be valid JSON.
|
||||
func SortJSON(input, output []byte) []byte {
|
||||
result := gjson.ParseBytes(input)
|
||||
|
||||
return sortJSONValue(result, input, output)
|
||||
}
|
||||
|
||||
// sortJSONValue takes a gjson.Result and sorts it. inputJSON must be the
|
||||
// raw JSON bytes that gjson.Result points to.
|
||||
func sortJSONValue(input gjson.Result, inputJSON, output []byte) []byte {
|
||||
if input.IsArray() {
|
||||
return sortJSONArray(input, inputJSON, output)
|
||||
}
|
||||
|
||||
if input.IsObject() {
|
||||
return sortJSONObject(input, inputJSON, output)
|
||||
}
|
||||
|
||||
// If its neither an object nor an array then there is no sub structure
|
||||
// to sort, so just append the raw bytes.
|
||||
return append(output, input.Raw...)
|
||||
}
|
||||
|
||||
// sortJSONArray takes a gjson.Result and sorts it, assuming its an array.
|
||||
// inputJSON must be the raw JSON bytes that gjson.Result points to.
|
||||
func sortJSONArray(input gjson.Result, inputJSON, output []byte) []byte {
|
||||
sep := byte('[')
|
||||
|
||||
// Iterate over each value in the array and sort it.
|
||||
input.ForEach(func(_, value gjson.Result) bool {
|
||||
output = append(output, sep)
|
||||
sep = ','
|
||||
output = sortJSONValue(value, inputJSON, output)
|
||||
return true // keep iterating
|
||||
})
|
||||
|
||||
if sep == '[' {
|
||||
// If sep is still '[' then the array was empty and we never wrote the
|
||||
// initial '[', so we write it now along with the closing ']'.
|
||||
output = append(output, '[', ']')
|
||||
} else {
|
||||
// Otherwise we end the array by writing a single ']'
|
||||
output = append(output, ']')
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// sortJSONObject takes a gjson.Result and sorts it, assuming its an object.
|
||||
// inputJSON must be the raw JSON bytes that gjson.Result points to.
|
||||
func sortJSONObject(input gjson.Result, inputJSON, output []byte) []byte {
|
||||
type entry struct {
|
||||
key string // The parsed key string
|
||||
rawKey string // The raw, unparsed key JSON string
|
||||
value gjson.Result
|
||||
}
|
||||
|
||||
var entries []entry
|
||||
|
||||
// Iterate over each key/value pair and add it to a slice
|
||||
// that we can sort
|
||||
input.ForEach(func(key, value gjson.Result) bool {
|
||||
entries = append(entries, entry{
|
||||
key: key.String(),
|
||||
rawKey: key.Raw,
|
||||
value: value,
|
||||
})
|
||||
return true // keep iterating
|
||||
})
|
||||
|
||||
// Sort the slice based on the *parsed* key
|
||||
sort.Slice(entries, func(a, b int) bool {
|
||||
return entries[a].key < entries[b].key
|
||||
})
|
||||
|
||||
sep := byte('{')
|
||||
|
||||
for _, entry := range entries {
|
||||
output = append(output, sep)
|
||||
sep = ','
|
||||
|
||||
// Append the raw unparsed JSON key, *not* the parsed key
|
||||
output = append(output, entry.rawKey...)
|
||||
output = append(output, ':')
|
||||
output = sortJSONValue(entry.value, inputJSON, output)
|
||||
}
|
||||
if sep == '{' {
|
||||
// If sep is still '{' then the object was empty and we never wrote the
|
||||
// initial '{', so we write it now along with the closing '}'.
|
||||
output = append(output, '{', '}')
|
||||
} else {
|
||||
// Otherwise we end the object by writing a single '}'
|
||||
output = append(output, '}')
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// CompactJSON makes the encoded JSON as small as possible by removing
|
||||
// whitespace and unneeded unicode escapes
|
||||
func CompactJSON(input, output []byte) []byte {
|
||||
var i int
|
||||
for i < len(input) {
|
||||
c := input[i]
|
||||
i++
|
||||
// The valid whitespace characters are all less than or equal to SPACE 0x20.
|
||||
// The valid non-white characters are all greater than SPACE 0x20.
|
||||
// So we can check for whitespace by comparing against SPACE 0x20.
|
||||
if c <= ' ' {
|
||||
// Skip over whitespace.
|
||||
continue
|
||||
}
|
||||
// Add the non-whitespace character to the output.
|
||||
output = append(output, c)
|
||||
if c == '"' {
|
||||
// We are inside a string.
|
||||
for i < len(input) {
|
||||
c = input[i]
|
||||
i++
|
||||
// Check if this is an escape sequence.
|
||||
if c == '\\' {
|
||||
escape := input[i]
|
||||
i++
|
||||
if escape == 'u' {
|
||||
// If this is a unicode escape then we need to handle it specially
|
||||
output, i = compactUnicodeEscape(input, output, i)
|
||||
} else if escape == '/' {
|
||||
// JSON does not require escaping '/', but allows encoders to escape it as a special case.
|
||||
// Since the escape isn't required we remove it.
|
||||
output = append(output, escape)
|
||||
} else {
|
||||
// All other permitted escapes are single charater escapes that are already in their shortest form.
|
||||
output = append(output, '\\', escape)
|
||||
}
|
||||
} else {
|
||||
output = append(output, c)
|
||||
}
|
||||
if c == '"' {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// compactUnicodeEscape unpacks a 4 byte unicode escape starting at index.
|
||||
// If the escape is a surrogate pair then decode the 6 byte \uXXXX escape
|
||||
// that follows. Returns the output slice and a new input index.
|
||||
func compactUnicodeEscape(input, output []byte, index int) ([]byte, int) {
|
||||
const (
|
||||
ESCAPES = "uuuuuuuubtnufruuuuuuuuuuuuuuuuuu"
|
||||
HEX = "0123456789ABCDEF"
|
||||
)
|
||||
// If there aren't enough bytes to decode the hex escape then return.
|
||||
if len(input)-index < 4 {
|
||||
return output, len(input)
|
||||
}
|
||||
// Decode the 4 hex digits.
|
||||
c := readHexDigits(input[index:])
|
||||
index += 4
|
||||
if c < ' ' {
|
||||
// If the character is less than SPACE 0x20 then it will need escaping.
|
||||
escape := ESCAPES[c]
|
||||
output = append(output, '\\', escape)
|
||||
if escape == 'u' {
|
||||
output = append(output, '0', '0', byte('0'+(c>>4)), HEX[c&0xF])
|
||||
}
|
||||
} else if c == '\\' || c == '"' {
|
||||
// Otherwise the character only needs escaping if it is a QUOTE '"' or BACKSLASH '\\'.
|
||||
output = append(output, '\\', byte(c))
|
||||
} else if c < 0xD800 || c >= 0xE000 {
|
||||
// If the character isn't a surrogate pair then encoded it directly as UTF-8.
|
||||
var buffer [4]byte
|
||||
n := utf8.EncodeRune(buffer[:], rune(c))
|
||||
output = append(output, buffer[:n]...)
|
||||
} else {
|
||||
// Otherwise the escaped character was the first part of a UTF-16 style surrogate pair.
|
||||
// The next 6 bytes MUST be a '\uXXXX'.
|
||||
// If there aren't enough bytes to decode the hex escape then return.
|
||||
if len(input)-index < 6 {
|
||||
return output, len(input)
|
||||
}
|
||||
// Decode the 4 hex digits from the '\uXXXX'.
|
||||
surrogate := readHexDigits(input[index+2:])
|
||||
index += 6
|
||||
// Reconstruct the UCS4 codepoint from the surrogates.
|
||||
codepoint := 0x10000 + (((c & 0x3FF) << 10) | (surrogate & 0x3FF))
|
||||
// Encode the charater as UTF-8.
|
||||
var buffer [4]byte
|
||||
n := utf8.EncodeRune(buffer[:], rune(codepoint))
|
||||
output = append(output, buffer[:n]...)
|
||||
}
|
||||
return output, index
|
||||
}
|
||||
|
||||
// Read 4 hex digits from the input slice.
|
||||
// Taken from https://github.com/NegativeMjark/indolentjson-rust/blob/8b959791fe2656a88f189c5d60d153be05fe3deb/src/readhex.rs#L21
|
||||
func readHexDigits(input []byte) uint32 {
|
||||
hex := binary.BigEndian.Uint32(input)
|
||||
// subtract '0'
|
||||
hex -= 0x30303030
|
||||
// strip the higher bits, maps 'a' => 'A'
|
||||
hex &= 0x1F1F1F1F
|
||||
mask := hex & 0x10101010
|
||||
// subtract 'A' - 10 - '9' - 9 = 7 from the letters.
|
||||
hex -= mask >> 1
|
||||
hex += mask >> 4
|
||||
// collect the nibbles
|
||||
hex |= hex >> 4
|
||||
hex &= 0xFF00FF
|
||||
hex |= hex >> 8
|
||||
return hex & 0xFFFF
|
||||
}
|
||||
143
vendor/maunium.net/go/mautrix/crypto/cross_sign_key.go
generated
vendored
Normal file
143
vendor/maunium.net/go/mautrix/crypto/cross_sign_key.go
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// CrossSigningKeysCache holds the three cross-signing keys for the current user.
|
||||
type CrossSigningKeysCache struct {
|
||||
MasterKey *olm.PkSigning
|
||||
SelfSigningKey *olm.PkSigning
|
||||
UserSigningKey *olm.PkSigning
|
||||
}
|
||||
|
||||
func (cskc *CrossSigningKeysCache) PublicKeys() *CrossSigningPublicKeysCache {
|
||||
return &CrossSigningPublicKeysCache{
|
||||
MasterKey: cskc.MasterKey.PublicKey,
|
||||
SelfSigningKey: cskc.SelfSigningKey.PublicKey,
|
||||
UserSigningKey: cskc.UserSigningKey.PublicKey,
|
||||
}
|
||||
}
|
||||
|
||||
type CrossSigningSeeds struct {
|
||||
MasterKey []byte
|
||||
SelfSigningKey []byte
|
||||
UserSigningKey []byte
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) ExportCrossSigningKeys() CrossSigningSeeds {
|
||||
return CrossSigningSeeds{
|
||||
MasterKey: mach.CrossSigningKeys.MasterKey.Seed,
|
||||
SelfSigningKey: mach.CrossSigningKeys.SelfSigningKey.Seed,
|
||||
UserSigningKey: mach.CrossSigningKeys.UserSigningKey.Seed,
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) ImportCrossSigningKeys(keys CrossSigningSeeds) (err error) {
|
||||
var keysCache CrossSigningKeysCache
|
||||
if keysCache.MasterKey, err = olm.NewPkSigningFromSeed(keys.MasterKey); err != nil {
|
||||
return
|
||||
}
|
||||
if keysCache.SelfSigningKey, err = olm.NewPkSigningFromSeed(keys.SelfSigningKey); err != nil {
|
||||
return
|
||||
}
|
||||
if keysCache.UserSigningKey, err = olm.NewPkSigningFromSeed(keys.UserSigningKey); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
mach.Log.Trace("Got cross-signing keys: Master `%v` Self-signing `%v` User-signing `%v`",
|
||||
keysCache.MasterKey.PublicKey, keysCache.SelfSigningKey.PublicKey, keysCache.UserSigningKey.PublicKey)
|
||||
|
||||
mach.CrossSigningKeys = &keysCache
|
||||
mach.crossSigningPubkeys = keysCache.PublicKeys()
|
||||
return
|
||||
}
|
||||
|
||||
// GenerateCrossSigningKeys generates new cross-signing keys.
|
||||
func (mach *OlmMachine) GenerateCrossSigningKeys() (*CrossSigningKeysCache, error) {
|
||||
var keysCache CrossSigningKeysCache
|
||||
var err error
|
||||
if keysCache.MasterKey, err = olm.NewPkSigning(); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate master key: %w", err)
|
||||
}
|
||||
if keysCache.SelfSigningKey, err = olm.NewPkSigning(); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate self-signing key: %w", err)
|
||||
}
|
||||
if keysCache.UserSigningKey, err = olm.NewPkSigning(); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate user-signing key: %w", err)
|
||||
}
|
||||
mach.Log.Debug("Generated cross-signing keys: Master: `%v` Self-signing: `%v` User-signing: `%v`",
|
||||
keysCache.MasterKey.PublicKey, keysCache.SelfSigningKey.PublicKey, keysCache.UserSigningKey.PublicKey)
|
||||
return &keysCache, nil
|
||||
}
|
||||
|
||||
// PublishCrossSigningKeys signs and uploads the public keys of the given cross-signing keys to the server.
|
||||
func (mach *OlmMachine) PublishCrossSigningKeys(keys *CrossSigningKeysCache, uiaCallback mautrix.UIACallback) error {
|
||||
userID := mach.Client.UserID
|
||||
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey.String())
|
||||
masterKey := mautrix.CrossSigningKeys{
|
||||
UserID: userID,
|
||||
Usage: []id.CrossSigningUsage{id.XSUsageMaster},
|
||||
Keys: map[id.KeyID]id.Ed25519{
|
||||
masterKeyID: keys.MasterKey.PublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
selfKey := mautrix.CrossSigningKeys{
|
||||
UserID: userID,
|
||||
Usage: []id.CrossSigningUsage{id.XSUsageSelfSigning},
|
||||
Keys: map[id.KeyID]id.Ed25519{
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, keys.SelfSigningKey.PublicKey.String()): keys.SelfSigningKey.PublicKey,
|
||||
},
|
||||
}
|
||||
selfSig, err := keys.MasterKey.SignJSON(selfKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign self-signing key: %w", err)
|
||||
}
|
||||
selfKey.Signatures = map[id.UserID]map[id.KeyID]string{
|
||||
userID: {
|
||||
masterKeyID: selfSig,
|
||||
},
|
||||
}
|
||||
|
||||
userKey := mautrix.CrossSigningKeys{
|
||||
UserID: userID,
|
||||
Usage: []id.CrossSigningUsage{id.XSUsageUserSigning},
|
||||
Keys: map[id.KeyID]id.Ed25519{
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, keys.UserSigningKey.PublicKey.String()): keys.UserSigningKey.PublicKey,
|
||||
},
|
||||
}
|
||||
userSig, err := keys.MasterKey.SignJSON(userKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign user-signing key: %w", err)
|
||||
}
|
||||
userKey.Signatures = map[id.UserID]map[id.KeyID]string{
|
||||
userID: {
|
||||
masterKeyID: userSig,
|
||||
},
|
||||
}
|
||||
|
||||
err = mach.Client.UploadCrossSigningKeys(&mautrix.UploadCrossSigningKeysReq{
|
||||
Master: masterKey,
|
||||
SelfSigning: selfKey,
|
||||
UserSigning: userKey,
|
||||
}, uiaCallback)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mach.CrossSigningKeys = keys
|
||||
mach.crossSigningPubkeys = keys.PublicKeys()
|
||||
|
||||
return nil
|
||||
}
|
||||
89
vendor/maunium.net/go/mautrix/crypto/cross_sign_pubkey.go
generated
vendored
Normal file
89
vendor/maunium.net/go/mautrix/crypto/cross_sign_pubkey.go
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2022 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 (
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type CrossSigningPublicKeysCache struct {
|
||||
MasterKey id.Ed25519
|
||||
SelfSigningKey id.Ed25519
|
||||
UserSigningKey id.Ed25519
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) GetOwnCrossSigningPublicKeys() *CrossSigningPublicKeysCache {
|
||||
if mach.crossSigningPubkeys != nil {
|
||||
return mach.crossSigningPubkeys
|
||||
}
|
||||
if mach.CrossSigningKeys != nil {
|
||||
mach.crossSigningPubkeys = mach.CrossSigningKeys.PublicKeys()
|
||||
return mach.crossSigningPubkeys
|
||||
}
|
||||
if mach.crossSigningPubkeysFetched {
|
||||
return nil
|
||||
}
|
||||
cspk, err := mach.GetCrossSigningPublicKeys(mach.Client.UserID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to get own cross-signing public keys: %v", err)
|
||||
return nil
|
||||
}
|
||||
mach.crossSigningPubkeys = cspk
|
||||
mach.crossSigningPubkeysFetched = true
|
||||
return mach.crossSigningPubkeys
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) GetCrossSigningPublicKeys(userID id.UserID) (*CrossSigningPublicKeysCache, error) {
|
||||
dbKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get keys from database: %w", err)
|
||||
}
|
||||
if len(dbKeys) > 0 {
|
||||
masterKey, ok := dbKeys[id.XSUsageMaster]
|
||||
if ok {
|
||||
selfSigning, _ := dbKeys[id.XSUsageSelfSigning]
|
||||
userSigning, _ := dbKeys[id.XSUsageUserSigning]
|
||||
return &CrossSigningPublicKeysCache{
|
||||
MasterKey: masterKey.Key,
|
||||
SelfSigningKey: selfSigning.Key,
|
||||
UserSigningKey: userSigning.Key,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
keys, err := mach.Client.QueryKeys(&mautrix.ReqQueryKeys{
|
||||
DeviceKeys: mautrix.DeviceKeysRequest{
|
||||
userID: mautrix.DeviceIDList{},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query keys: %w", err)
|
||||
}
|
||||
|
||||
var cspk CrossSigningPublicKeysCache
|
||||
|
||||
masterKeys, ok := keys.MasterKeys[userID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
cspk.MasterKey = masterKeys.FirstKey()
|
||||
|
||||
selfSigningKeys, ok := keys.SelfSigningKeys[userID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
cspk.SelfSigningKey = selfSigningKeys.FirstKey()
|
||||
|
||||
userSigningKeys, ok := keys.UserSigningKeys[userID]
|
||||
if ok {
|
||||
cspk.UserSigningKey = userSigningKeys.FirstKey()
|
||||
}
|
||||
return &cspk, nil
|
||||
}
|
||||
223
vendor/maunium.net/go/mautrix/crypto/cross_sign_signing.go
generated
vendored
Normal file
223
vendor/maunium.net/go/mautrix/crypto/cross_sign_signing.go
generated
vendored
Normal file
@@ -0,0 +1,223 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
// Copyright (c) 2022 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCrossSigningKeysNotCached = errors.New("cross-signing private keys not in cache")
|
||||
ErrUserSigningKeyNotCached = errors.New("user-signing private key not in cache")
|
||||
ErrSelfSigningKeyNotCached = errors.New("self-signing private key not in cache")
|
||||
ErrSignatureUploadFail = errors.New("server-side failure uploading signatures")
|
||||
ErrCantSignOwnMasterKey = errors.New("signing your own master key is not allowed")
|
||||
ErrCantSignOtherDevice = errors.New("signing other users' devices is not allowed")
|
||||
ErrUserNotInQueryResponse = errors.New("could not find user in query keys response")
|
||||
ErrDeviceNotInQueryResponse = errors.New("could not find device in query keys response")
|
||||
ErrOlmAccountNotLoaded = errors.New("olm account has not been loaded")
|
||||
|
||||
ErrCrossSigningMasterKeyNotFound = errors.New("cross-signing master key not found")
|
||||
ErrMasterKeyMACNotFound = errors.New("found cross-signing master key, but didn't find corresponding MAC in verification request")
|
||||
ErrMismatchingMasterKeyMAC = errors.New("mismatching cross-signing master key MAC")
|
||||
)
|
||||
|
||||
func (mach *OlmMachine) fetchMasterKey(device *id.Device, content *event.VerificationMacEventContent, verState *verificationState, transactionID string) (id.Ed25519, error) {
|
||||
crossSignKeys, err := mach.CryptoStore.GetCrossSigningKeys(device.UserID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch cross-signing keys: %w", err)
|
||||
}
|
||||
masterKey, ok := crossSignKeys[id.XSUsageMaster]
|
||||
if !ok {
|
||||
return "", ErrCrossSigningMasterKeyNotFound
|
||||
}
|
||||
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.Key.String())
|
||||
masterKeyMAC, ok := content.Mac[masterKeyID]
|
||||
if !ok {
|
||||
return masterKey.Key, ErrMasterKeyMACNotFound
|
||||
}
|
||||
expectedMasterKeyMAC, _, err := mach.getPKAndKeysMAC(verState.sas, device.UserID, device.DeviceID,
|
||||
mach.Client.UserID, mach.Client.DeviceID, transactionID, masterKey.Key, masterKeyID, content.Mac)
|
||||
if err != nil {
|
||||
return masterKey.Key, fmt.Errorf("failed to calculate expected MAC for master key: %w", err)
|
||||
}
|
||||
if masterKeyMAC != expectedMasterKeyMAC {
|
||||
err = fmt.Errorf("%w: expected %s, got %s", ErrMismatchingMasterKeyMAC, expectedMasterKeyMAC, masterKeyMAC)
|
||||
}
|
||||
return masterKey.Key, err
|
||||
}
|
||||
|
||||
// SignUser creates a cross-signing signature for a user, stores it and uploads it to the server.
|
||||
func (mach *OlmMachine) SignUser(userID id.UserID, masterKey id.Ed25519) error {
|
||||
if userID == mach.Client.UserID {
|
||||
return ErrCantSignOwnMasterKey
|
||||
} else if mach.CrossSigningKeys == nil || mach.CrossSigningKeys.UserSigningKey == nil {
|
||||
return ErrUserSigningKeyNotCached
|
||||
}
|
||||
|
||||
masterKeyObj := mautrix.ReqKeysSignatures{
|
||||
UserID: userID,
|
||||
Usage: []id.CrossSigningUsage{id.XSUsageMaster},
|
||||
Keys: map[id.KeyID]string{
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String()): masterKey.String(),
|
||||
},
|
||||
}
|
||||
|
||||
signature, err := mach.signAndUpload(masterKeyObj, userID, masterKey.String(), mach.CrossSigningKeys.UserSigningKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mach.Log.Trace("Signed master key of %s with user-signing key: `%v`", userID, signature)
|
||||
|
||||
if err := mach.CryptoStore.PutSignature(userID, masterKey, mach.Client.UserID, mach.CrossSigningKeys.UserSigningKey.PublicKey, signature); err != nil {
|
||||
return fmt.Errorf("error storing signature in crypto store: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignOwnMasterKey uses the current account for signing the current user's master key and uploads the signature.
|
||||
func (mach *OlmMachine) SignOwnMasterKey() error {
|
||||
if mach.CrossSigningKeys == nil {
|
||||
return ErrCrossSigningKeysNotCached
|
||||
} else if mach.account == nil {
|
||||
return ErrOlmAccountNotLoaded
|
||||
}
|
||||
|
||||
userID := mach.Client.UserID
|
||||
deviceID := mach.Client.DeviceID
|
||||
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
|
||||
|
||||
masterKeyObj := mautrix.ReqKeysSignatures{
|
||||
UserID: userID,
|
||||
Usage: []id.CrossSigningUsage{id.XSUsageMaster},
|
||||
Keys: map[id.KeyID]string{
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String()): masterKey.String(),
|
||||
},
|
||||
}
|
||||
signature, err := mach.account.Internal.SignJSON(masterKeyObj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign JSON: %w", err)
|
||||
}
|
||||
masterKeyObj.Signatures = mautrix.Signatures{
|
||||
userID: map[id.KeyID]string{
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
|
||||
},
|
||||
}
|
||||
mach.Log.Trace("Signed own master key with device %v: `%v`", deviceID, signature)
|
||||
|
||||
resp, err := mach.Client.UploadSignatures(&mautrix.ReqUploadSignatures{
|
||||
userID: map[string]mautrix.ReqKeysSignatures{
|
||||
masterKey.String(): masterKeyObj,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while uploading signatures: %w", err)
|
||||
} else if len(resp.Failures) > 0 {
|
||||
return fmt.Errorf("%w: %+v", ErrSignatureUploadFail, resp.Failures)
|
||||
}
|
||||
|
||||
if err := mach.CryptoStore.PutSignature(userID, masterKey, userID, mach.account.SigningKey(), signature); err != nil {
|
||||
return fmt.Errorf("error storing signature in crypto store: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignOwnDevice creates a cross-signing signature for a device belonging to the current user and uploads it to the server.
|
||||
func (mach *OlmMachine) SignOwnDevice(device *id.Device) error {
|
||||
if device.UserID != mach.Client.UserID {
|
||||
return ErrCantSignOtherDevice
|
||||
} else if mach.CrossSigningKeys == nil || mach.CrossSigningKeys.SelfSigningKey == nil {
|
||||
return ErrSelfSigningKeyNotCached
|
||||
}
|
||||
|
||||
deviceKeys, err := mach.getFullDeviceKeys(device)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deviceKeyObj := mautrix.ReqKeysSignatures{
|
||||
UserID: device.UserID,
|
||||
DeviceID: device.DeviceID,
|
||||
Algorithms: deviceKeys.Algorithms,
|
||||
Keys: make(map[id.KeyID]string),
|
||||
}
|
||||
for keyID, key := range deviceKeys.Keys {
|
||||
deviceKeyObj.Keys[id.KeyID(keyID)] = key
|
||||
}
|
||||
|
||||
signature, err := mach.signAndUpload(deviceKeyObj, device.UserID, device.DeviceID.String(), mach.CrossSigningKeys.SelfSigningKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mach.Log.Trace("Signed own device %s with self-signing key: `%v`", device.UserID, device.DeviceID, signature)
|
||||
|
||||
if err := mach.CryptoStore.PutSignature(device.UserID, device.SigningKey, mach.Client.UserID, mach.CrossSigningKeys.SelfSigningKey.PublicKey, signature); err != nil {
|
||||
return fmt.Errorf("error storing signature in crypto store: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getFullDeviceKeys gets the full device keys object for the given device.
|
||||
// This is used because we don't cache some of the details like list of algorithms and unsupported key types.
|
||||
func (mach *OlmMachine) getFullDeviceKeys(device *id.Device) (*mautrix.DeviceKeys, error) {
|
||||
devicesKeys, err := mach.Client.QueryKeys(&mautrix.ReqQueryKeys{
|
||||
DeviceKeys: mautrix.DeviceKeysRequest{
|
||||
device.UserID: mautrix.DeviceIDList{device.DeviceID},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error querying device keys for %s: %w", device.DeviceID, err)
|
||||
}
|
||||
userKeys, ok := devicesKeys.DeviceKeys[device.UserID]
|
||||
if !ok {
|
||||
return nil, ErrUserNotInQueryResponse
|
||||
}
|
||||
deviceKeys, ok := userKeys[device.DeviceID]
|
||||
if !ok {
|
||||
return nil, ErrDeviceNotInQueryResponse
|
||||
}
|
||||
_, err = mach.validateDevice(device.UserID, device.DeviceID, deviceKeys, device)
|
||||
return &deviceKeys, err
|
||||
}
|
||||
|
||||
// signAndUpload signs the given key signatures object and uploads it to the server.
|
||||
func (mach *OlmMachine) signAndUpload(req mautrix.ReqKeysSignatures, userID id.UserID, signedThing string, key *olm.PkSigning) (string, error) {
|
||||
signature, err := key.SignJSON(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign JSON: %w", err)
|
||||
}
|
||||
req.Signatures = mautrix.Signatures{
|
||||
mach.Client.UserID: map[id.KeyID]string{
|
||||
id.NewKeyID(id.KeyAlgorithmEd25519, key.PublicKey.String()): signature,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := mach.Client.UploadSignatures(&mautrix.ReqUploadSignatures{
|
||||
userID: map[string]mautrix.ReqKeysSignatures{
|
||||
signedThing: req,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error while uploading signatures: %w", err)
|
||||
} else if len(resp.Failures) > 0 {
|
||||
return "", fmt.Errorf("%w: %+v", ErrSignatureUploadFail, resp.Failures)
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
119
vendor/maunium.net/go/mautrix/crypto/cross_sign_ssss.go
generated
vendored
Normal file
119
vendor/maunium.net/go/mautrix/crypto/cross_sign_ssss.go
generated
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// 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 (
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/ssss"
|
||||
"maunium.net/go/mautrix/crypto/utils"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
// FetchCrossSigningKeysFromSSSS fetches all the cross-signing keys from SSSS, decrypts them using the given key and stores them in the olm machine.
|
||||
func (mach *OlmMachine) FetchCrossSigningKeysFromSSSS(key *ssss.Key) error {
|
||||
masterKey, err := mach.retrieveDecryptXSigningKey(event.AccountDataCrossSigningMaster, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selfSignKey, err := mach.retrieveDecryptXSigningKey(event.AccountDataCrossSigningSelf, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userSignKey, err := mach.retrieveDecryptXSigningKey(event.AccountDataCrossSigningUser, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return mach.ImportCrossSigningKeys(CrossSigningSeeds{
|
||||
MasterKey: masterKey[:],
|
||||
SelfSigningKey: selfSignKey[:],
|
||||
UserSigningKey: userSignKey[:],
|
||||
})
|
||||
}
|
||||
|
||||
// retrieveDecryptXSigningKey retrieves the requested cross-signing key from SSSS and decrypts it using the given SSSS key.
|
||||
func (mach *OlmMachine) retrieveDecryptXSigningKey(keyName event.Type, key *ssss.Key) ([utils.AESCTRKeyLength]byte, error) {
|
||||
var decryptedKey [utils.AESCTRKeyLength]byte
|
||||
var encData ssss.EncryptedAccountDataEventContent
|
||||
|
||||
// retrieve and parse the account data for this key type from SSSS
|
||||
err := mach.Client.GetAccountData(keyName.Type, &encData)
|
||||
if err != nil {
|
||||
return decryptedKey, err
|
||||
}
|
||||
|
||||
decrypted, err := encData.Decrypt(keyName.Type, key)
|
||||
if err != nil {
|
||||
return decryptedKey, err
|
||||
}
|
||||
copy(decryptedKey[:], decrypted)
|
||||
return decryptedKey, nil
|
||||
}
|
||||
|
||||
// GenerateAndUploadCrossSigningKeys generates a new key with all corresponding cross-signing keys.
|
||||
//
|
||||
// A passphrase can be provided to generate the SSSS key. If the passphrase is empty, a random key
|
||||
// is used. The base58-formatted recovery key is the first return parameter.
|
||||
//
|
||||
// The account password of the user is required for uploading keys to the server.
|
||||
func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(userPassword, passphrase string) (string, error) {
|
||||
key, err := mach.SSSS.GenerateAndUploadKey(passphrase)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate and upload SSSS key: %w", err)
|
||||
}
|
||||
|
||||
// generate the three cross-signing keys
|
||||
keysCache, err := mach.GenerateCrossSigningKeys()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
recoveryKey := key.RecoveryKey()
|
||||
|
||||
// Store the private keys in SSSS
|
||||
if err := mach.UploadCrossSigningKeysToSSSS(key, keysCache); err != nil {
|
||||
return recoveryKey, fmt.Errorf("failed to upload cross-signing keys to SSSS: %w", err)
|
||||
}
|
||||
|
||||
// Publish cross-signing keys
|
||||
err = mach.PublishCrossSigningKeys(keysCache, func(uiResp *mautrix.RespUserInteractive) interface{} {
|
||||
return &mautrix.ReqUIAuthLogin{
|
||||
BaseAuthData: mautrix.BaseAuthData{
|
||||
Type: mautrix.AuthTypePassword,
|
||||
Session: uiResp.Session,
|
||||
},
|
||||
User: mach.Client.UserID.String(),
|
||||
Password: userPassword,
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return recoveryKey, fmt.Errorf("failed to publish cross-signing keys: %w", err)
|
||||
}
|
||||
|
||||
err = mach.SSSS.SetDefaultKeyID(key.ID)
|
||||
if err != nil {
|
||||
return recoveryKey, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err)
|
||||
}
|
||||
|
||||
return recoveryKey, nil
|
||||
}
|
||||
|
||||
// UploadCrossSigningKeysToSSSS stores the given cross-signing keys on the server encrypted with the given key.
|
||||
func (mach *OlmMachine) UploadCrossSigningKeysToSSSS(key *ssss.Key, keys *CrossSigningKeysCache) error {
|
||||
if err := mach.SSSS.SetEncryptedAccountData(event.AccountDataCrossSigningMaster, keys.MasterKey.Seed, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mach.SSSS.SetEncryptedAccountData(event.AccountDataCrossSigningSelf, keys.SelfSigningKey.Seed, key); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mach.SSSS.SetEncryptedAccountData(event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed, key); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
84
vendor/maunium.net/go/mautrix/crypto/cross_sign_store.go
generated
vendored
Normal file
84
vendor/maunium.net/go/mautrix/crypto/cross_sign_store.go
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
// Copyright (c) 2022 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/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func (mach *OlmMachine) storeCrossSigningKeys(crossSigningKeys map[id.UserID]mautrix.CrossSigningKeys, deviceKeys map[id.UserID]map[id.DeviceID]mautrix.DeviceKeys) {
|
||||
for userID, userKeys := range crossSigningKeys {
|
||||
currentKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error fetching current cross-signing keys of user %v: %v", userID, err)
|
||||
}
|
||||
if currentKeys != nil {
|
||||
for curKeyUsage, curKey := range currentKeys {
|
||||
// got a new key with the same usage as an existing key
|
||||
for _, newKeyUsage := range userKeys.Usage {
|
||||
if newKeyUsage == curKeyUsage {
|
||||
if _, ok := userKeys.Keys[id.NewKeyID(id.KeyAlgorithmEd25519, curKey.Key.String())]; !ok {
|
||||
// old key is not in the new key map, so we drop signatures made by it
|
||||
if count, err := mach.CryptoStore.DropSignaturesByKey(userID, curKey.Key); err != nil {
|
||||
mach.Log.Error("Error deleting old signatures made by %s (%s): %v", curKey, curKeyUsage, err)
|
||||
} else {
|
||||
mach.Log.Debug("Dropped %d signatures made by key %s (%s) as it has been replaced", count, curKey, curKeyUsage)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range userKeys.Keys {
|
||||
for _, usage := range userKeys.Usage {
|
||||
mach.Log.Debug("Storing cross-signing key for %s: %s (type %s)", userID, key, usage)
|
||||
if err = mach.CryptoStore.PutCrossSigningKey(userID, usage, key); err != nil {
|
||||
mach.Log.Error("Error storing cross-signing key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for signUserID, keySigs := range userKeys.Signatures {
|
||||
for signKeyID, signature := range keySigs {
|
||||
_, signKeyName := signKeyID.Parse()
|
||||
signingKey := id.Ed25519(signKeyName)
|
||||
// if the signer is one of this user's own devices, find the key from the key ID
|
||||
if signUserID == userID {
|
||||
ownDeviceID := id.DeviceID(signKeyName)
|
||||
if ownDeviceKeys, ok := deviceKeys[userID][ownDeviceID]; ok {
|
||||
signingKey = ownDeviceKeys.Keys.GetEd25519(ownDeviceID)
|
||||
mach.Log.Trace("Treating %s as the device ID -> signing key %s", signKeyName, signingKey)
|
||||
}
|
||||
}
|
||||
if len(signingKey) != 43 {
|
||||
mach.Log.Trace("Cross-signing key %s/%s/%v has a signature from an unknown key %s", userID, key, userKeys.Usage, signKeyID)
|
||||
continue
|
||||
}
|
||||
|
||||
mach.Log.Debug("Verifying cross-signing key %s/%s/%v with key %s/%s", userID, key, userKeys.Usage, signUserID, signingKey)
|
||||
if verified, err := olm.VerifySignatureJSON(userKeys, signUserID, signKeyName, signingKey); err != nil {
|
||||
mach.Log.Warn("Error while verifying signature from %s for %s: %v", signingKey, key, err)
|
||||
} else {
|
||||
if verified {
|
||||
mach.Log.Debug("Signature from %s for %s verified", signingKey, key)
|
||||
err = mach.CryptoStore.PutSignature(userID, key, signUserID, signingKey, signature)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to store signature from %s for %s: %v", signingKey, key, err)
|
||||
}
|
||||
} else {
|
||||
mach.Log.Error("Invalid signature from %s for %s", signingKey, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
103
vendor/maunium.net/go/mautrix/crypto/cross_sign_validation.go
generated
vendored
Normal file
103
vendor/maunium.net/go/mautrix/crypto/cross_sign_validation.go
generated
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
// Copyright (c) 2022 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/id"
|
||||
)
|
||||
|
||||
// ResolveTrust resolves the trust state of the device from cross-signing.
|
||||
func (mach *OlmMachine) ResolveTrust(device *id.Device) id.TrustState {
|
||||
if device.Trust == id.TrustStateVerified || device.Trust == id.TrustStateBlacklisted {
|
||||
return device.Trust
|
||||
}
|
||||
theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(device.UserID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error retrieving cross-singing key of user %v from database: %v", device.UserID, err)
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
theirMSK, ok := theirKeys[id.XSUsageMaster]
|
||||
if !ok {
|
||||
mach.Log.Error("Master key of user %v not found", device.UserID)
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
theirSSK, ok := theirKeys[id.XSUsageSelfSigning]
|
||||
if !ok {
|
||||
mach.Log.Error("Self-signing key of user %v not found", device.UserID)
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
sskSigExists, err := mach.CryptoStore.IsKeySignedBy(device.UserID, theirSSK.Key, device.UserID, theirMSK.Key)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", device.UserID, err)
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
if !sskSigExists {
|
||||
mach.Log.Warn("Self-signing key of user %v is not signed by their master key", device.UserID)
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
deviceSigExists, err := mach.CryptoStore.IsKeySignedBy(device.UserID, device.SigningKey, device.UserID, theirSSK.Key)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", device.UserID, err)
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
if deviceSigExists {
|
||||
if mach.IsUserTrusted(device.UserID) {
|
||||
return id.TrustStateCrossSignedVerified
|
||||
} else if theirMSK.Key == theirMSK.First {
|
||||
return id.TrustStateCrossSignedTOFU
|
||||
}
|
||||
return id.TrustStateCrossSignedUntrusted
|
||||
}
|
||||
return id.TrustStateUnset
|
||||
}
|
||||
|
||||
// IsDeviceTrusted returns whether a device has been determined to be trusted either through verification or cross-signing.
|
||||
func (mach *OlmMachine) IsDeviceTrusted(device *id.Device) bool {
|
||||
switch mach.ResolveTrust(device) {
|
||||
case id.TrustStateVerified, id.TrustStateCrossSignedTOFU, id.TrustStateCrossSignedVerified:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsUserTrusted returns whether a user has been determined to be trusted by our user-signing key having signed their master key.
|
||||
// In the case the user ID is our own and we have successfully retrieved our cross-signing keys, we trust our own user.
|
||||
func (mach *OlmMachine) IsUserTrusted(userID id.UserID) bool {
|
||||
csPubkeys := mach.GetOwnCrossSigningPublicKeys()
|
||||
if csPubkeys == nil {
|
||||
return false
|
||||
}
|
||||
if userID == mach.Client.UserID {
|
||||
return true
|
||||
}
|
||||
// first we verify our user-signing key
|
||||
ourUserSigningKeyTrusted, err := mach.CryptoStore.IsKeySignedBy(mach.Client.UserID, csPubkeys.UserSigningKey, mach.Client.UserID, csPubkeys.MasterKey)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error retrieving our self-singing key signatures: %v", err)
|
||||
return false
|
||||
} else if !ourUserSigningKeyTrusted {
|
||||
return false
|
||||
}
|
||||
theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error retrieving cross-singing key of user %v from database: %v", userID, err)
|
||||
return false
|
||||
}
|
||||
theirMskKey, ok := theirKeys[id.XSUsageMaster]
|
||||
if !ok {
|
||||
mach.Log.Error("Master key of user %v not found", userID)
|
||||
return false
|
||||
}
|
||||
sigExists, err := mach.CryptoStore.IsKeySignedBy(userID, theirMskKey.Key, mach.Client.UserID, csPubkeys.UserSigningKey)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", userID, err)
|
||||
return false
|
||||
}
|
||||
return sigExists
|
||||
}
|
||||
137
vendor/maunium.net/go/mautrix/crypto/decryptmegolm.go
generated
vendored
Normal file
137
vendor/maunium.net/go/mautrix/crypto/decryptmegolm.go
generated
vendored
Normal file
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2020 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
IncorrectEncryptedContentType = errors.New("event content is not instance of *event.EncryptedEventContent")
|
||||
NoSessionFound = errors.New("failed to decrypt megolm event: no session with given ID found")
|
||||
DuplicateMessageIndex = errors.New("duplicate megolm message index")
|
||||
WrongRoom = errors.New("encrypted megolm event is not intended for this room")
|
||||
DeviceKeyMismatch = errors.New("device keys in event and verified device info do not match")
|
||||
SenderKeyMismatch = errors.New("sender keys in content and megolm session do not match")
|
||||
)
|
||||
|
||||
type megolmEvent struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
Type event.Type `json:"type"`
|
||||
Content event.Content `json:"content"`
|
||||
}
|
||||
|
||||
// DecryptMegolmEvent decrypts an m.room.encrypted event where the algorithm is m.megolm.v1.aes-sha2
|
||||
func (mach *OlmMachine) DecryptMegolmEvent(evt *event.Event) (*event.Event, error) {
|
||||
content, ok := evt.Content.Parsed.(*event.EncryptedEventContent)
|
||||
if !ok {
|
||||
return nil, IncorrectEncryptedContentType
|
||||
} else if content.Algorithm != id.AlgorithmMegolmV1 {
|
||||
return nil, UnsupportedAlgorithm
|
||||
}
|
||||
sess, err := mach.CryptoStore.GetGroupSession(evt.RoomID, content.SenderKey, content.SessionID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group session: %w", err)
|
||||
} else if sess == nil {
|
||||
return nil, fmt.Errorf("%w (ID %s)", NoSessionFound, content.SessionID)
|
||||
} else if content.SenderKey != "" && content.SenderKey != sess.SenderKey {
|
||||
return nil, SenderKeyMismatch
|
||||
}
|
||||
plaintext, messageIndex, err := sess.Internal.Decrypt(content.MegolmCiphertext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt megolm event: %w", err)
|
||||
} else if ok, err = mach.CryptoStore.ValidateMessageIndex(sess.SenderKey, content.SessionID, evt.ID, messageIndex, evt.Timestamp); err != nil {
|
||||
return nil, fmt.Errorf("failed to check if message index is duplicate: %w", err)
|
||||
} else if !ok {
|
||||
return nil, DuplicateMessageIndex
|
||||
}
|
||||
|
||||
var trustLevel id.TrustState
|
||||
var forwardedKeys bool
|
||||
var device *id.Device
|
||||
ownSigningKey, ownIdentityKey := mach.account.Keys()
|
||||
if sess.SigningKey == ownSigningKey && sess.SenderKey == ownIdentityKey && len(sess.ForwardingChains) == 0 {
|
||||
trustLevel = id.TrustStateVerified
|
||||
} else {
|
||||
device, err = mach.GetOrFetchDeviceByKey(evt.Sender, sess.SenderKey)
|
||||
if err != nil {
|
||||
// We don't want to throw these errors as the message can still be decrypted.
|
||||
mach.Log.Debug("Failed to get device %s/%s to verify session %s: %v", evt.Sender, sess.SenderKey, sess.ID(), err)
|
||||
trustLevel = id.TrustStateUnknownDevice
|
||||
} else if len(sess.ForwardingChains) == 0 || (len(sess.ForwardingChains) == 1 && sess.ForwardingChains[0] == sess.SenderKey.String()) {
|
||||
if device == nil {
|
||||
mach.Log.Debug("Couldn't resolve trust level of session %s: sent by unknown device %s/%s", sess.ID(), evt.Sender, sess.SenderKey)
|
||||
trustLevel = id.TrustStateUnknownDevice
|
||||
} else if device.SigningKey != sess.SigningKey || device.IdentityKey != sess.SenderKey {
|
||||
return nil, DeviceKeyMismatch
|
||||
} else {
|
||||
trustLevel = mach.ResolveTrust(device)
|
||||
}
|
||||
} else {
|
||||
forwardedKeys = true
|
||||
lastChainItem := sess.ForwardingChains[len(sess.ForwardingChains)-1]
|
||||
device, _ = mach.CryptoStore.FindDeviceByKey(evt.Sender, id.IdentityKey(lastChainItem))
|
||||
if device != nil {
|
||||
trustLevel = mach.ResolveTrust(device)
|
||||
} else {
|
||||
mach.Log.Debug("Couldn't resolve trust level of session %s: forwarding chain ends with unknown device %s", sess.ID(), lastChainItem)
|
||||
trustLevel = id.TrustStateForwarded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
megolmEvt := &megolmEvent{}
|
||||
err = json.Unmarshal(plaintext, &megolmEvt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse megolm payload: %w", err)
|
||||
} else if megolmEvt.RoomID != evt.RoomID {
|
||||
return nil, WrongRoom
|
||||
}
|
||||
megolmEvt.Type.Class = evt.Type.Class
|
||||
err = megolmEvt.Content.ParseRaw(megolmEvt.Type)
|
||||
if err != nil {
|
||||
if event.IsUnsupportedContentType(err) {
|
||||
mach.Log.Warn("Unsupported event type %s in encrypted event %s", megolmEvt.Type.Repr(), evt.ID)
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to parse content of megolm payload event: %w", err)
|
||||
}
|
||||
}
|
||||
if content.RelatesTo != nil {
|
||||
relatable, ok := megolmEvt.Content.Parsed.(event.Relatable)
|
||||
if ok {
|
||||
if relatable.OptionalGetRelatesTo() == nil {
|
||||
relatable.SetRelatesTo(content.RelatesTo)
|
||||
} else {
|
||||
mach.Log.Trace("Not overriding relation data in %s, as encrypted payload already has it", evt.ID)
|
||||
}
|
||||
} else {
|
||||
mach.Log.Warn("Encrypted event %s has relation data, but content type %T (%s) doesn't support it", evt.ID, megolmEvt.Content.Parsed, megolmEvt.Type.String())
|
||||
}
|
||||
}
|
||||
megolmEvt.Type.Class = evt.Type.Class
|
||||
return &event.Event{
|
||||
Sender: evt.Sender,
|
||||
Type: megolmEvt.Type,
|
||||
Timestamp: evt.Timestamp,
|
||||
ID: evt.ID,
|
||||
RoomID: evt.RoomID,
|
||||
Content: megolmEvt.Content,
|
||||
Unsigned: evt.Unsigned,
|
||||
Mautrix: event.MautrixInfo{
|
||||
TrustState: trustLevel,
|
||||
TrustSource: device,
|
||||
ForwardedKeys: forwardedKeys,
|
||||
WasEncrypted: true,
|
||||
ReceivedAt: evt.Mautrix.ReceivedAt,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
247
vendor/maunium.net/go/mautrix/crypto/decryptolm.go
generated
vendored
Normal file
247
vendor/maunium.net/go/mautrix/crypto/decryptolm.go
generated
vendored
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright (c) 2021 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
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")
|
||||
)
|
||||
|
||||
// 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:"-"`
|
||||
|
||||
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) decryptOlmEvent(evt *event.Event, traceID string) (*DecryptedOlmEvent, error) {
|
||||
content, ok := evt.Content.Parsed.(*event.EncryptedEventContent)
|
||||
if !ok {
|
||||
return nil, IncorrectEncryptedContentType
|
||||
} else if content.Algorithm != id.AlgorithmOlmV1 {
|
||||
return nil, UnsupportedAlgorithm
|
||||
}
|
||||
ownContent, ok := content.OlmCiphertext[mach.account.IdentityKey()]
|
||||
if !ok {
|
||||
return nil, NotEncryptedForMe
|
||||
}
|
||||
decrypted, err := mach.decryptAndParseOlmCiphertext(evt.Sender, content.SenderKey, ownContent.Type, ownContent.Body, traceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decrypted.Source = evt
|
||||
return decrypted, nil
|
||||
}
|
||||
|
||||
type OlmEventKeys struct {
|
||||
Ed25519 id.Ed25519 `json:"ed25519"`
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) decryptAndParseOlmCiphertext(sender id.UserID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string, traceID string) (*DecryptedOlmEvent, error) {
|
||||
if olmType != id.OlmMsgTypePreKey && olmType != id.OlmMsgTypeMsg {
|
||||
return nil, UnsupportedOlmMessageType
|
||||
}
|
||||
|
||||
endTimeTrace := mach.timeTrace("decrypting olm ciphertext", traceID, 5*time.Second)
|
||||
plaintext, err := mach.tryDecryptOlmCiphertext(sender, senderKey, olmType, ciphertext, traceID)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer mach.timeTrace("parsing decrypted olm event", traceID, time.Second)()
|
||||
|
||||
var olmEvt DecryptedOlmEvent
|
||||
err = json.Unmarshal(plaintext, &olmEvt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse olm payload: %w", err)
|
||||
}
|
||||
if sender != olmEvt.Sender {
|
||||
return nil, SenderMismatch
|
||||
} else if mach.Client.UserID != olmEvt.Recipient {
|
||||
return nil, RecipientMismatch
|
||||
} else if mach.account.SigningKey() != olmEvt.RecipientKeys.Ed25519 {
|
||||
return nil, RecipientKeyMismatch
|
||||
}
|
||||
|
||||
err = olmEvt.Content.ParseRaw(olmEvt.Type)
|
||||
if err != nil && !event.IsUnsupportedContentType(err) {
|
||||
return nil, fmt.Errorf("failed to parse content of olm payload event: %w", err)
|
||||
}
|
||||
|
||||
olmEvt.SenderKey = senderKey
|
||||
|
||||
return &olmEvt, nil
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) tryDecryptOlmCiphertext(sender id.UserID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string, traceID string) ([]byte, error) {
|
||||
endTimeTrace := mach.timeTrace("waiting for olm lock", traceID, 5*time.Second)
|
||||
mach.olmLock.Lock()
|
||||
endTimeTrace()
|
||||
defer mach.olmLock.Unlock()
|
||||
|
||||
plaintext, err := mach.tryDecryptOlmCiphertextWithExistingSession(senderKey, olmType, ciphertext, traceID)
|
||||
if err != nil {
|
||||
if err == DecryptionFailedWithMatchingSession {
|
||||
mach.Log.Warn("Found matching session yet decryption failed for sender %s with key %s", sender, senderKey)
|
||||
go mach.unwedgeDevice(sender, senderKey)
|
||||
}
|
||||
return nil, fmt.Errorf("failed to decrypt olm event: %w", err)
|
||||
}
|
||||
|
||||
if plaintext != nil {
|
||||
// Decryption successful
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// Decryption failed with every known session or no known sessions, let's try to create a new session.
|
||||
//
|
||||
// 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 {
|
||||
go mach.unwedgeDevice(sender, senderKey)
|
||||
return nil, DecryptionFailedForNormalMessage
|
||||
}
|
||||
|
||||
mach.Log.Trace("Trying to create inbound session for %s/%s", sender, senderKey)
|
||||
endTimeTrace = mach.timeTrace("creating inbound olm session", traceID, time.Second)
|
||||
session, err := mach.createInboundSession(senderKey, ciphertext)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
go mach.unwedgeDevice(sender, senderKey)
|
||||
return nil, fmt.Errorf("failed to create new session from prekey message: %w", err)
|
||||
}
|
||||
mach.Log.Debug("Created inbound olm session %s for %s/%s: %s", session.ID(), sender, senderKey, session.Describe())
|
||||
|
||||
endTimeTrace = mach.timeTrace(fmt.Sprintf("decrypting prekey olm message with %s/%s", senderKey, session.ID()), traceID, time.Second)
|
||||
plaintext, err = session.Decrypt(ciphertext, olmType)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
go mach.unwedgeDevice(sender, senderKey)
|
||||
return nil, fmt.Errorf("failed to decrypt olm event with session created from prekey message: %w", err)
|
||||
}
|
||||
|
||||
endTimeTrace = mach.timeTrace(fmt.Sprintf("updating new session %s/%s in database", senderKey, session.ID()), traceID, time.Second)
|
||||
err = mach.CryptoStore.UpdateSession(senderKey, session)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to update new olm session in crypto store after decrypting: %v", err)
|
||||
}
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) tryDecryptOlmCiphertextWithExistingSession(senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string, traceID string) ([]byte, error) {
|
||||
endTimeTrace := mach.timeTrace(fmt.Sprintf("getting sessions with %s", senderKey), traceID, time.Second)
|
||||
sessions, err := mach.CryptoStore.GetSessions(senderKey)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get session for %s: %w", senderKey, err)
|
||||
}
|
||||
|
||||
for _, session := range sessions {
|
||||
if olmType == id.OlmMsgTypePreKey {
|
||||
endTimeTrace = mach.timeTrace(fmt.Sprintf("checking if prekey olm message matches session %s/%s", senderKey, session.ID()), traceID, time.Second)
|
||||
matches, err := session.Internal.MatchesInboundSession(ciphertext)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check if ciphertext matches inbound session: %w", err)
|
||||
} else if !matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
mach.Log.Trace("Trying to decrypt olm message from %s with session %s: %s", senderKey, session.ID(), session.Describe())
|
||||
endTimeTrace = mach.timeTrace(fmt.Sprintf("decrypting olm message with %s/%s", senderKey, session.ID()), traceID, time.Second)
|
||||
plaintext, err := session.Decrypt(ciphertext, olmType)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
if olmType == id.OlmMsgTypePreKey {
|
||||
return nil, DecryptionFailedWithMatchingSession
|
||||
}
|
||||
} else {
|
||||
endTimeTrace = mach.timeTrace(fmt.Sprintf("updating session %s/%s in database", senderKey, session.ID()), traceID, time.Second)
|
||||
err = mach.CryptoStore.UpdateSession(senderKey, session)
|
||||
endTimeTrace()
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to update olm session in crypto store after decrypting: %v", err)
|
||||
}
|
||||
mach.Log.Trace("Decrypted olm message from %s with session %s", senderKey, session.ID())
|
||||
return plaintext, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) createInboundSession(senderKey id.SenderKey, ciphertext string) (*OlmSession, error) {
|
||||
session, err := mach.account.NewInboundSessionFrom(senderKey, ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mach.saveAccount()
|
||||
err = mach.CryptoStore.AddSession(senderKey, session)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to store created inbound session: %v", err)
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
const MinUnwedgeInterval = 1 * time.Hour
|
||||
|
||||
func (mach *OlmMachine) unwedgeDevice(sender id.UserID, senderKey id.SenderKey) {
|
||||
mach.recentlyUnwedgedLock.Lock()
|
||||
prevUnwedge, ok := mach.recentlyUnwedged[senderKey]
|
||||
delta := time.Now().Sub(prevUnwedge)
|
||||
if ok && delta < MinUnwedgeInterval {
|
||||
mach.Log.Debug("Not creating new Olm session with %s/%s, previous recreation was %s ago", sender, senderKey, delta)
|
||||
mach.recentlyUnwedgedLock.Unlock()
|
||||
return
|
||||
}
|
||||
mach.recentlyUnwedged[senderKey] = time.Now()
|
||||
mach.recentlyUnwedgedLock.Unlock()
|
||||
|
||||
deviceIdentity, err := mach.GetOrFetchDeviceByKey(sender, senderKey)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to find device info by identity key: %v", err)
|
||||
return
|
||||
} else if deviceIdentity == nil {
|
||||
mach.Log.Warn("Didn't find identity of %s/%s, can't unwedge session", sender, senderKey)
|
||||
return
|
||||
}
|
||||
|
||||
mach.Log.Debug("Creating new Olm session with %s/%s (key: %s)", sender, deviceIdentity.DeviceID, senderKey)
|
||||
mach.devicesToUnwedgeLock.Lock()
|
||||
mach.devicesToUnwedge[senderKey] = true
|
||||
mach.devicesToUnwedgeLock.Unlock()
|
||||
err = mach.SendEncryptedToDevice(deviceIdentity, event.ToDeviceDummy, event.Content{})
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to send dummy event to unwedge session with %s/%s: %v", sender, senderKey, err)
|
||||
}
|
||||
}
|
||||
205
vendor/maunium.net/go/mautrix/crypto/devicelist.go
generated
vendored
Normal file
205
vendor/maunium.net/go/mautrix/crypto/devicelist.go
generated
vendored
Normal file
@@ -0,0 +1,205 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
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) LoadDevices(user id.UserID) map[id.DeviceID]*id.Device {
|
||||
return mach.fetchKeys([]id.UserID{user}, "", true)[user]
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) storeDeviceSelfSignatures(userID id.UserID, deviceID id.DeviceID, resp *mautrix.RespQueryKeys) {
|
||||
deviceKeys := resp.DeviceKeys[userID][deviceID]
|
||||
for signerUserID, signerKeys := range deviceKeys.Signatures {
|
||||
for signerKey, signature := range signerKeys {
|
||||
// verify and save self-signing key signature for each device
|
||||
if selfSignKeys, ok := resp.SelfSigningKeys[signerUserID]; ok {
|
||||
for _, pubKey := range selfSignKeys.Keys {
|
||||
if selfSigs, ok := deviceKeys.Signatures[signerUserID]; !ok {
|
||||
continue
|
||||
} else if _, ok := selfSigs[id.NewKeyID(id.KeyAlgorithmEd25519, pubKey.String())]; !ok {
|
||||
continue
|
||||
}
|
||||
if verified, err := olm.VerifySignatureJSON(deviceKeys, signerUserID, pubKey.String(), pubKey); verified {
|
||||
if signKey, ok := deviceKeys.Keys[id.DeviceKeyID(signerKey)]; ok {
|
||||
signature := deviceKeys.Signatures[signerUserID][id.NewKeyID(id.KeyAlgorithmEd25519, pubKey.String())]
|
||||
mach.Log.Trace("Verified self-signing signature for device %s/%s: %s", signerUserID, deviceID, signature)
|
||||
err = mach.CryptoStore.PutSignature(userID, id.Ed25519(signKey), signerUserID, pubKey, signature)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to store self-signing signature for device %s/%s: %v", signerUserID, deviceID, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
err = errors.New("invalid signature")
|
||||
}
|
||||
mach.Log.Warn("Could not verify device self-signing signature for %s/%s: %v", signerUserID, deviceID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
// save signature of device made by its own device signing key
|
||||
if signKey, ok := deviceKeys.Keys[id.DeviceKeyID(signerKey)]; ok {
|
||||
err := mach.CryptoStore.PutSignature(userID, id.Ed25519(signKey), signerUserID, id.Ed25519(signKey), signature)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to store self-signing signature for %s/%s: %v", signerUserID, signKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) fetchKeys(users []id.UserID, sinceToken string, includeUntracked bool) (data map[id.UserID]map[id.DeviceID]*id.Device) {
|
||||
// TODO this function should probably return errors
|
||||
req := &mautrix.ReqQueryKeys{
|
||||
DeviceKeys: mautrix.DeviceKeysRequest{},
|
||||
Timeout: 10 * 1000,
|
||||
Token: sinceToken,
|
||||
}
|
||||
if !includeUntracked {
|
||||
var err error
|
||||
users, err = mach.CryptoStore.FilterTrackedUsers(users)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to filter tracked user list: %v", err)
|
||||
}
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return
|
||||
}
|
||||
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))
|
||||
data = make(map[id.UserID]map[id.DeviceID]*id.Device)
|
||||
for userID, devices := range resp.DeviceKeys {
|
||||
delete(req.DeviceKeys, userID)
|
||||
|
||||
newDevices := make(map[id.DeviceID]*id.Device)
|
||||
existingDevices, err := mach.CryptoStore.GetDevices(userID)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to get existing devices for %s: %v", userID, err)
|
||||
existingDevices = make(map[id.DeviceID]*id.Device)
|
||||
}
|
||||
mach.Log.Trace("Updating devices for %s, got %d devices, have %d in store", userID, len(devices), len(existingDevices))
|
||||
changed := false
|
||||
for deviceID, deviceKeys := range devices {
|
||||
existing, ok := existingDevices[deviceID]
|
||||
if !ok {
|
||||
// New device
|
||||
changed = true
|
||||
}
|
||||
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.storeDeviceSelfSignatures(userID, deviceID, resp)
|
||||
}
|
||||
}
|
||||
mach.Log.Trace("Storing new device list for %s containing %d devices", userID, len(newDevices))
|
||||
err = mach.CryptoStore.PutDevices(userID, newDevices)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to update device list for %s: %v", userID, err)
|
||||
}
|
||||
data[userID] = newDevices
|
||||
|
||||
changed = changed || len(newDevices) != len(existingDevices)
|
||||
if changed {
|
||||
mach.OnDevicesChanged(userID)
|
||||
}
|
||||
}
|
||||
for userID := range req.DeviceKeys {
|
||||
mach.Log.Warn("Didn't get any keys for user %s", userID)
|
||||
}
|
||||
|
||||
mach.storeCrossSigningKeys(resp.MasterKeys, resp.DeviceKeys)
|
||||
mach.storeCrossSigningKeys(resp.SelfSigningKeys, resp.DeviceKeys)
|
||||
mach.storeCrossSigningKeys(resp.UserSigningKeys, resp.DeviceKeys)
|
||||
|
||||
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)
|
||||
err := mach.CryptoStore.RemoveOutboundGroupSession(roomID)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to invalidate outbound group session of %s on device change for %s: %v", roomID, userID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) validateDevice(userID id.UserID, deviceID id.DeviceID, deviceKeys mautrix.DeviceKeys, existing *id.Device) (*id.Device, error) {
|
||||
if deviceID != deviceKeys.DeviceID {
|
||||
return nil, fmt.Errorf("%w (expected %s, got %s)", MismatchingDeviceID, deviceID, deviceKeys.DeviceID)
|
||||
} else if userID != deviceKeys.UserID {
|
||||
return nil, fmt.Errorf("%w (expected %s, got %s)", MismatchingUserID, userID, deviceKeys.UserID)
|
||||
}
|
||||
|
||||
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, fmt.Errorf("%w (expected %s, got %s)", MismatchingSigningKey, existing.SigningKey, signingKey)
|
||||
}
|
||||
|
||||
ok, err := olm.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey)
|
||||
if err != nil {
|
||||
return existing, fmt.Errorf("failed to verify signature: %w", err)
|
||||
} else if !ok {
|
||||
return existing, InvalidKeySignature
|
||||
}
|
||||
|
||||
name, ok := deviceKeys.Unsigned["device_display_name"].(string)
|
||||
if !ok {
|
||||
name = string(deviceID)
|
||||
}
|
||||
|
||||
return &id.Device{
|
||||
UserID: userID,
|
||||
DeviceID: deviceID,
|
||||
IdentityKey: identityKey,
|
||||
SigningKey: signingKey,
|
||||
Trust: id.TrustStateUnset,
|
||||
Name: name,
|
||||
Deleted: false,
|
||||
}, nil
|
||||
}
|
||||
287
vendor/maunium.net/go/mautrix/crypto/encryptmegolm.go
generated
vendored
Normal file
287
vendor/maunium.net/go/mautrix/crypto/encryptmegolm.go
generated
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
// Copyright (c) 2020 Tulir Asokan
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"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 getRelatesTo(content interface{}) *event.RelatesTo {
|
||||
contentStruct, ok := content.(*event.Content)
|
||||
if ok {
|
||||
content = contentStruct.Parsed
|
||||
}
|
||||
relatable, ok := content.(event.Relatable)
|
||||
if ok {
|
||||
return relatable.OptionalGetRelatesTo()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type rawMegolmEvent struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
Type event.Type `json:"type"`
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get outbound group session: %w", err)
|
||||
} else if session == nil {
|
||||
return nil, NoGroupSession
|
||||
}
|
||||
plaintext, err := json.Marshal(&rawMegolmEvent{
|
||||
RoomID: roomID,
|
||||
Type: evtType,
|
||||
Content: content,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext, err := session.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = mach.CryptoStore.UpdateOutboundGroupSession(session)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to update megolm session in crypto store after encrypting: %v", err)
|
||||
}
|
||||
return &event.EncryptedEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
SessionID: session.ID(),
|
||||
MegolmCiphertext: ciphertext,
|
||||
RelatesTo: getRelatesTo(content),
|
||||
|
||||
// These are deprecated
|
||||
SenderKey: mach.account.IdentityKey(),
|
||||
DeviceID: mach.Client.DeviceID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) newOutboundGroupSession(roomID id.RoomID) *OutboundGroupSession {
|
||||
session := NewOutboundGroupSession(roomID, mach.StateStore.GetEncryptionEvent(roomID))
|
||||
signingKey, idKey := mach.account.Keys()
|
||||
mach.createGroupSession(idKey, signingKey, roomID, session.ID(), session.Internal.Key(), "create")
|
||||
return session
|
||||
}
|
||||
|
||||
type deviceSessionWrapper struct {
|
||||
session *OlmSession
|
||||
identity *id.Device
|
||||
}
|
||||
|
||||
// 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.Debug("Sharing group session for room %s to %v", roomID, users)
|
||||
session, err := mach.CryptoStore.GetOutboundGroupSession(roomID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get previous outbound group session: %w", err)
|
||||
} else if session != nil && session.Shared && !session.Expired() {
|
||||
return AlreadyShared
|
||||
}
|
||||
if session == nil || session.Expired() {
|
||||
session = mach.newOutboundGroupSession(roomID)
|
||||
}
|
||||
|
||||
withheldCount := 0
|
||||
toDeviceWithheld := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)}
|
||||
olmSessions := make(map[id.UserID]map[id.DeviceID]deviceSessionWrapper)
|
||||
missingSessions := make(map[id.UserID]map[id.DeviceID]*id.Device)
|
||||
missingUserSessions := make(map[id.DeviceID]*id.Device)
|
||||
var fetchKeys []id.UserID
|
||||
|
||||
for _, userID := range users {
|
||||
devices, err := mach.CryptoStore.GetDevices(userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to get devices of %s", userID)
|
||||
} else if devices == nil {
|
||||
mach.Log.Trace("GetDevices returned nil for %s, will fetch keys and retry", userID)
|
||||
fetchKeys = append(fetchKeys, userID)
|
||||
} else if len(devices) == 0 {
|
||||
mach.Log.Trace("%s has no devices, skipping", userID)
|
||||
} else {
|
||||
mach.Log.Trace("Trying to find olm sessions to encrypt %s for %s", session.ID(), userID)
|
||||
toDeviceWithheld.Messages[userID] = make(map[id.DeviceID]*event.Content)
|
||||
olmSessions[userID] = make(map[id.DeviceID]deviceSessionWrapper)
|
||||
mach.findOlmSessionsForUser(session, userID, devices, olmSessions[userID], toDeviceWithheld.Messages[userID], missingUserSessions)
|
||||
mach.Log.Trace("Found %d sessions, withholding from %d sessions and missing %d sessions to encrypt %s for for %s", len(olmSessions[userID]), len(toDeviceWithheld.Messages[userID]), len(missingUserSessions), session.ID(), userID)
|
||||
withheldCount += len(toDeviceWithheld.Messages[userID])
|
||||
if len(missingUserSessions) > 0 {
|
||||
missingSessions[userID] = missingUserSessions
|
||||
missingUserSessions = make(map[id.DeviceID]*id.Device)
|
||||
}
|
||||
if len(toDeviceWithheld.Messages[userID]) == 0 {
|
||||
delete(toDeviceWithheld.Messages, userID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(fetchKeys) > 0 {
|
||||
mach.Log.Trace("Fetching missing keys for %v", fetchKeys)
|
||||
for userID, devices := range mach.fetchKeys(fetchKeys, "", true) {
|
||||
mach.Log.Trace("Got %d device keys for %s", len(devices), userID)
|
||||
missingSessions[userID] = devices
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingSessions) > 0 {
|
||||
mach.Log.Trace("Creating missing outbound sessions")
|
||||
err = mach.createOutboundSessions(missingSessions)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to create missing outbound sessions: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for userID, devices := range missingSessions {
|
||||
if len(devices) == 0 {
|
||||
// No missing sessions
|
||||
continue
|
||||
}
|
||||
output, ok := olmSessions[userID]
|
||||
if !ok {
|
||||
output = make(map[id.DeviceID]deviceSessionWrapper)
|
||||
olmSessions[userID] = output
|
||||
}
|
||||
withheld, ok := toDeviceWithheld.Messages[userID]
|
||||
if !ok {
|
||||
withheld = make(map[id.DeviceID]*event.Content)
|
||||
toDeviceWithheld.Messages[userID] = withheld
|
||||
}
|
||||
mach.Log.Trace("Trying to find olm sessions to encrypt %s for %s (post-fetch retry)", session.ID(), userID)
|
||||
mach.findOlmSessionsForUser(session, userID, devices, output, withheld, nil)
|
||||
mach.Log.Trace("Found %d sessions and withholding from %d sessions to encrypt %s for for %s (post-fetch retry)", len(output), len(withheld), session.ID(), userID)
|
||||
withheldCount += len(toDeviceWithheld.Messages[userID])
|
||||
if len(toDeviceWithheld.Messages[userID]) == 0 {
|
||||
delete(toDeviceWithheld.Messages, userID)
|
||||
}
|
||||
}
|
||||
|
||||
err = mach.encryptAndSendGroupSession(session, olmSessions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to share group session: %w", err)
|
||||
}
|
||||
|
||||
if len(toDeviceWithheld.Messages) > 0 {
|
||||
mach.Log.Trace("Sending to-device messages to %d devices of %d users to report withheld keys in %s", withheldCount, len(toDeviceWithheld.Messages), roomID)
|
||||
// TODO remove the next 4 lines once clients support m.room_key.withheld
|
||||
_, err = mach.Client.SendToDevice(event.ToDeviceOrgMatrixRoomKeyWithheld, toDeviceWithheld)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to report withheld keys in %s (legacy event type): %v", roomID, err)
|
||||
}
|
||||
_, err = mach.Client.SendToDevice(event.ToDeviceRoomKeyWithheld, toDeviceWithheld)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to report withheld keys in %s: %v", roomID, err)
|
||||
}
|
||||
}
|
||||
|
||||
mach.Log.Debug("Group session %s for %s successfully shared", session.ID(), roomID)
|
||||
session.Shared = true
|
||||
return mach.CryptoStore.AddOutboundGroupSession(session)
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) encryptAndSendGroupSession(session *OutboundGroupSession, olmSessions map[id.UserID]map[id.DeviceID]deviceSessionWrapper) error {
|
||||
mach.olmLock.Lock()
|
||||
defer mach.olmLock.Unlock()
|
||||
mach.Log.Trace("Encrypting group session %s for all found devices", session.ID())
|
||||
deviceCount := 0
|
||||
toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)}
|
||||
for userID, sessions := range olmSessions {
|
||||
if len(sessions) == 0 {
|
||||
continue
|
||||
}
|
||||
output := make(map[id.DeviceID]*event.Content)
|
||||
toDevice.Messages[userID] = output
|
||||
for deviceID, device := range sessions {
|
||||
mach.Log.Trace("Encrypting group session %s for %s of %s", session.ID(), deviceID, userID)
|
||||
content := mach.encryptOlmEvent(device.session, device.identity, event.ToDeviceRoomKey, session.ShareContent())
|
||||
output[deviceID] = &event.Content{Parsed: content}
|
||||
deviceCount++
|
||||
mach.Log.Trace("Encrypted group session %s for %s of %s", session.ID(), deviceID, userID)
|
||||
}
|
||||
}
|
||||
|
||||
mach.Log.Trace("Sending to-device to %d devices of %d users to share group session %s", deviceCount, len(toDevice.Messages), session.ID())
|
||||
_, err := mach.Client.SendToDevice(event.ToDeviceEncrypted, toDevice)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) findOlmSessionsForUser(session *OutboundGroupSession, userID id.UserID, devices map[id.DeviceID]*id.Device, output map[id.DeviceID]deviceSessionWrapper, withheld map[id.DeviceID]*event.Content, missingOutput map[id.DeviceID]*id.Device) {
|
||||
for deviceID, device := range devices {
|
||||
userKey := UserDevice{UserID: userID, DeviceID: deviceID}
|
||||
if state := session.Users[userKey]; state != OGSNotShared {
|
||||
continue
|
||||
} else if userID == mach.Client.UserID && deviceID == mach.Client.DeviceID {
|
||||
session.Users[userKey] = OGSIgnored
|
||||
} else if device.Trust == id.TrustStateBlacklisted {
|
||||
mach.Log.Debug(
|
||||
"Not encrypting group session %s for %s of %s: device is blacklisted",
|
||||
session.ID(), deviceID, userID,
|
||||
)
|
||||
withheld[deviceID] = &event.Content{Parsed: &event.RoomKeyWithheldEventContent{
|
||||
RoomID: session.RoomID,
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
SessionID: session.ID(),
|
||||
SenderKey: mach.account.IdentityKey(),
|
||||
Code: event.RoomKeyWithheldBlacklisted,
|
||||
Reason: "Device is blacklisted",
|
||||
}}
|
||||
session.Users[userKey] = OGSIgnored
|
||||
} else if trustState := mach.ResolveTrust(device); trustState < mach.SendKeysMinTrust {
|
||||
mach.Log.Debug(
|
||||
"Not encrypting group session %s for %s of %s: device is not verified (minimum: %s, device: %s)",
|
||||
session.ID(), deviceID, userID, mach.SendKeysMinTrust, trustState,
|
||||
)
|
||||
withheld[deviceID] = &event.Content{Parsed: &event.RoomKeyWithheldEventContent{
|
||||
RoomID: session.RoomID,
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
SessionID: session.ID(),
|
||||
SenderKey: mach.account.IdentityKey(),
|
||||
Code: event.RoomKeyWithheldUnverified,
|
||||
Reason: "This device does not encrypt messages for unverified devices",
|
||||
}}
|
||||
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)
|
||||
if missingOutput != nil {
|
||||
missingOutput[deviceID] = device
|
||||
}
|
||||
} else {
|
||||
output[deviceID] = deviceSessionWrapper{
|
||||
session: deviceSession,
|
||||
identity: device,
|
||||
}
|
||||
session.Users[userKey] = OGSAlreadyShared
|
||||
}
|
||||
}
|
||||
}
|
||||
118
vendor/maunium.net/go/mautrix/crypto/encryptolm.go
generated
vendored
Normal file
118
vendor/maunium.net/go/mautrix/crypto/encryptolm.go
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
func (mach *OlmMachine) encryptOlmEvent(session *OlmSession, recipient *id.Device, evtType event.Type, content event.Content) *event.EncryptedEventContent {
|
||||
evt := &DecryptedOlmEvent{
|
||||
Sender: mach.Client.UserID,
|
||||
SenderDevice: mach.Client.DeviceID,
|
||||
Keys: OlmEventKeys{Ed25519: mach.account.SigningKey()},
|
||||
Recipient: recipient.UserID,
|
||||
RecipientKeys: OlmEventKeys{Ed25519: recipient.SigningKey},
|
||||
Type: evtType,
|
||||
Content: content,
|
||||
}
|
||||
plaintext, err := json.Marshal(evt)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
mach.Log.Trace("Encrypting olm message for %s with session %s: %s", recipient.IdentityKey, session.ID(), session.Describe())
|
||||
msgType, ciphertext := session.Encrypt(plaintext)
|
||||
err = mach.CryptoStore.UpdateSession(recipient.IdentityKey, session)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to update olm session in crypto store after encrypting: %v", err)
|
||||
}
|
||||
return &event.EncryptedEventContent{
|
||||
Algorithm: id.AlgorithmOlmV1,
|
||||
SenderKey: mach.account.IdentityKey(),
|
||||
OlmCiphertext: event.OlmCiphertexts{
|
||||
recipient.IdentityKey: {
|
||||
Type: msgType,
|
||||
Body: string(ciphertext),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) shouldCreateNewSession(identityKey id.IdentityKey) bool {
|
||||
if !mach.CryptoStore.HasSession(identityKey) {
|
||||
return true
|
||||
}
|
||||
mach.devicesToUnwedgeLock.Lock()
|
||||
_, shouldUnwedge := mach.devicesToUnwedge[identityKey]
|
||||
if shouldUnwedge {
|
||||
delete(mach.devicesToUnwedge, identityKey)
|
||||
}
|
||||
mach.devicesToUnwedgeLock.Unlock()
|
||||
return shouldUnwedge
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) createOutboundSessions(input map[id.UserID]map[id.DeviceID]*id.Device) error {
|
||||
request := make(mautrix.OneTimeKeysRequest)
|
||||
for userID, devices := range input {
|
||||
request[userID] = make(map[id.DeviceID]id.KeyAlgorithm)
|
||||
for deviceID, identity := range devices {
|
||||
if mach.shouldCreateNewSession(identity.IdentityKey) {
|
||||
request[userID][deviceID] = id.KeyAlgorithmSignedCurve25519
|
||||
}
|
||||
}
|
||||
if len(request[userID]) == 0 {
|
||||
delete(request, userID)
|
||||
}
|
||||
}
|
||||
if len(request) == 0 {
|
||||
return nil
|
||||
}
|
||||
resp, err := mach.Client.ClaimKeys(&mautrix.ReqClaimKeys{
|
||||
OneTimeKeys: request,
|
||||
Timeout: 10 * 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to claim keys: %w", err)
|
||||
}
|
||||
for userID, user := range resp.OneTimeKeys {
|
||||
for deviceID, oneTimeKeys := range user {
|
||||
var oneTimeKey mautrix.OneTimeKey
|
||||
var keyID id.KeyID
|
||||
for keyID, oneTimeKey = range oneTimeKeys {
|
||||
break
|
||||
}
|
||||
keyAlg, keyIndex := keyID.Parse()
|
||||
if keyAlg != id.KeyAlgorithmSignedCurve25519 {
|
||||
mach.Log.Warn("Unexpected key ID algorithm in one-time key response for %s of %s: %s", deviceID, userID, keyID)
|
||||
continue
|
||||
}
|
||||
identity := input[userID][deviceID]
|
||||
if ok, err := olm.VerifySignatureJSON(oneTimeKey, userID, deviceID.String(), identity.SigningKey); err != nil {
|
||||
mach.Log.Error("Failed to verify signature for %s of %s: %v", deviceID, userID, err)
|
||||
} else if !ok {
|
||||
mach.Log.Warn("Invalid signature for %s of %s", deviceID, userID)
|
||||
} else if sess, err := mach.account.Internal.NewOutboundSession(identity.IdentityKey, oneTimeKey.Key); err != nil {
|
||||
mach.Log.Error("Failed to create outbound session for %s of %s: %v", deviceID, userID, err)
|
||||
} else {
|
||||
wrapped := wrapSession(sess)
|
||||
err = mach.CryptoStore.AddSession(identity.IdentityKey, wrapped)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to store created session for %s of %s: %v", deviceID, userID, err)
|
||||
} else {
|
||||
mach.Log.Debug("Created new Olm session with %s/%s (OTK ID: %s)", userID, deviceID, keyIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
189
vendor/maunium.net/go/mautrix/crypto/keyexport.go
generated
vendored
Normal file
189
vendor/maunium.net/go/mautrix/crypto/keyexport.go
generated
vendored
Normal file
@@ -0,0 +1,189 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type SenderClaimedKeys struct {
|
||||
Ed25519 id.Ed25519 `json:"ed25519"`
|
||||
}
|
||||
|
||||
type ExportedSession struct {
|
||||
Algorithm id.Algorithm `json:"algorithm"`
|
||||
ForwardingChains []string `json:"forwarding_curve25519_key_chain"`
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
SenderKey id.SenderKey `json:"sender_key"`
|
||||
SenderClaimedKeys SenderClaimedKeys `json:"sender_claimed_keys"`
|
||||
SessionID id.SessionID `json:"session_id"`
|
||||
SessionKey string `json:"session_key"`
|
||||
}
|
||||
|
||||
// The default number of pbkdf2 rounds to use when exporting keys
|
||||
const defaultPassphraseRounds = 100000
|
||||
|
||||
const exportPrefix = "-----BEGIN MEGOLM SESSION DATA-----\n"
|
||||
const exportSuffix = "-----END MEGOLM SESSION DATA-----\n"
|
||||
|
||||
// Only version 0x01 is currently specified in the spec
|
||||
const exportVersion1 = 0x01
|
||||
|
||||
// The standard for wrapping base64 is 76 bytes
|
||||
const exportLineLengthLimit = 76
|
||||
|
||||
// Byte count for version + salt + iv + number of rounds
|
||||
const exportHeaderLength = 1 + 16 + 16 + 4
|
||||
|
||||
// SHA-256 hash length
|
||||
const exportHashLength = 32
|
||||
|
||||
func computeKey(passphrase string, salt []byte, rounds int) (encryptionKey, hashKey []byte) {
|
||||
key := pbkdf2.Key([]byte(passphrase), salt, rounds, 64, sha512.New)
|
||||
encryptionKey = key[:32]
|
||||
hashKey = key[32:]
|
||||
return
|
||||
}
|
||||
|
||||
func makeExportIV() []byte {
|
||||
iv := make([]byte, 16)
|
||||
_, err := rand.Read(iv)
|
||||
if err != nil {
|
||||
panic(olm.NotEnoughGoRandom)
|
||||
}
|
||||
// Set bit 63 to zero
|
||||
iv[7] &= 0b11111110
|
||||
return iv
|
||||
}
|
||||
|
||||
func makeExportKeys(passphrase string) (encryptionKey, hashKey, salt, iv []byte) {
|
||||
salt = make([]byte, 16)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
panic(olm.NotEnoughGoRandom)
|
||||
}
|
||||
|
||||
encryptionKey, hashKey = computeKey(passphrase, salt, defaultPassphraseRounds)
|
||||
|
||||
iv = makeExportIV()
|
||||
return
|
||||
}
|
||||
|
||||
func exportSessions(sessions []*InboundGroupSession) ([]ExportedSession, error) {
|
||||
export := make([]ExportedSession, len(sessions))
|
||||
for i, session := range sessions {
|
||||
key, err := session.Internal.Export(session.Internal.FirstKnownIndex())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to export session: %w", err)
|
||||
}
|
||||
export[i] = ExportedSession{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
ForwardingChains: session.ForwardingChains,
|
||||
RoomID: session.RoomID,
|
||||
SenderKey: session.SenderKey,
|
||||
SenderClaimedKeys: SenderClaimedKeys{},
|
||||
SessionID: session.ID(),
|
||||
SessionKey: key,
|
||||
}
|
||||
}
|
||||
return export, nil
|
||||
}
|
||||
|
||||
func exportSessionsJSON(sessions []*InboundGroupSession) ([]byte, error) {
|
||||
exportedSessions, err := exportSessions(sessions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return json.Marshal(exportedSessions)
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func formatKeyExportData(data []byte) []byte {
|
||||
base64Data := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||
base64.StdEncoding.Encode(base64Data, data)
|
||||
|
||||
// Prefix + data and newline for each 76 characters of data + suffix
|
||||
outputLength := len(exportPrefix) +
|
||||
len(base64Data) + int(math.Ceil(float64(len(base64Data))/exportLineLengthLimit)) +
|
||||
len(exportSuffix)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.Grow(outputLength)
|
||||
buf.WriteString(exportPrefix)
|
||||
for ptr := 0; ptr < len(base64Data); ptr += exportLineLengthLimit {
|
||||
buf.Write(base64Data[ptr:min(ptr+exportLineLengthLimit, len(base64Data))])
|
||||
buf.WriteRune('\n')
|
||||
}
|
||||
buf.WriteString(exportSuffix)
|
||||
if buf.Len() != buf.Cap() || buf.Len() != outputLength {
|
||||
panic(fmt.Errorf("unexpected length %d / %d / %d", buf.Len(), buf.Cap(), outputLength))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// ExportKeys exports the given Megolm sessions with the format specified in the Matrix spec.
|
||||
// See https://spec.matrix.org/v1.2/client-server-api/#key-exports
|
||||
func ExportKeys(passphrase string, sessions []*InboundGroupSession) ([]byte, error) {
|
||||
// Make all the keys necessary for exporting
|
||||
encryptionKey, hashKey, salt, iv := makeExportKeys(passphrase)
|
||||
// Export all the given sessions and put them in JSON
|
||||
unencryptedData, err := exportSessionsJSON(sessions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The export data consists of:
|
||||
// 1 byte of export format version
|
||||
// 16 bytes of salt
|
||||
// 16 bytes of IV (initialization vector)
|
||||
// 4 bytes of the number of rounds
|
||||
// the encrypted export data
|
||||
// 32 bytes of the hash of all the data above
|
||||
|
||||
exportData := make([]byte, exportHeaderLength+len(unencryptedData)+exportHashLength)
|
||||
dataWithoutHashLength := len(exportData) - exportHashLength
|
||||
|
||||
// Create the header for the export data
|
||||
exportData[0] = exportVersion1
|
||||
copy(exportData[1:17], salt)
|
||||
copy(exportData[17:33], iv)
|
||||
binary.BigEndian.PutUint32(exportData[33:37], defaultPassphraseRounds)
|
||||
|
||||
// Encrypt data with AES-256-CTR
|
||||
block, _ := aes.NewCipher(encryptionKey)
|
||||
cipher.NewCTR(block, iv).XORKeyStream(exportData[exportHeaderLength:dataWithoutHashLength], unencryptedData)
|
||||
|
||||
// Hash all the data with HMAC-SHA256 and put it at the end
|
||||
mac := hmac.New(sha256.New, hashKey)
|
||||
mac.Write(exportData[:dataWithoutHashLength])
|
||||
mac.Sum(exportData[:dataWithoutHashLength])
|
||||
|
||||
// Format the export (prefix, base64'd exportData, suffix) and return
|
||||
return formatKeyExportData(exportData), nil
|
||||
}
|
||||
150
vendor/maunium.net/go/mautrix/crypto/keyimport.go
generated
vendored
Normal file
150
vendor/maunium.net/go/mautrix/crypto/keyimport.go
generated
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
// 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 (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingExportPrefix = errors.New("invalid Matrix key export: missing prefix")
|
||||
ErrMissingExportSuffix = errors.New("invalid Matrix key export: missing suffix")
|
||||
ErrUnsupportedExportVersion = errors.New("unsupported Matrix key export format version")
|
||||
ErrMismatchingExportHash = errors.New("mismatching hash; incorrect passphrase?")
|
||||
ErrInvalidExportedAlgorithm = errors.New("session has unknown algorithm")
|
||||
ErrMismatchingExportedSessionID = errors.New("imported session has different ID than expected")
|
||||
)
|
||||
|
||||
var exportPrefixBytes, exportSuffixBytes = []byte(exportPrefix), []byte(exportSuffix)
|
||||
|
||||
func decodeKeyExport(data []byte) ([]byte, error) {
|
||||
// If the valid prefix and suffix aren't there, it's probably not a Matrix key export
|
||||
if !bytes.HasPrefix(data, exportPrefixBytes) {
|
||||
return nil, ErrMissingExportPrefix
|
||||
} else if !bytes.HasSuffix(data, exportSuffixBytes) {
|
||||
return nil, ErrMissingExportSuffix
|
||||
}
|
||||
// Remove the prefix and suffix, we don't care about them anymore
|
||||
data = data[len(exportPrefix) : len(data)-len(exportSuffix)]
|
||||
|
||||
// Allocate space for the decoded data. Ignore newlines when counting the length
|
||||
exportData := make([]byte, base64.StdEncoding.DecodedLen(len(data)-bytes.Count(data, []byte{'\n'})))
|
||||
n, err := base64.StdEncoding.Decode(exportData, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return exportData[:n], nil
|
||||
}
|
||||
|
||||
func decryptKeyExport(passphrase string, exportData []byte) ([]ExportedSession, error) {
|
||||
if exportData[0] != exportVersion1 {
|
||||
return nil, ErrUnsupportedExportVersion
|
||||
}
|
||||
|
||||
// Get all the different parts of the export
|
||||
salt := exportData[1:17]
|
||||
iv := exportData[17:33]
|
||||
passphraseRounds := binary.BigEndian.Uint32(exportData[33:37])
|
||||
dataWithoutHashLength := len(exportData) - exportHashLength
|
||||
encryptedData := exportData[exportHeaderLength:dataWithoutHashLength]
|
||||
hash := exportData[dataWithoutHashLength:]
|
||||
|
||||
// Compute the encryption and hash keys from the passphrase and salt
|
||||
encryptionKey, hashKey := computeKey(passphrase, salt, int(passphraseRounds))
|
||||
|
||||
// Compute and verify the hash. If it doesn't match, the passphrase is probably wrong
|
||||
mac := hmac.New(sha256.New, hashKey)
|
||||
mac.Write(exportData[:dataWithoutHashLength])
|
||||
if !bytes.Equal(hash, mac.Sum(nil)) {
|
||||
return nil, ErrMismatchingExportHash
|
||||
}
|
||||
|
||||
// Decrypt the export
|
||||
block, _ := aes.NewCipher(encryptionKey)
|
||||
unencryptedData := make([]byte, len(exportData)-exportHashLength-exportHeaderLength)
|
||||
cipher.NewCTR(block, iv).XORKeyStream(unencryptedData, encryptedData)
|
||||
|
||||
// Parse the decrypted JSON
|
||||
var sessionsJSON []ExportedSession
|
||||
err := json.Unmarshal(unencryptedData, &sessionsJSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid export json: %w", err)
|
||||
}
|
||||
return sessionsJSON, nil
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) importExportedRoomKey(session ExportedSession) (bool, error) {
|
||||
if session.Algorithm != id.AlgorithmMegolmV1 {
|
||||
return false, ErrInvalidExportedAlgorithm
|
||||
}
|
||||
|
||||
igsInternal, err := olm.InboundGroupSessionImport([]byte(session.SessionKey))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to import session: %w", err)
|
||||
} else if igsInternal.ID() != session.SessionID {
|
||||
return false, ErrMismatchingExportedSessionID
|
||||
}
|
||||
igs := &InboundGroupSession{
|
||||
Internal: *igsInternal,
|
||||
SigningKey: session.SenderClaimedKeys.Ed25519,
|
||||
SenderKey: session.SenderKey,
|
||||
RoomID: session.RoomID,
|
||||
// TODO should we add something here to mark the signing key as unverified like key requests do?
|
||||
ForwardingChains: session.ForwardingChains,
|
||||
}
|
||||
existingIGS, _ := mach.CryptoStore.GetGroupSession(igs.RoomID, igs.SenderKey, igs.ID())
|
||||
if existingIGS != nil && existingIGS.Internal.FirstKnownIndex() <= igs.Internal.FirstKnownIndex() {
|
||||
// We already have an equivalent or better session in the store, so don't override it.
|
||||
return false, nil
|
||||
}
|
||||
err = mach.CryptoStore.PutGroupSession(igs.RoomID, igs.SenderKey, igs.ID(), igs)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to store imported session: %w", err)
|
||||
}
|
||||
mach.markSessionReceived(igs.ID())
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ImportKeys imports data that was exported with the format specified in the Matrix spec.
|
||||
// See https://spec.matrix.org/v1.2/client-server-api/#key-exports
|
||||
func (mach *OlmMachine) ImportKeys(passphrase string, data []byte) (int, int, error) {
|
||||
exportData, err := decodeKeyExport(data)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
sessions, err := decryptKeyExport(passphrase, exportData)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, session := range sessions {
|
||||
imported, err := mach.importExportedRoomKey(session)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to import Megolm session %s/%s from file: %v", session.RoomID, session.SessionID, err)
|
||||
} else if imported {
|
||||
mach.Log.Debug("Imported Megolm session %s/%s from file", session.RoomID, session.SessionID)
|
||||
count++
|
||||
} else {
|
||||
mach.Log.Debug("Skipped Megolm session %s/%s: already in store", session.RoomID, session.SessionID)
|
||||
}
|
||||
}
|
||||
return count, len(sessions), nil
|
||||
}
|
||||
264
vendor/maunium.net/go/mautrix/crypto/keysharing.go
generated
vendored
Normal file
264
vendor/maunium.net/go/mautrix/crypto/keysharing.go
generated
vendored
Normal file
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// 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/.
|
||||
|
||||
//go:build !nosas
|
||||
// +build !nosas
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
type KeyShareRejection struct {
|
||||
Code event.RoomKeyWithheldCode
|
||||
Reason string
|
||||
}
|
||||
|
||||
var (
|
||||
// Reject a key request without responding
|
||||
KeyShareRejectNoResponse = KeyShareRejection{}
|
||||
|
||||
KeyShareRejectBlacklisted = KeyShareRejection{event.RoomKeyWithheldBlacklisted, "You have been blacklisted by this device"}
|
||||
KeyShareRejectUnverified = KeyShareRejection{event.RoomKeyWithheldUnverified, "This device does not share keys to unverified devices"}
|
||||
KeyShareRejectOtherUser = KeyShareRejection{event.RoomKeyWithheldUnauthorized, "This device does not share keys to other users"}
|
||||
KeyShareRejectUnavailable = KeyShareRejection{event.RoomKeyWithheldUnavailable, "Requested session ID not found on this device"}
|
||||
KeyShareRejectInternalError = KeyShareRejection{event.RoomKeyWithheldUnavailable, "An internal error occurred while trying to share the requested session"}
|
||||
)
|
||||
|
||||
// RequestRoomKey sends a key request for a room to the current user's devices. If the context is cancelled, then so is the key request.
|
||||
// Returns a bool channel that will get notified either when the key is received or the request is cancelled.
|
||||
//
|
||||
// Deprecated: this only supports a single key request target, so the whole automatic cancelling feature isn't very useful.
|
||||
func (mach *OlmMachine) RequestRoomKey(ctx context.Context, toUser id.UserID, toDevice id.DeviceID,
|
||||
roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (chan bool, error) {
|
||||
|
||||
requestID := mach.Client.TxnID()
|
||||
keyResponseReceived := make(chan struct{})
|
||||
mach.roomKeyRequestFilled.Store(sessionID, keyResponseReceived)
|
||||
|
||||
err := mach.SendRoomKeyRequest(roomID, senderKey, sessionID, requestID, map[id.UserID][]id.DeviceID{toUser: {toDevice}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resChan := make(chan bool, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-keyResponseReceived:
|
||||
// key request successful
|
||||
mach.Log.Debug("Key for session %v was received, cancelling other key requests", sessionID)
|
||||
resChan <- true
|
||||
case <-ctx.Done():
|
||||
// if the context is done, key request was unsuccessful
|
||||
mach.Log.Debug("Context closed (%v) before forwared key for session %v received, sending key request cancellation", ctx.Err(), sessionID)
|
||||
resChan <- false
|
||||
}
|
||||
|
||||
// send a message to all devices cancelling this key request
|
||||
mach.roomKeyRequestFilled.Delete(sessionID)
|
||||
|
||||
cancelEvtContent := &event.Content{
|
||||
Parsed: event.RoomKeyRequestEventContent{
|
||||
Action: event.KeyRequestActionCancel,
|
||||
RequestID: requestID,
|
||||
RequestingDeviceID: mach.Client.DeviceID,
|
||||
},
|
||||
}
|
||||
|
||||
toDeviceCancel := &mautrix.ReqSendToDevice{
|
||||
Messages: map[id.UserID]map[id.DeviceID]*event.Content{
|
||||
toUser: {
|
||||
toDevice: cancelEvtContent,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mach.Client.SendToDevice(event.ToDeviceRoomKeyRequest, toDeviceCancel)
|
||||
}()
|
||||
return resChan, nil
|
||||
}
|
||||
|
||||
// SendRoomKeyRequest sends a key request for the given key (identified by the room ID, sender key and session ID) to the given users.
|
||||
//
|
||||
// The request ID parameter is optional. If it's empty, a random ID will be generated.
|
||||
//
|
||||
// This function does not wait for the keys to arrive. You can use WaitForSession to wait for the session to
|
||||
// arrive (in any way, not just as a reply to this request). There's also RequestRoomKey which waits for a response
|
||||
// to the specific key request, but currently it only supports a single target device and is therefore deprecated.
|
||||
// A future function may properly support multiple targets and automatically canceling the other requests when receiving
|
||||
// the first response.
|
||||
func (mach *OlmMachine) SendRoomKeyRequest(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, requestID string, users map[id.UserID][]id.DeviceID) error {
|
||||
if len(requestID) == 0 {
|
||||
requestID = mach.Client.TxnID()
|
||||
}
|
||||
requestEvent := &event.Content{
|
||||
Parsed: &event.RoomKeyRequestEventContent{
|
||||
Action: event.KeyRequestActionRequest,
|
||||
Body: event.RequestedKeyInfo{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
RoomID: roomID,
|
||||
SenderKey: senderKey,
|
||||
SessionID: sessionID,
|
||||
},
|
||||
RequestID: requestID,
|
||||
RequestingDeviceID: mach.Client.DeviceID,
|
||||
},
|
||||
}
|
||||
|
||||
toDeviceReq := &mautrix.ReqSendToDevice{
|
||||
Messages: make(map[id.UserID]map[id.DeviceID]*event.Content, len(users)),
|
||||
}
|
||||
for user, devices := range users {
|
||||
toDeviceReq.Messages[user] = make(map[id.DeviceID]*event.Content, len(devices))
|
||||
for _, device := range devices {
|
||||
toDeviceReq.Messages[user][device] = requestEvent
|
||||
}
|
||||
}
|
||||
_, err := mach.Client.SendToDevice(event.ToDeviceRoomKeyRequest, toDeviceReq)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) importForwardedRoomKey(evt *DecryptedOlmEvent, content *event.ForwardedRoomKeyEventContent) bool {
|
||||
if content.Algorithm != id.AlgorithmMegolmV1 || evt.Keys.Ed25519 == "" {
|
||||
mach.Log.Debug("Ignoring weird forwarded room key from %s/%s: alg=%s, ed25519=%s, sessionid=%s, roomid=%s", evt.Sender, evt.SenderDevice, content.Algorithm, evt.Keys.Ed25519, content.SessionID, content.RoomID)
|
||||
return false
|
||||
}
|
||||
|
||||
igsInternal, err := olm.InboundGroupSessionImport([]byte(content.SessionKey))
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to import inbound group session: %v", err)
|
||||
return false
|
||||
} else if igsInternal.ID() != content.SessionID {
|
||||
mach.Log.Warn("Mismatched session ID while creating inbound group session")
|
||||
return false
|
||||
}
|
||||
igs := &InboundGroupSession{
|
||||
Internal: *igsInternal,
|
||||
SigningKey: evt.Keys.Ed25519,
|
||||
SenderKey: content.SenderKey,
|
||||
RoomID: content.RoomID,
|
||||
ForwardingChains: append(content.ForwardingKeyChain, evt.SenderKey.String()),
|
||||
id: content.SessionID,
|
||||
}
|
||||
err = mach.CryptoStore.PutGroupSession(content.RoomID, content.SenderKey, content.SessionID, igs)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to store new inbound group session: %v", err)
|
||||
return false
|
||||
}
|
||||
mach.markSessionReceived(content.SessionID)
|
||||
mach.Log.Trace("Received forwarded inbound group session %s/%s/%s", content.RoomID, content.SenderKey, content.SessionID)
|
||||
return true
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) rejectKeyRequest(rejection KeyShareRejection, device *id.Device, request event.RequestedKeyInfo) {
|
||||
if rejection.Code == "" {
|
||||
// If the rejection code is empty, it means don't share keys, but also don't tell the requester.
|
||||
return
|
||||
}
|
||||
content := event.RoomKeyWithheldEventContent{
|
||||
RoomID: request.RoomID,
|
||||
Algorithm: request.Algorithm,
|
||||
SessionID: request.SessionID,
|
||||
SenderKey: request.SenderKey,
|
||||
Code: rejection.Code,
|
||||
Reason: rejection.Reason,
|
||||
}
|
||||
err := mach.sendToOneDevice(device.UserID, device.DeviceID, event.ToDeviceRoomKeyWithheld, &content)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to send key share rejection %s to %s/%s: %v", rejection.Code, device.UserID, device.DeviceID, err)
|
||||
}
|
||||
err = mach.sendToOneDevice(device.UserID, device.DeviceID, event.ToDeviceOrgMatrixRoomKeyWithheld, &content)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to send key share rejection %s (org.matrix.) to %s/%s: %v", rejection.Code, device.UserID, device.DeviceID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) defaultAllowKeyShare(device *id.Device, _ event.RequestedKeyInfo) *KeyShareRejection {
|
||||
if mach.Client.UserID != device.UserID {
|
||||
mach.Log.Debug("Ignoring key request from a different user (%s)", device.UserID)
|
||||
return &KeyShareRejectOtherUser
|
||||
} else if mach.Client.DeviceID == device.DeviceID {
|
||||
mach.Log.Debug("Ignoring key request from ourselves")
|
||||
return &KeyShareRejectNoResponse
|
||||
} else if device.Trust == id.TrustStateBlacklisted {
|
||||
mach.Log.Debug("Ignoring key request from blacklisted device %s", device.DeviceID)
|
||||
return &KeyShareRejectBlacklisted
|
||||
} else if trustState := mach.ResolveTrust(device); trustState >= mach.ShareKeysMinTrust {
|
||||
mach.Log.Debug("Accepting key request from device %s (trust state: %s)", device.DeviceID, trustState)
|
||||
return nil
|
||||
} else {
|
||||
mach.Log.Debug("Ignoring key request from unverified device %s (trust state: %s)", device.DeviceID, trustState)
|
||||
return &KeyShareRejectUnverified
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) handleRoomKeyRequest(sender id.UserID, content *event.RoomKeyRequestEventContent) {
|
||||
if content.Action != event.KeyRequestActionRequest {
|
||||
return
|
||||
} else if content.RequestingDeviceID == mach.Client.DeviceID && sender == mach.Client.UserID {
|
||||
mach.Log.Debug("Ignoring key request %s from ourselves", content.RequestID)
|
||||
return
|
||||
}
|
||||
|
||||
mach.Log.Debug("Received key request %s for %s from %s/%s", content.RequestID, content.Body.SessionID, sender, content.RequestingDeviceID)
|
||||
|
||||
device, err := mach.GetOrFetchDevice(sender, content.RequestingDeviceID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to fetch device %s/%s that requested keys: %v", sender, content.RequestingDeviceID, err)
|
||||
return
|
||||
}
|
||||
|
||||
rejection := mach.AllowKeyShare(device, content.Body)
|
||||
if rejection != nil {
|
||||
mach.rejectKeyRequest(*rejection, device, content.Body)
|
||||
return
|
||||
}
|
||||
|
||||
igs, err := mach.CryptoStore.GetGroupSession(content.Body.RoomID, content.Body.SenderKey, content.Body.SessionID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to fetch group session to forward to %s/%s: %v", device.UserID, device.DeviceID, err)
|
||||
mach.rejectKeyRequest(KeyShareRejectInternalError, device, content.Body)
|
||||
return
|
||||
} else if igs == nil {
|
||||
mach.Log.Warn("Didn't find group session %s to forward to %s/%s", content.Body.SessionID, device.UserID, device.DeviceID)
|
||||
mach.rejectKeyRequest(KeyShareRejectUnavailable, device, content.Body)
|
||||
return
|
||||
}
|
||||
|
||||
exportedKey, err := igs.Internal.Export(igs.Internal.FirstKnownIndex())
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to export session %s to forward to %s/%s: %v", igs.ID(), device.UserID, device.DeviceID, err)
|
||||
mach.rejectKeyRequest(KeyShareRejectInternalError, device, content.Body)
|
||||
return
|
||||
}
|
||||
|
||||
forwardedRoomKey := event.Content{
|
||||
Parsed: &event.ForwardedRoomKeyEventContent{
|
||||
RoomKeyEventContent: event.RoomKeyEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
RoomID: igs.RoomID,
|
||||
SessionID: igs.ID(),
|
||||
SessionKey: exportedKey,
|
||||
},
|
||||
SenderKey: content.Body.SenderKey,
|
||||
ForwardingKeyChain: igs.ForwardingChains,
|
||||
SenderClaimedKey: igs.SigningKey,
|
||||
},
|
||||
}
|
||||
|
||||
if err := mach.SendEncryptedToDevice(device, event.ToDeviceForwardedRoomKey, forwardedRoomKey); err != nil {
|
||||
mach.Log.Error("Failed to send encrypted forwarded key %s to %s/%s: %v", igs.ID(), device.UserID, device.DeviceID, err)
|
||||
}
|
||||
|
||||
mach.Log.Debug("Sent encrypted forwarded key to device %s/%s for session %s", device.UserID, device.DeviceID, igs.ID())
|
||||
}
|
||||
525
vendor/maunium.net/go/mautrix/crypto/machine.go
generated
vendored
Normal file
525
vendor/maunium.net/go/mautrix/crypto/machine.go
generated
vendored
Normal file
@@ -0,0 +1,525 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/crypto/ssss"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"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{})
|
||||
Debug(message string, args ...interface{})
|
||||
Trace(message string, args ...interface{})
|
||||
}
|
||||
|
||||
// OlmMachine is the main struct for handling Matrix end-to-end encryption.
|
||||
type OlmMachine struct {
|
||||
Client *mautrix.Client
|
||||
SSSS *ssss.Machine
|
||||
Log Logger
|
||||
|
||||
CryptoStore Store
|
||||
StateStore StateStore
|
||||
|
||||
SendKeysMinTrust id.TrustState
|
||||
ShareKeysMinTrust id.TrustState
|
||||
|
||||
AllowKeyShare func(*id.Device, event.RequestedKeyInfo) *KeyShareRejection
|
||||
|
||||
DefaultSASTimeout time.Duration
|
||||
// AcceptVerificationFrom determines whether the machine will accept verification requests from this device.
|
||||
AcceptVerificationFrom func(string, *id.Device, id.RoomID) (VerificationRequestResponse, VerificationHooks)
|
||||
|
||||
account *OlmAccount
|
||||
|
||||
roomKeyRequestFilled *sync.Map
|
||||
keyVerificationTransactionState *sync.Map
|
||||
|
||||
keyWaiters map[id.SessionID]chan struct{}
|
||||
keyWaitersLock sync.Mutex
|
||||
|
||||
devicesToUnwedge map[id.IdentityKey]bool
|
||||
devicesToUnwedgeLock sync.Mutex
|
||||
recentlyUnwedged map[id.IdentityKey]time.Time
|
||||
recentlyUnwedgedLock sync.Mutex
|
||||
|
||||
olmLock sync.Mutex
|
||||
|
||||
otkUploadLock sync.Mutex
|
||||
lastOTKUpload time.Time
|
||||
|
||||
CrossSigningKeys *CrossSigningKeysCache
|
||||
crossSigningPubkeys *CrossSigningPublicKeysCache
|
||||
|
||||
crossSigningPubkeysFetched bool
|
||||
}
|
||||
|
||||
// StateStore is used by OlmMachine to get room state information that's needed for encryption.
|
||||
type StateStore interface {
|
||||
// IsEncrypted returns whether a room is encrypted.
|
||||
IsEncrypted(id.RoomID) bool
|
||||
// GetEncryptionEvent returns the encryption event's content for an encrypted room.
|
||||
GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent
|
||||
// FindSharedRooms returns the encrypted rooms that another user is also in for a user ID.
|
||||
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 {
|
||||
mach := &OlmMachine{
|
||||
Client: client,
|
||||
SSSS: ssss.NewSSSSMachine(client),
|
||||
Log: log,
|
||||
CryptoStore: cryptoStore,
|
||||
StateStore: stateStore,
|
||||
|
||||
SendKeysMinTrust: id.TrustStateUnset,
|
||||
ShareKeysMinTrust: id.TrustStateCrossSignedTOFU,
|
||||
|
||||
DefaultSASTimeout: 10 * time.Minute,
|
||||
AcceptVerificationFrom: func(string, *id.Device, id.RoomID) (VerificationRequestResponse, VerificationHooks) {
|
||||
// Reject requests by default. Users need to override this to return appropriate verification hooks.
|
||||
return RejectRequest, nil
|
||||
},
|
||||
|
||||
roomKeyRequestFilled: &sync.Map{},
|
||||
keyVerificationTransactionState: &sync.Map{},
|
||||
|
||||
keyWaiters: make(map[id.SessionID]chan struct{}),
|
||||
|
||||
devicesToUnwedge: make(map[id.IdentityKey]bool),
|
||||
recentlyUnwedged: make(map[id.IdentityKey]time.Time),
|
||||
}
|
||||
mach.AllowKeyShare = mach.defaultAllowKeyShare
|
||||
return mach
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return
|
||||
}
|
||||
if mach.account == nil {
|
||||
mach.account = NewOlmAccount()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) timeTrace(thing, trace string, expectedDuration time.Duration) func() {
|
||||
start := time.Now()
|
||||
return func() {
|
||||
duration := time.Now().Sub(start)
|
||||
if duration > expectedDuration {
|
||||
mach.Log.Warn("%s took %s (trace: %s)", thing, duration, trace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deprecated: moved to SigningKey.Fingerprint
|
||||
func Fingerprint(key id.SigningKey) string {
|
||||
return key.Fingerprint()
|
||||
}
|
||||
|
||||
// Fingerprint returns the fingerprint of the Olm account that can be used for non-interactive verification.
|
||||
func (mach *OlmMachine) Fingerprint() string {
|
||||
return mach.account.SigningKey().Fingerprint()
|
||||
}
|
||||
|
||||
// OwnIdentity returns this device's DeviceIdentity struct
|
||||
func (mach *OlmMachine) OwnIdentity() *id.Device {
|
||||
return &id.Device{
|
||||
UserID: mach.Client.UserID,
|
||||
DeviceID: mach.Client.DeviceID,
|
||||
IdentityKey: mach.account.IdentityKey(),
|
||||
SigningKey: mach.account.SigningKey(),
|
||||
Trust: id.TrustStateVerified,
|
||||
Deleted: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) AddAppserviceListener(ep *appservice.EventProcessor, az *appservice.AppService) {
|
||||
// ToDeviceForwardedRoomKey and ToDeviceRoomKey should only be present inside encrypted to-device events
|
||||
ep.On(event.ToDeviceEncrypted, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceRoomKeyRequest, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceRoomKeyWithheld, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceOrgMatrixRoomKeyWithheld, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceVerificationRequest, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceVerificationStart, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceVerificationAccept, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceVerificationKey, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceVerificationMAC, mach.HandleToDeviceEvent)
|
||||
ep.On(event.ToDeviceVerificationCancel, mach.HandleToDeviceEvent)
|
||||
ep.OnOTK(mach.HandleOTKCounts)
|
||||
ep.OnDeviceList(mach.HandleDeviceLists)
|
||||
mach.Log.Trace("Added listeners for encryption data coming from appservice transactions")
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) HandleDeviceLists(dl *mautrix.DeviceLists, since string) {
|
||||
if len(dl.Changed) > 0 {
|
||||
traceID := time.Now().Format("15:04:05.000000")
|
||||
mach.Log.Trace("Device list changes in /sync: %v (trace: %s)", dl.Changed, traceID)
|
||||
mach.fetchKeys(dl.Changed, since, false)
|
||||
mach.Log.Trace("Finished handling device list changes (trace: %s)", traceID)
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) HandleOTKCounts(otkCount *mautrix.OTKCount) {
|
||||
if (len(otkCount.UserID) > 0 && otkCount.UserID != mach.Client.UserID) || (len(otkCount.DeviceID) > 0 && otkCount.DeviceID != mach.Client.DeviceID) {
|
||||
// TODO This log probably needs to be silence-able if someone wants to use encrypted appservices with multiple e2ee sessions
|
||||
mach.Log.Debug("Dropping OTK counts targeted to %s/%s (not us)", otkCount.UserID, otkCount.DeviceID)
|
||||
return
|
||||
}
|
||||
|
||||
minCount := mach.account.Internal.MaxNumberOfOneTimeKeys() / 2
|
||||
if otkCount.SignedCurve25519 < int(minCount) {
|
||||
traceID := time.Now().Format("15:04:05.000000")
|
||||
mach.Log.Debug("Sync response said we have %d signed curve25519 keys left, sharing new ones... (trace: %s)", otkCount.SignedCurve25519, traceID)
|
||||
err := mach.ShareKeys(otkCount.SignedCurve25519)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to share keys: %v (trace: %s)", err, traceID)
|
||||
} else {
|
||||
mach.Log.Debug("Successfully shared keys (trace: %s)", traceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) bool {
|
||||
mach.HandleDeviceLists(&resp.DeviceLists, since)
|
||||
|
||||
for _, evt := range resp.ToDevice.Events {
|
||||
evt.Type.Class = event.ToDeviceEventType
|
||||
err := evt.Content.ParseRaw(evt.Type)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to parse to-device event of type %s: %v", evt.Type.Type, err)
|
||||
continue
|
||||
}
|
||||
mach.HandleToDeviceEvent(evt)
|
||||
}
|
||||
|
||||
mach.HandleOTKCounts(&resp.DeviceOTKCount)
|
||||
return true
|
||||
}
|
||||
|
||||
// HandleMemberEvent handles a single membership event.
|
||||
//
|
||||
// Currently this is not automatically called, so you must add a listener yourself:
|
||||
//
|
||||
// client.Syncer.(*mautrix.DefaultSyncer).OnEventType(event.StateMember, c.crypto.HandleMemberEvent)
|
||||
func (mach *OlmMachine) HandleMemberEvent(evt *event.Event) {
|
||||
if !mach.StateStore.IsEncrypted(evt.RoomID) {
|
||||
return
|
||||
}
|
||||
content := evt.Content.AsMember()
|
||||
if content == nil {
|
||||
return
|
||||
}
|
||||
var prevContent *event.MemberEventContent
|
||||
if evt.Unsigned.PrevContent != nil {
|
||||
_ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
|
||||
prevContent = evt.Unsigned.PrevContent.AsMember()
|
||||
}
|
||||
if prevContent == nil {
|
||||
prevContent = &event.MemberEventContent{Membership: "unknown"}
|
||||
}
|
||||
if prevContent.Membership == content.Membership ||
|
||||
(prevContent.Membership == event.MembershipInvite && content.Membership == event.MembershipJoin) ||
|
||||
(prevContent.Membership == event.MembershipBan && content.Membership == event.MembershipLeave) ||
|
||||
(prevContent.Membership == event.MembershipLeave && content.Membership == event.MembershipBan) {
|
||||
return
|
||||
}
|
||||
mach.Log.Trace("Got membership state event in %s changing %s from %s to %s, invalidating group session", evt.RoomID, evt.GetStateKey(), prevContent.Membership, content.Membership)
|
||||
err := mach.CryptoStore.RemoveOutboundGroupSession(evt.RoomID)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to invalidate outbound group session of %s: %v", evt.RoomID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if len(evt.ToUserID) > 0 && (evt.ToUserID != mach.Client.UserID || evt.ToDeviceID != mach.Client.DeviceID) {
|
||||
// TODO This log probably needs to be silence-able if someone wants to use encrypted appservices with multiple e2ee sessions
|
||||
mach.Log.Debug("Dropping to-device event targeted to %s/%s (not us)", evt.ToUserID, evt.ToDeviceID)
|
||||
return
|
||||
}
|
||||
traceID := time.Now().Format("15:04:05.000000")
|
||||
if evt.Type != event.ToDeviceEncrypted {
|
||||
mach.Log.Trace("Starting handling to-device event of type %s from %s (trace: %s)", evt.Type.Type, evt.Sender, traceID)
|
||||
}
|
||||
switch content := evt.Content.Parsed.(type) {
|
||||
case *event.EncryptedEventContent:
|
||||
mach.Log.Debug("Handling encrypted to-device event from %s/%s (trace: %s)", evt.Sender, content.SenderKey, traceID)
|
||||
decryptedEvt, err := mach.decryptOlmEvent(evt, traceID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to decrypt to-device event: %v (trace: %s)", err, traceID)
|
||||
return
|
||||
}
|
||||
mach.Log.Trace("Successfully decrypted to-device from %s/%s into type %s (sender key: %s, trace: %s)", decryptedEvt.Sender, decryptedEvt.SenderDevice, decryptedEvt.Type.String(), decryptedEvt.SenderKey, traceID)
|
||||
|
||||
switch decryptedContent := decryptedEvt.Content.Parsed.(type) {
|
||||
case *event.RoomKeyEventContent:
|
||||
mach.receiveRoomKey(decryptedEvt, decryptedContent, traceID)
|
||||
mach.Log.Trace("Handled room key event from %s/%s (trace: %s)", decryptedEvt.Sender, decryptedEvt.SenderDevice, traceID)
|
||||
case *event.ForwardedRoomKeyEventContent:
|
||||
if mach.importForwardedRoomKey(decryptedEvt, decryptedContent) {
|
||||
if ch, ok := mach.roomKeyRequestFilled.Load(decryptedContent.SessionID); ok {
|
||||
// close channel to notify listener that the key was received
|
||||
close(ch.(chan struct{}))
|
||||
}
|
||||
}
|
||||
mach.Log.Trace("Handled forwarded room key event from %s/%s (trace: %s)", decryptedEvt.Sender, decryptedEvt.SenderDevice, traceID)
|
||||
case *event.DummyEventContent:
|
||||
mach.Log.Debug("Received encrypted dummy event from %s/%s (trace: %s)", decryptedEvt.Sender, decryptedEvt.SenderDevice, traceID)
|
||||
default:
|
||||
mach.Log.Debug("Unhandled encrypted to-device event of type %s from %s/%s (trace: %s)", decryptedEvt.Type.String(), decryptedEvt.Sender, decryptedEvt.SenderDevice, traceID)
|
||||
}
|
||||
return
|
||||
case *event.RoomKeyRequestEventContent:
|
||||
go mach.handleRoomKeyRequest(evt.Sender, content)
|
||||
// verification cases
|
||||
case *event.VerificationStartEventContent:
|
||||
mach.handleVerificationStart(evt.Sender, content, content.TransactionID, 10*time.Minute, "")
|
||||
case *event.VerificationAcceptEventContent:
|
||||
mach.handleVerificationAccept(evt.Sender, content, content.TransactionID)
|
||||
case *event.VerificationKeyEventContent:
|
||||
mach.handleVerificationKey(evt.Sender, content, content.TransactionID)
|
||||
case *event.VerificationMacEventContent:
|
||||
mach.handleVerificationMAC(evt.Sender, content, content.TransactionID)
|
||||
case *event.VerificationCancelEventContent:
|
||||
mach.handleVerificationCancel(evt.Sender, content, content.TransactionID)
|
||||
case *event.VerificationRequestEventContent:
|
||||
mach.handleVerificationRequest(evt.Sender, content, content.TransactionID, "")
|
||||
case *event.RoomKeyWithheldEventContent:
|
||||
mach.handleRoomKeyWithheld(content)
|
||||
default:
|
||||
deviceID, _ := evt.Content.Raw["device_id"].(string)
|
||||
mach.Log.Trace("Unhandled to-device event of type %s from %s/%s (trace: %s)", evt.Type.Type, evt.Sender, deviceID, traceID)
|
||||
return
|
||||
}
|
||||
mach.Log.Trace("Finished handling to-device event of type %s from %s (trace: %s)", evt.Type.Type, evt.Sender, traceID)
|
||||
}
|
||||
|
||||
// GetOrFetchDevice attempts to retrieve the device identity for the given device from the store
|
||||
// and if it's not found it asks the server for it.
|
||||
func (mach *OlmMachine) GetOrFetchDevice(userID id.UserID, deviceID id.DeviceID) (*id.Device, error) {
|
||||
// get device identity
|
||||
device, err := mach.CryptoStore.GetDevice(userID, deviceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get sender device from store: %w", err)
|
||||
} else if device != nil {
|
||||
return device, nil
|
||||
}
|
||||
// try to fetch if not found
|
||||
usersToDevices := mach.fetchKeys([]id.UserID{userID}, "", true)
|
||||
if devices, ok := usersToDevices[userID]; ok {
|
||||
if device, ok = devices[deviceID]; ok {
|
||||
return device, nil
|
||||
}
|
||||
return nil, fmt.Errorf("didn't get identity for device %s of %s", deviceID, userID)
|
||||
}
|
||||
return nil, fmt.Errorf("didn't get any devices for %s", userID)
|
||||
}
|
||||
|
||||
// GetOrFetchDeviceByKey attempts to retrieve the device identity for the device with the given identity key from the
|
||||
// store and if it's not found it asks the server for it. This returns nil if the server doesn't return a device with
|
||||
// the given identity key.
|
||||
func (mach *OlmMachine) GetOrFetchDeviceByKey(userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) {
|
||||
deviceIdentity, err := mach.CryptoStore.FindDeviceByKey(userID, identityKey)
|
||||
if err != nil || deviceIdentity != nil {
|
||||
return deviceIdentity, err
|
||||
}
|
||||
mach.Log.Debug("Didn't find identity of %s/%s in crypto store, fetching from server", userID, identityKey)
|
||||
devices := mach.LoadDevices(userID)
|
||||
for _, device := range devices {
|
||||
if device.IdentityKey == identityKey {
|
||||
return device, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// SendEncryptedToDevice sends an Olm-encrypted event to the given user device.
|
||||
func (mach *OlmMachine) SendEncryptedToDevice(device *id.Device, evtType event.Type, content event.Content) error {
|
||||
if err := mach.createOutboundSessions(map[id.UserID]map[id.DeviceID]*id.Device{
|
||||
device.UserID: {
|
||||
device.DeviceID: device,
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mach.olmLock.Lock()
|
||||
defer mach.olmLock.Unlock()
|
||||
|
||||
olmSess, err := mach.CryptoStore.GetLatestSession(device.IdentityKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if olmSess == nil {
|
||||
return fmt.Errorf("didn't find created outbound session for device %s of %s", device.DeviceID, device.UserID)
|
||||
}
|
||||
|
||||
encrypted := mach.encryptOlmEvent(olmSess, device, evtType, content)
|
||||
encryptedContent := &event.Content{Parsed: &encrypted}
|
||||
|
||||
mach.Log.Debug("Sending encrypted to-device event of type %s to %s/%s (identity key: %s, olm session ID: %s)", evtType.Type, device.UserID, device.DeviceID, device.IdentityKey, olmSess.ID())
|
||||
_, err = mach.Client.SendToDevice(event.ToDeviceEncrypted,
|
||||
&mautrix.ReqSendToDevice{
|
||||
Messages: map[id.UserID]map[id.DeviceID]*event.Content{
|
||||
device.UserID: {
|
||||
device.DeviceID: encryptedContent,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) createGroupSession(senderKey id.SenderKey, signingKey id.Ed25519, roomID id.RoomID, sessionID id.SessionID, sessionKey string, traceID string) {
|
||||
igs, err := NewInboundGroupSession(senderKey, signingKey, roomID, sessionKey)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to create inbound group session: %v", err)
|
||||
return
|
||||
} else if igs.ID() != sessionID {
|
||||
mach.Log.Warn("Mismatched session ID while creating inbound group session")
|
||||
return
|
||||
}
|
||||
err = mach.CryptoStore.PutGroupSession(roomID, senderKey, sessionID, igs)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to store new inbound group session: %v", err)
|
||||
return
|
||||
}
|
||||
mach.markSessionReceived(sessionID)
|
||||
mach.Log.Debug("Received inbound group session %s / %s / %s", roomID, senderKey, sessionID)
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) markSessionReceived(id id.SessionID) {
|
||||
mach.keyWaitersLock.Lock()
|
||||
ch, ok := mach.keyWaiters[id]
|
||||
if ok {
|
||||
close(ch)
|
||||
delete(mach.keyWaiters, id)
|
||||
}
|
||||
mach.keyWaitersLock.Unlock()
|
||||
}
|
||||
|
||||
// WaitForSession waits for the given Megolm session to arrive.
|
||||
func (mach *OlmMachine) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool {
|
||||
mach.keyWaitersLock.Lock()
|
||||
ch, ok := mach.keyWaiters[sessionID]
|
||||
if !ok {
|
||||
ch := make(chan struct{})
|
||||
mach.keyWaiters[sessionID] = ch
|
||||
}
|
||||
mach.keyWaitersLock.Unlock()
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
case <-time.After(timeout):
|
||||
sess, err := mach.CryptoStore.GetGroupSession(roomID, senderKey, sessionID)
|
||||
// Check if the session somehow appeared in the store without telling us
|
||||
// We accept withheld sessions as received, as then the decryption attempt will show the error.
|
||||
return sess != nil || errors.Is(err, ErrGroupSessionWithheld)
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) receiveRoomKey(evt *DecryptedOlmEvent, content *event.RoomKeyEventContent, traceID string) {
|
||||
// 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 == "" {
|
||||
mach.Log.Debug("Ignoring weird room key from %s/%s: alg=%s, ed25519=%s, sessionid=%s, roomid=%s", evt.Sender, evt.SenderDevice, content.Algorithm, evt.Keys.Ed25519, content.SessionID, content.RoomID)
|
||||
return
|
||||
}
|
||||
|
||||
mach.createGroupSession(evt.SenderKey, evt.Keys.Ed25519, content.RoomID, content.SessionID, content.SessionKey, traceID)
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) handleRoomKeyWithheld(content *event.RoomKeyWithheldEventContent) {
|
||||
if content.Algorithm != id.AlgorithmMegolmV1 {
|
||||
mach.Log.Debug("Non-megolm room key withheld event: %+v", content)
|
||||
return
|
||||
}
|
||||
err := mach.CryptoStore.PutWithheldGroupSession(*content)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to save room key withheld event: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
start := time.Now()
|
||||
mach.otkUploadLock.Lock()
|
||||
defer mach.otkUploadLock.Unlock()
|
||||
if mach.lastOTKUpload.Add(1 * time.Minute).After(start) {
|
||||
mach.Log.Trace("Checking OTK count from server due to suspiciously close share keys requests")
|
||||
resp, err := mach.Client.UploadKeys(&mautrix.ReqUploadKeys{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check current OTK counts: %w", err)
|
||||
}
|
||||
mach.Log.Trace("Fetched current OTK count (%d) from server (input count was %d)", resp.OneTimeKeyCounts.SignedCurve25519, currentOTKCount)
|
||||
currentOTKCount = resp.OneTimeKeyCounts.SignedCurve25519
|
||||
}
|
||||
var deviceKeys *mautrix.DeviceKeys
|
||||
if !mach.account.Shared {
|
||||
deviceKeys = mach.account.getInitialKeys(mach.Client.UserID, mach.Client.DeviceID)
|
||||
mach.Log.Trace("Going to upload initial account keys")
|
||||
}
|
||||
oneTimeKeys := mach.account.getOneTimeKeys(mach.Client.UserID, mach.Client.DeviceID, currentOTKCount)
|
||||
if len(oneTimeKeys) == 0 && deviceKeys == nil {
|
||||
mach.Log.Trace("No one-time keys nor device keys got when trying to share keys")
|
||||
return nil
|
||||
}
|
||||
req := &mautrix.ReqUploadKeys{
|
||||
DeviceKeys: deviceKeys,
|
||||
OneTimeKeys: oneTimeKeys,
|
||||
}
|
||||
mach.Log.Trace("Uploading %d one-time keys", len(oneTimeKeys))
|
||||
_, err := mach.Client.UploadKeys(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mach.lastOTKUpload = time.Now()
|
||||
mach.account.Shared = true
|
||||
mach.saveAccount()
|
||||
return nil
|
||||
}
|
||||
177
vendor/maunium.net/go/mautrix/crypto/olm/LICENSE
generated
vendored
Normal file
177
vendor/maunium.net/go/mautrix/crypto/olm/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
2
vendor/maunium.net/go/mautrix/crypto/olm/README.md
generated
vendored
Normal file
2
vendor/maunium.net/go/mautrix/crypto/olm/README.md
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Go olm bindings
|
||||
Based on [Dhole/go-olm](https://github.com/Dhole/go-olm)
|
||||
398
vendor/maunium.net/go/mautrix/crypto/olm/account.go
generated
vendored
Normal file
398
vendor/maunium.net/go/mautrix/crypto/olm/account.go
generated
vendored
Normal file
@@ -0,0 +1,398 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/canonicaljson"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// Account stores a device account for end to end encrypted messaging.
|
||||
type Account struct {
|
||||
int *C.OlmAccount
|
||||
mem []byte
|
||||
}
|
||||
|
||||
// AccountFromPickled loads an Account from a pickled base64 string. Decrypts
|
||||
// the Account using the supplied key. Returns error on failure. If the key
|
||||
// doesn't match the one used to encrypt the Account then the error will be
|
||||
// "BAD_ACCOUNT_KEY". If the base64 couldn't be decoded then the error will be
|
||||
// "INVALID_BASE64".
|
||||
func AccountFromPickled(pickled, key []byte) (*Account, error) {
|
||||
if len(pickled) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
a := NewBlankAccount()
|
||||
return a, a.Unpickle(pickled, key)
|
||||
}
|
||||
|
||||
func NewBlankAccount() *Account {
|
||||
memory := make([]byte, accountSize())
|
||||
return &Account{
|
||||
int: C.olm_account(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAccount creates a new Account.
|
||||
func NewAccount() *Account {
|
||||
a := NewBlankAccount()
|
||||
random := make([]byte, a.createRandomLen()+1)
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
r := C.olm_create_account(
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&random[0]),
|
||||
C.size_t(len(random)))
|
||||
if r == errorVal() {
|
||||
panic(a.lastError())
|
||||
} else {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
// accountSize returns the size of an account object in bytes.
|
||||
func accountSize() uint {
|
||||
return uint(C.olm_account_size())
|
||||
}
|
||||
|
||||
// lastError returns an error describing the most recent error to happen to an
|
||||
// account.
|
||||
func (a *Account) lastError() error {
|
||||
return convertError(C.GoString(C.olm_account_last_error((*C.OlmAccount)(a.int))))
|
||||
}
|
||||
|
||||
// Clear clears the memory used to back this Account.
|
||||
func (a *Account) Clear() error {
|
||||
r := C.olm_clear_account((*C.OlmAccount)(a.int))
|
||||
if r == errorVal() {
|
||||
return a.lastError()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// pickleLen returns the number of bytes needed to store an Account.
|
||||
func (a *Account) pickleLen() uint {
|
||||
return uint(C.olm_pickle_account_length((*C.OlmAccount)(a.int)))
|
||||
}
|
||||
|
||||
// createRandomLen returns the number of random bytes needed to create an
|
||||
// Account.
|
||||
func (a *Account) createRandomLen() uint {
|
||||
return uint(C.olm_create_account_random_length((*C.OlmAccount)(a.int)))
|
||||
}
|
||||
|
||||
// identityKeysLen returns the size of the output buffer needed to hold the
|
||||
// identity keys.
|
||||
func (a *Account) identityKeysLen() uint {
|
||||
return uint(C.olm_account_identity_keys_length((*C.OlmAccount)(a.int)))
|
||||
}
|
||||
|
||||
// signatureLen returns the length of an ed25519 signature encoded as base64.
|
||||
func (a *Account) signatureLen() uint {
|
||||
return uint(C.olm_account_signature_length((*C.OlmAccount)(a.int)))
|
||||
}
|
||||
|
||||
// oneTimeKeysLen returns the size of the output buffer needed to hold the one
|
||||
// time keys.
|
||||
func (a *Account) oneTimeKeysLen() uint {
|
||||
return uint(C.olm_account_one_time_keys_length((*C.OlmAccount)(a.int)))
|
||||
}
|
||||
|
||||
// genOneTimeKeysRandomLen returns the number of random bytes needed to
|
||||
// generate a given number of new one time keys.
|
||||
func (a *Account) genOneTimeKeysRandomLen(num uint) uint {
|
||||
return uint(C.olm_account_generate_one_time_keys_random_length(
|
||||
(*C.OlmAccount)(a.int),
|
||||
C.size_t(num)))
|
||||
}
|
||||
|
||||
// Pickle returns an Account as a base64 string. Encrypts the Account using the
|
||||
// supplied key.
|
||||
func (a *Account) Pickle(key []byte) []byte {
|
||||
if len(key) == 0 {
|
||||
panic(NoKeyProvided)
|
||||
}
|
||||
pickled := make([]byte, a.pickleLen())
|
||||
r := C.olm_pickle_account(
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
panic(a.lastError())
|
||||
}
|
||||
return pickled[:r]
|
||||
}
|
||||
|
||||
func (a *Account) Unpickle(pickled, key []byte) error {
|
||||
if len(key) == 0 {
|
||||
return NoKeyProvided
|
||||
}
|
||||
r := C.olm_unpickle_account(
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
return a.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Account) GobEncode() ([]byte, error) {
|
||||
pickled := a.Pickle(pickleKey)
|
||||
length := base64.RawStdEncoding.DecodedLen(len(pickled))
|
||||
rawPickled := make([]byte, length)
|
||||
_, err := base64.RawStdEncoding.Decode(rawPickled, pickled)
|
||||
return rawPickled, err
|
||||
}
|
||||
|
||||
func (a *Account) GobDecode(rawPickled []byte) error {
|
||||
if a.int == nil {
|
||||
*a = *NewBlankAccount()
|
||||
}
|
||||
length := base64.RawStdEncoding.EncodedLen(len(rawPickled))
|
||||
pickled := make([]byte, length)
|
||||
base64.RawStdEncoding.Encode(pickled, rawPickled)
|
||||
return a.Unpickle(pickled, pickleKey)
|
||||
}
|
||||
|
||||
func (a *Account) MarshalJSON() ([]byte, error) {
|
||||
pickled := a.Pickle(pickleKey)
|
||||
quotes := make([]byte, len(pickled)+2)
|
||||
quotes[0] = '"'
|
||||
quotes[len(quotes)-1] = '"'
|
||||
copy(quotes[1:len(quotes)-1], pickled)
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
func (a *Account) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
|
||||
return InputNotJSONString
|
||||
}
|
||||
if a.int == nil {
|
||||
*a = *NewBlankAccount()
|
||||
}
|
||||
return a.Unpickle(data[1:len(data)-1], pickleKey)
|
||||
}
|
||||
|
||||
// IdentityKeysJSON returns the public parts of the identity keys for the Account.
|
||||
func (a *Account) IdentityKeysJSON() []byte {
|
||||
identityKeys := make([]byte, a.identityKeysLen())
|
||||
r := C.olm_account_identity_keys(
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&identityKeys[0]),
|
||||
C.size_t(len(identityKeys)))
|
||||
if r == errorVal() {
|
||||
panic(a.lastError())
|
||||
} else {
|
||||
return identityKeys
|
||||
}
|
||||
}
|
||||
|
||||
// IdentityKeys returns the public parts of the Ed25519 and Curve25519 identity
|
||||
// keys for the Account.
|
||||
func (a *Account) IdentityKeys() (id.Ed25519, id.Curve25519) {
|
||||
identityKeysJSON := a.IdentityKeysJSON()
|
||||
results := gjson.GetManyBytes(identityKeysJSON, "ed25519", "curve25519")
|
||||
return id.Ed25519(results[0].Str), id.Curve25519(results[1].Str)
|
||||
}
|
||||
|
||||
// Sign returns the signature of a message using the ed25519 key for this
|
||||
// Account.
|
||||
func (a *Account) Sign(message []byte) []byte {
|
||||
if len(message) == 0 {
|
||||
panic(EmptyInput)
|
||||
}
|
||||
signature := make([]byte, a.signatureLen())
|
||||
r := C.olm_account_sign(
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&message[0]),
|
||||
C.size_t(len(message)),
|
||||
unsafe.Pointer(&signature[0]),
|
||||
C.size_t(len(signature)))
|
||||
if r == errorVal() {
|
||||
panic(a.lastError())
|
||||
}
|
||||
return signature
|
||||
}
|
||||
|
||||
// SignJSON signs the given JSON object following the Matrix specification:
|
||||
// https://matrix.org/docs/spec/appendices#signing-json
|
||||
func (a *Account) SignJSON(obj interface{}) (string, error) {
|
||||
objJSON, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
objJSON, _ = sjson.DeleteBytes(objJSON, "unsigned")
|
||||
objJSON, _ = sjson.DeleteBytes(objJSON, "signatures")
|
||||
return string(a.Sign(canonicaljson.CanonicalJSONAssumeValid(objJSON))), nil
|
||||
}
|
||||
|
||||
// OneTimeKeys returns the public parts of the unpublished one time keys for
|
||||
// the Account.
|
||||
//
|
||||
// The returned data is a struct with the single value "Curve25519", which is
|
||||
// itself an object mapping key id to base64-encoded Curve25519 key. For
|
||||
// example:
|
||||
//
|
||||
// {
|
||||
// Curve25519: {
|
||||
// "AAAAAA": "wo76WcYtb0Vk/pBOdmduiGJ0wIEjW4IBMbbQn7aSnTo",
|
||||
// "AAAAAB": "LRvjo46L1X2vx69sS9QNFD29HWulxrmW11Up5AfAjgU"
|
||||
// }
|
||||
// }
|
||||
func (a *Account) OneTimeKeys() map[string]id.Curve25519 {
|
||||
oneTimeKeysJSON := make([]byte, a.oneTimeKeysLen())
|
||||
r := C.olm_account_one_time_keys(
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&oneTimeKeysJSON[0]),
|
||||
C.size_t(len(oneTimeKeysJSON)))
|
||||
if r == errorVal() {
|
||||
panic(a.lastError())
|
||||
}
|
||||
var oneTimeKeys struct {
|
||||
Curve25519 map[string]id.Curve25519 `json:"curve25519"`
|
||||
}
|
||||
err := json.Unmarshal(oneTimeKeysJSON, &oneTimeKeys)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return oneTimeKeys.Curve25519
|
||||
}
|
||||
|
||||
// MarkKeysAsPublished marks the current set of one time keys as being
|
||||
// published.
|
||||
func (a *Account) MarkKeysAsPublished() {
|
||||
C.olm_account_mark_keys_as_published((*C.OlmAccount)(a.int))
|
||||
}
|
||||
|
||||
// MaxNumberOfOneTimeKeys returns the largest number of one time keys this
|
||||
// Account can store.
|
||||
func (a *Account) MaxNumberOfOneTimeKeys() uint {
|
||||
return uint(C.olm_account_max_number_of_one_time_keys((*C.OlmAccount)(a.int)))
|
||||
}
|
||||
|
||||
// GenOneTimeKeys generates a number of new one time keys. If the total number
|
||||
// of keys stored by this Account exceeds MaxNumberOfOneTimeKeys then the old
|
||||
// keys are discarded.
|
||||
func (a *Account) GenOneTimeKeys(num uint) {
|
||||
random := make([]byte, a.genOneTimeKeysRandomLen(num)+1)
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
r := C.olm_account_generate_one_time_keys(
|
||||
(*C.OlmAccount)(a.int),
|
||||
C.size_t(num),
|
||||
unsafe.Pointer(&random[0]),
|
||||
C.size_t(len(random)))
|
||||
if r == errorVal() {
|
||||
panic(a.lastError())
|
||||
}
|
||||
}
|
||||
|
||||
// NewOutboundSession creates a new out-bound session for sending messages to a
|
||||
// given curve25519 identityKey and oneTimeKey. Returns error on failure. If the
|
||||
// keys couldn't be decoded as base64 then the error will be "INVALID_BASE64"
|
||||
func (a *Account) NewOutboundSession(theirIdentityKey, theirOneTimeKey id.Curve25519) (*Session, error) {
|
||||
if len(theirIdentityKey) == 0 || len(theirOneTimeKey) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankSession()
|
||||
random := make([]byte, s.createOutboundRandomLen()+1)
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
r := C.olm_create_outbound_session(
|
||||
(*C.OlmSession)(s.int),
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&([]byte(theirIdentityKey)[0])),
|
||||
C.size_t(len(theirIdentityKey)),
|
||||
unsafe.Pointer(&([]byte(theirOneTimeKey)[0])),
|
||||
C.size_t(len(theirOneTimeKey)),
|
||||
unsafe.Pointer(&random[0]),
|
||||
C.size_t(len(random)))
|
||||
if r == errorVal() {
|
||||
return nil, s.lastError()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// NewInboundSession creates a new in-bound session for sending/receiving
|
||||
// messages from an incoming PRE_KEY message. Returns error on failure. If
|
||||
// the base64 couldn't be decoded then the error will be "INVALID_BASE64". If
|
||||
// the message was for an unsupported protocol version then the error will be
|
||||
// "BAD_MESSAGE_VERSION". If the message couldn't be decoded then then the
|
||||
// error will be "BAD_MESSAGE_FORMAT". If the message refers to an unknown one
|
||||
// time key then the error will be "BAD_MESSAGE_KEY_ID".
|
||||
func (a *Account) NewInboundSession(oneTimeKeyMsg string) (*Session, error) {
|
||||
if len(oneTimeKeyMsg) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankSession()
|
||||
r := C.olm_create_inbound_session(
|
||||
(*C.OlmSession)(s.int),
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&([]byte(oneTimeKeyMsg)[0])),
|
||||
C.size_t(len(oneTimeKeyMsg)))
|
||||
if r == errorVal() {
|
||||
return nil, s.lastError()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// NewInboundSessionFrom creates a new in-bound session for sending/receiving
|
||||
// messages from an incoming PRE_KEY message. Returns error on failure. If
|
||||
// the base64 couldn't be decoded then the error will be "INVALID_BASE64". If
|
||||
// the message was for an unsupported protocol version then the error will be
|
||||
// "BAD_MESSAGE_VERSION". If the message couldn't be decoded then then the
|
||||
// error will be "BAD_MESSAGE_FORMAT". If the message refers to an unknown one
|
||||
// time key then the error will be "BAD_MESSAGE_KEY_ID".
|
||||
func (a *Account) NewInboundSessionFrom(theirIdentityKey id.Curve25519, oneTimeKeyMsg string) (*Session, error) {
|
||||
if len(theirIdentityKey) == 0 || len(oneTimeKeyMsg) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankSession()
|
||||
r := C.olm_create_inbound_session_from(
|
||||
(*C.OlmSession)(s.int),
|
||||
(*C.OlmAccount)(a.int),
|
||||
unsafe.Pointer(&([]byte(theirIdentityKey)[0])),
|
||||
C.size_t(len(theirIdentityKey)),
|
||||
unsafe.Pointer(&([]byte(oneTimeKeyMsg)[0])),
|
||||
C.size_t(len(oneTimeKeyMsg)))
|
||||
if r == errorVal() {
|
||||
return nil, s.lastError()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// RemoveOneTimeKeys removes the one time keys that the session used from the
|
||||
// Account. Returns error on failure. If the Account doesn't have any
|
||||
// matching one time keys then the error will be "BAD_MESSAGE_KEY_ID".
|
||||
func (a *Account) RemoveOneTimeKeys(s *Session) error {
|
||||
r := C.olm_remove_one_time_keys(
|
||||
(*C.OlmAccount)(a.int),
|
||||
(*C.OlmSession)(s.int))
|
||||
if r == errorVal() {
|
||||
return a.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
60
vendor/maunium.net/go/mautrix/crypto/olm/error.go
generated
vendored
Normal file
60
vendor/maunium.net/go/mautrix/crypto/olm/error.go
generated
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
package olm
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Error codes from go-olm
|
||||
var (
|
||||
EmptyInput = errors.New("empty input")
|
||||
NoKeyProvided = errors.New("no pickle key provided")
|
||||
NotEnoughGoRandom = errors.New("couldn't get enough randomness from crypto/rand")
|
||||
SignatureNotFound = errors.New("input JSON doesn't contain signature from specified device")
|
||||
InputNotJSONString = errors.New("input doesn't look like a JSON string")
|
||||
)
|
||||
|
||||
// Error codes from olm code
|
||||
var (
|
||||
NotEnoughRandom = errors.New("not enough entropy was supplied")
|
||||
OutputBufferTooSmall = errors.New("supplied output buffer is too small")
|
||||
BadMessageVersion = errors.New("the message version is unsupported")
|
||||
BadMessageFormat = errors.New("the message couldn't be decoded")
|
||||
BadMessageMAC = errors.New("the message couldn't be decrypted")
|
||||
BadMessageKeyID = errors.New("the message references an unknown key ID")
|
||||
InvalidBase64 = errors.New("the input base64 was invalid")
|
||||
BadAccountKey = errors.New("the supplied account key is invalid")
|
||||
UnknownPickleVersion = errors.New("the pickled object is too new")
|
||||
CorruptedPickle = errors.New("the pickled object couldn't be decoded")
|
||||
BadSessionKey = errors.New("attempt to initialise an inbound group session from an invalid session key")
|
||||
UnknownMessageIndex = errors.New("attempt to decode a message whose index is earlier than our earliest known session key")
|
||||
BadLegacyAccountPickle = errors.New("attempt to unpickle an account which uses pickle version 1")
|
||||
BadSignature = errors.New("received message had a bad signature")
|
||||
InputBufferTooSmall = errors.New("the input data was too small to be valid")
|
||||
)
|
||||
|
||||
var errorMap = map[string]error{
|
||||
"NOT_ENOUGH_RANDOM": NotEnoughRandom,
|
||||
"OUTPUT_BUFFER_TOO_SMALL": OutputBufferTooSmall,
|
||||
"BAD_MESSAGE_VERSION": BadMessageVersion,
|
||||
"BAD_MESSAGE_FORMAT": BadMessageFormat,
|
||||
"BAD_MESSAGE_MAC": BadMessageMAC,
|
||||
"BAD_MESSAGE_KEY_ID": BadMessageKeyID,
|
||||
"INVALID_BASE64": InvalidBase64,
|
||||
"BAD_ACCOUNT_KEY": BadAccountKey,
|
||||
"UNKNOWN_PICKLE_VERSION": UnknownPickleVersion,
|
||||
"CORRUPTED_PICKLE": CorruptedPickle,
|
||||
"BAD_SESSION_KEY": BadSessionKey,
|
||||
"UNKNOWN_MESSAGE_INDEX": UnknownMessageIndex,
|
||||
"BAD_LEGACY_ACCOUNT_PICKLE": BadLegacyAccountPickle,
|
||||
"BAD_SIGNATURE": BadSignature,
|
||||
"INPUT_BUFFER_TOO_SMALL": InputBufferTooSmall,
|
||||
}
|
||||
|
||||
func convertError(errCode string) error {
|
||||
err, ok := errorMap[errCode]
|
||||
if ok {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("unknown error: %s", errCode)
|
||||
}
|
||||
302
vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go
generated
vendored
Normal file
302
vendor/maunium.net/go/mautrix/crypto/olm/inboundgroupsession.go
generated
vendored
Normal file
@@ -0,0 +1,302 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"unsafe"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// InboundGroupSession stores an inbound encrypted messaging session for a
|
||||
// group.
|
||||
type InboundGroupSession struct {
|
||||
int *C.OlmInboundGroupSession
|
||||
mem []byte
|
||||
}
|
||||
|
||||
// InboundGroupSessionFromPickled loads an InboundGroupSession from a pickled
|
||||
// base64 string. Decrypts the InboundGroupSession using the supplied key.
|
||||
// Returns error on failure. If the key doesn't match the one used to encrypt
|
||||
// the InboundGroupSession then the error will be "BAD_SESSION_KEY". If the
|
||||
// base64 couldn't be decoded then the error will be "INVALID_BASE64".
|
||||
func InboundGroupSessionFromPickled(pickled, key []byte) (*InboundGroupSession, error) {
|
||||
if len(pickled) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
lenKey := len(key)
|
||||
if lenKey == 0 {
|
||||
key = []byte(" ")
|
||||
}
|
||||
s := NewBlankInboundGroupSession()
|
||||
return s, s.Unpickle(pickled, key)
|
||||
}
|
||||
|
||||
// NewInboundGroupSession creates a new inbound group session from a key
|
||||
// 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".
|
||||
func NewInboundGroupSession(sessionKey []byte) (*InboundGroupSession, error) {
|
||||
if len(sessionKey) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankInboundGroupSession()
|
||||
r := C.olm_init_inbound_group_session(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&sessionKey[0]),
|
||||
C.size_t(len(sessionKey)))
|
||||
if r == errorVal() {
|
||||
return nil, s.lastError()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// InboundGroupSessionImport imports an inbound group session from a previous
|
||||
// export. 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".
|
||||
func InboundGroupSessionImport(sessionKey []byte) (*InboundGroupSession, error) {
|
||||
if len(sessionKey) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankInboundGroupSession()
|
||||
r := C.olm_import_inbound_group_session(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&sessionKey[0]),
|
||||
C.size_t(len(sessionKey)))
|
||||
if r == errorVal() {
|
||||
return nil, s.lastError()
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// inboundGroupSessionSize is the size of an inbound group session object in
|
||||
// bytes.
|
||||
func inboundGroupSessionSize() uint {
|
||||
return uint(C.olm_inbound_group_session_size())
|
||||
}
|
||||
|
||||
// newInboundGroupSession initialises an empty InboundGroupSession.
|
||||
func NewBlankInboundGroupSession() *InboundGroupSession {
|
||||
memory := make([]byte, inboundGroupSessionSize())
|
||||
return &InboundGroupSession{
|
||||
int: C.olm_inbound_group_session(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// lastError returns an error describing the most recent error to happen to an
|
||||
// inbound group session.
|
||||
func (s *InboundGroupSession) lastError() error {
|
||||
return convertError(C.GoString(C.olm_inbound_group_session_last_error((*C.OlmInboundGroupSession)(s.int))))
|
||||
}
|
||||
|
||||
// Clear clears the memory used to back this InboundGroupSession.
|
||||
func (s *InboundGroupSession) Clear() error {
|
||||
r := C.olm_clear_inbound_group_session((*C.OlmInboundGroupSession)(s.int))
|
||||
if r == errorVal() {
|
||||
return s.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pickleLen returns the number of bytes needed to store an inbound group
|
||||
// session.
|
||||
func (s *InboundGroupSession) pickleLen() uint {
|
||||
return uint(C.olm_pickle_inbound_group_session_length((*C.OlmInboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// Pickle returns an InboundGroupSession as a base64 string. Encrypts the
|
||||
// InboundGroupSession using the supplied key.
|
||||
func (s *InboundGroupSession) Pickle(key []byte) []byte {
|
||||
if len(key) == 0 {
|
||||
panic(NoKeyProvided)
|
||||
}
|
||||
pickled := make([]byte, s.pickleLen())
|
||||
r := C.olm_pickle_inbound_group_session(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return pickled[:r]
|
||||
}
|
||||
|
||||
func (s *InboundGroupSession) Unpickle(pickled, key []byte) error {
|
||||
if len(key) == 0 {
|
||||
return NoKeyProvided
|
||||
} else if len(pickled) == 0 {
|
||||
return EmptyInput
|
||||
}
|
||||
r := C.olm_unpickle_inbound_group_session(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
return s.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundGroupSession) GobEncode() ([]byte, error) {
|
||||
pickled := s.Pickle(pickleKey)
|
||||
length := base64.RawStdEncoding.DecodedLen(len(pickled))
|
||||
rawPickled := make([]byte, length)
|
||||
_, err := base64.RawStdEncoding.Decode(rawPickled, pickled)
|
||||
return rawPickled, err
|
||||
}
|
||||
|
||||
func (s *InboundGroupSession) GobDecode(rawPickled []byte) error {
|
||||
if s == nil || s.int == nil {
|
||||
*s = *NewBlankInboundGroupSession()
|
||||
}
|
||||
length := base64.RawStdEncoding.EncodedLen(len(rawPickled))
|
||||
pickled := make([]byte, length)
|
||||
base64.RawStdEncoding.Encode(pickled, rawPickled)
|
||||
return s.Unpickle(pickled, pickleKey)
|
||||
}
|
||||
|
||||
func (s *InboundGroupSession) MarshalJSON() ([]byte, error) {
|
||||
pickled := s.Pickle(pickleKey)
|
||||
quotes := make([]byte, len(pickled)+2)
|
||||
quotes[0] = '"'
|
||||
quotes[len(quotes)-1] = '"'
|
||||
copy(quotes[1:len(quotes)-1], pickled)
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
func (s *InboundGroupSession) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
|
||||
return InputNotJSONString
|
||||
}
|
||||
if s == nil || s.int == nil {
|
||||
*s = *NewBlankInboundGroupSession()
|
||||
}
|
||||
return s.Unpickle(data[1:len(data)-1], pickleKey)
|
||||
}
|
||||
|
||||
func clone(original []byte) []byte {
|
||||
clone := make([]byte, len(original))
|
||||
copy(clone, original)
|
||||
return clone
|
||||
}
|
||||
|
||||
// decryptMaxPlaintextLen returns the maximum number of bytes of plain-text a
|
||||
// given message could decode to. The actual size could be different due to
|
||||
// padding. Returns error on failure. If the message base64 couldn't be
|
||||
// decoded then the error will be "INVALID_BASE64". If the message is for an
|
||||
// unsupported version of the protocol then the error will be
|
||||
// "BAD_MESSAGE_VERSION". If the message couldn't be decoded then the error
|
||||
// will be "BAD_MESSAGE_FORMAT".
|
||||
func (s *InboundGroupSession) decryptMaxPlaintextLen(message []byte) (uint, error) {
|
||||
if len(message) == 0 {
|
||||
return 0, EmptyInput
|
||||
}
|
||||
// olm_group_decrypt_max_plaintext_length destroys the input, so we have to clone it
|
||||
message = clone(message)
|
||||
r := C.olm_group_decrypt_max_plaintext_length(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&message[0]),
|
||||
C.size_t(len(message)))
|
||||
if r == errorVal() {
|
||||
return 0, s.lastError()
|
||||
}
|
||||
return uint(r), nil
|
||||
}
|
||||
|
||||
// Decrypt decrypts a message using the InboundGroupSession. Returns the the
|
||||
// plain-text and message index on success. Returns error on failure. If the
|
||||
// base64 couldn't be decoded then the error will be "INVALID_BASE64". If the
|
||||
// message is for an unsupported version of the protocol then the error will be
|
||||
// "BAD_MESSAGE_VERSION". If the message couldn't be decoded then the error
|
||||
// will be BAD_MESSAGE_FORMAT". If the MAC on the message was invalid then the
|
||||
// error will be "BAD_MESSAGE_MAC". If we do not have a session key
|
||||
// corresponding to the message's index (ie, it was sent before the session key
|
||||
// was shared with us) the error will be "OLM_UNKNOWN_MESSAGE_INDEX".
|
||||
func (s *InboundGroupSession) Decrypt(message []byte) ([]byte, uint, error) {
|
||||
if len(message) == 0 {
|
||||
return nil, 0, EmptyInput
|
||||
}
|
||||
decryptMaxPlaintextLen, err := s.decryptMaxPlaintextLen(message)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
plaintext := make([]byte, decryptMaxPlaintextLen)
|
||||
var messageIndex uint32
|
||||
r := C.olm_group_decrypt(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&message[0]),
|
||||
C.size_t(len(message)),
|
||||
(*C.uint8_t)(&plaintext[0]),
|
||||
C.size_t(len(plaintext)),
|
||||
(*C.uint32_t)(&messageIndex))
|
||||
if r == errorVal() {
|
||||
return nil, 0, s.lastError()
|
||||
}
|
||||
return plaintext[:r], uint(messageIndex), nil
|
||||
}
|
||||
|
||||
// sessionIdLen returns the number of bytes needed to store a session ID.
|
||||
func (s *InboundGroupSession) sessionIdLen() uint {
|
||||
return uint(C.olm_inbound_group_session_id_length((*C.OlmInboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// ID returns a base64-encoded identifier for this session.
|
||||
func (s *InboundGroupSession) ID() id.SessionID {
|
||||
sessionID := make([]byte, s.sessionIdLen())
|
||||
r := C.olm_inbound_group_session_id(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&sessionID[0]),
|
||||
C.size_t(len(sessionID)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return id.SessionID(sessionID[:r])
|
||||
}
|
||||
|
||||
// FirstKnownIndex returns the first message index we know how to decrypt.
|
||||
func (s *InboundGroupSession) FirstKnownIndex() uint32 {
|
||||
return uint32(C.olm_inbound_group_session_first_known_index((*C.OlmInboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// IsVerified check if the session has been verified as a valid session. (A
|
||||
// session is verified either because the original session share was signed, or
|
||||
// because we have subsequently successfully decrypted a message.)
|
||||
func (s *InboundGroupSession) IsVerified() uint {
|
||||
return uint(C.olm_inbound_group_session_is_verified((*C.OlmInboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// exportLen returns the number of bytes needed to export an inbound group
|
||||
// session.
|
||||
func (s *InboundGroupSession) exportLen() uint {
|
||||
return uint(C.olm_export_inbound_group_session_length((*C.OlmInboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// Export returns the base64-encoded ratchet key for this session, at the given
|
||||
// index, in a format which can be used by
|
||||
// InboundGroupSession.InboundGroupSessionImport(). Encrypts the
|
||||
// InboundGroupSession using the supplied key. Returns error on failure.
|
||||
// if we do not have a session key corresponding to the given index (ie, it was
|
||||
// sent before the session key was shared with us) the error will be
|
||||
// "OLM_UNKNOWN_MESSAGE_INDEX".
|
||||
func (s *InboundGroupSession) Export(messageIndex uint32) (string, error) {
|
||||
key := make([]byte, s.exportLen())
|
||||
r := C.olm_export_inbound_group_session(
|
||||
(*C.OlmInboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
C.uint32_t(messageIndex))
|
||||
if r == errorVal() {
|
||||
return "", s.lastError()
|
||||
}
|
||||
return string(key[:r]), nil
|
||||
}
|
||||
32
vendor/maunium.net/go/mautrix/crypto/olm/olm.go
generated
vendored
Normal file
32
vendor/maunium.net/go/mautrix/crypto/olm/olm.go
generated
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
import "C"
|
||||
import (
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// Signatures is the data structure used to sign JSON objects.
|
||||
type Signatures map[id.UserID]map[id.DeviceKeyID]string
|
||||
|
||||
// Version returns the version number of the olm library.
|
||||
func Version() (major, minor, patch uint8) {
|
||||
C.olm_get_library_version(
|
||||
(*C.uint8_t)(&major),
|
||||
(*C.uint8_t)(&minor),
|
||||
(*C.uint8_t)(&patch))
|
||||
return
|
||||
}
|
||||
|
||||
// errorVal returns the value that olm functions return if there was an error.
|
||||
func errorVal() C.size_t {
|
||||
return C.olm_error()
|
||||
}
|
||||
|
||||
var pickleKey = []byte("maunium.net/go/mautrix/crypto/olm")
|
||||
|
||||
// SetPickleKey sets the global pickle key used when encoding structs with Gob or JSON.
|
||||
func SetPickleKey(key []byte) {
|
||||
pickleKey = key
|
||||
}
|
||||
233
vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go
generated
vendored
Normal file
233
vendor/maunium.net/go/mautrix/crypto/olm/outboundgroupsession.go
generated
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"unsafe"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// OutboundGroupSession stores an outbound encrypted messaging session for a
|
||||
// group.
|
||||
type OutboundGroupSession struct {
|
||||
int *C.OlmOutboundGroupSession
|
||||
mem []byte
|
||||
}
|
||||
|
||||
// OutboundGroupSessionFromPickled loads an OutboundGroupSession from a pickled
|
||||
// base64 string. Decrypts the OutboundGroupSession using the supplied key.
|
||||
// Returns error on failure. If the key doesn't match the one used to encrypt
|
||||
// the OutboundGroupSession then the error will be "BAD_SESSION_KEY". If the
|
||||
// base64 couldn't be decoded then the error will be "INVALID_BASE64".
|
||||
func OutboundGroupSessionFromPickled(pickled, key []byte) (*OutboundGroupSession, error) {
|
||||
if len(pickled) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankOutboundGroupSession()
|
||||
return s, s.Unpickle(pickled, key)
|
||||
}
|
||||
|
||||
// NewOutboundGroupSession creates a new outbound group session.
|
||||
func NewOutboundGroupSession() *OutboundGroupSession {
|
||||
s := NewBlankOutboundGroupSession()
|
||||
random := make([]byte, s.createRandomLen()+1)
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
r := C.olm_init_outbound_group_session(
|
||||
(*C.OlmOutboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&random[0]),
|
||||
C.size_t(len(random)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// outboundGroupSessionSize is the size of an outbound group session object in
|
||||
// bytes.
|
||||
func outboundGroupSessionSize() uint {
|
||||
return uint(C.olm_outbound_group_session_size())
|
||||
}
|
||||
|
||||
// newOutboundGroupSession initialises an empty OutboundGroupSession.
|
||||
func NewBlankOutboundGroupSession() *OutboundGroupSession {
|
||||
memory := make([]byte, outboundGroupSessionSize())
|
||||
return &OutboundGroupSession{
|
||||
int: C.olm_outbound_group_session(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// lastError returns an error describing the most recent error to happen to an
|
||||
// outbound group session.
|
||||
func (s *OutboundGroupSession) lastError() error {
|
||||
return convertError(C.GoString(C.olm_outbound_group_session_last_error((*C.OlmOutboundGroupSession)(s.int))))
|
||||
}
|
||||
|
||||
// Clear clears the memory used to back this OutboundGroupSession.
|
||||
func (s *OutboundGroupSession) Clear() error {
|
||||
r := C.olm_clear_outbound_group_session((*C.OlmOutboundGroupSession)(s.int))
|
||||
if r == errorVal() {
|
||||
return s.lastError()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// pickleLen returns the number of bytes needed to store an outbound group
|
||||
// session.
|
||||
func (s *OutboundGroupSession) pickleLen() uint {
|
||||
return uint(C.olm_pickle_outbound_group_session_length((*C.OlmOutboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// Pickle returns an OutboundGroupSession as a base64 string. Encrypts the
|
||||
// OutboundGroupSession using the supplied key.
|
||||
func (s *OutboundGroupSession) Pickle(key []byte) []byte {
|
||||
if len(key) == 0 {
|
||||
panic(NoKeyProvided)
|
||||
}
|
||||
pickled := make([]byte, s.pickleLen())
|
||||
r := C.olm_pickle_outbound_group_session(
|
||||
(*C.OlmOutboundGroupSession)(s.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return pickled[:r]
|
||||
}
|
||||
|
||||
func (s *OutboundGroupSession) Unpickle(pickled, key []byte) error {
|
||||
if len(key) == 0 {
|
||||
return NoKeyProvided
|
||||
}
|
||||
r := C.olm_unpickle_outbound_group_session(
|
||||
(*C.OlmOutboundGroupSession)(s.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
return s.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *OutboundGroupSession) GobEncode() ([]byte, error) {
|
||||
pickled := s.Pickle(pickleKey)
|
||||
length := base64.RawStdEncoding.DecodedLen(len(pickled))
|
||||
rawPickled := make([]byte, length)
|
||||
_, err := base64.RawStdEncoding.Decode(rawPickled, pickled)
|
||||
return rawPickled, err
|
||||
}
|
||||
|
||||
func (s *OutboundGroupSession) GobDecode(rawPickled []byte) error {
|
||||
if s == nil || s.int == nil {
|
||||
*s = *NewBlankOutboundGroupSession()
|
||||
}
|
||||
length := base64.RawStdEncoding.EncodedLen(len(rawPickled))
|
||||
pickled := make([]byte, length)
|
||||
base64.RawStdEncoding.Encode(pickled, rawPickled)
|
||||
return s.Unpickle(pickled, pickleKey)
|
||||
}
|
||||
|
||||
func (s *OutboundGroupSession) MarshalJSON() ([]byte, error) {
|
||||
pickled := s.Pickle(pickleKey)
|
||||
quotes := make([]byte, len(pickled)+2)
|
||||
quotes[0] = '"'
|
||||
quotes[len(quotes)-1] = '"'
|
||||
copy(quotes[1:len(quotes)-1], pickled)
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
func (s *OutboundGroupSession) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
|
||||
return InputNotJSONString
|
||||
}
|
||||
if s == nil || s.int == nil {
|
||||
*s = *NewBlankOutboundGroupSession()
|
||||
}
|
||||
return s.Unpickle(data[1:len(data)-1], pickleKey)
|
||||
}
|
||||
|
||||
// createRandomLen returns the number of random bytes needed to create an
|
||||
// Account.
|
||||
func (s *OutboundGroupSession) createRandomLen() uint {
|
||||
return uint(C.olm_init_outbound_group_session_random_length((*C.OlmOutboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// encryptMsgLen returns the size of the next message in bytes for the given
|
||||
// number of plain-text bytes.
|
||||
func (s *OutboundGroupSession) encryptMsgLen(plainTextLen int) uint {
|
||||
return uint(C.olm_group_encrypt_message_length((*C.OlmOutboundGroupSession)(s.int), C.size_t(plainTextLen)))
|
||||
}
|
||||
|
||||
// Encrypt encrypts a message using the Session. Returns the encrypted message
|
||||
// as base64.
|
||||
func (s *OutboundGroupSession) Encrypt(plaintext []byte) []byte {
|
||||
if len(plaintext) == 0 {
|
||||
panic(EmptyInput)
|
||||
}
|
||||
message := make([]byte, s.encryptMsgLen(len(plaintext)))
|
||||
r := C.olm_group_encrypt(
|
||||
(*C.OlmOutboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&plaintext[0]),
|
||||
C.size_t(len(plaintext)),
|
||||
(*C.uint8_t)(&message[0]),
|
||||
C.size_t(len(message)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return message[:r]
|
||||
}
|
||||
|
||||
// sessionIdLen returns the number of bytes needed to store a session ID.
|
||||
func (s *OutboundGroupSession) sessionIdLen() uint {
|
||||
return uint(C.olm_outbound_group_session_id_length((*C.OlmOutboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// ID returns a base64-encoded identifier for this session.
|
||||
func (s *OutboundGroupSession) ID() id.SessionID {
|
||||
sessionID := make([]byte, s.sessionIdLen())
|
||||
r := C.olm_outbound_group_session_id(
|
||||
(*C.OlmOutboundGroupSession)(s.int),
|
||||
(*C.uint8_t)(&sessionID[0]),
|
||||
C.size_t(len(sessionID)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return id.SessionID(sessionID[:r])
|
||||
}
|
||||
|
||||
// MessageIndex returns the message index for this session. Each message is
|
||||
// sent with an increasing index; this returns the index for the next message.
|
||||
func (s *OutboundGroupSession) MessageIndex() uint {
|
||||
return uint(C.olm_outbound_group_session_message_index((*C.OlmOutboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// sessionKeyLen returns the number of bytes needed to store a session key.
|
||||
func (s *OutboundGroupSession) sessionKeyLen() uint {
|
||||
return uint(C.olm_outbound_group_session_key_length((*C.OlmOutboundGroupSession)(s.int)))
|
||||
}
|
||||
|
||||
// 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),
|
||||
(*C.uint8_t)(&sessionKey[0]),
|
||||
C.size_t(len(sessionKey)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return string(sessionKey[:r])
|
||||
}
|
||||
111
vendor/maunium.net/go/mautrix/crypto/olm/pk.go
generated
vendored
Normal file
111
vendor/maunium.net/go/mautrix/crypto/olm/pk.go
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
// #include <olm/pk.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/canonicaljson"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// PkSigning stores a key pair for signing messages.
|
||||
type PkSigning struct {
|
||||
int *C.OlmPkSigning
|
||||
mem []byte
|
||||
PublicKey id.Ed25519
|
||||
Seed []byte
|
||||
}
|
||||
|
||||
func pkSigningSize() uint {
|
||||
return uint(C.olm_pk_signing_size())
|
||||
}
|
||||
|
||||
func pkSigningSeedLength() uint {
|
||||
return uint(C.olm_pk_signing_seed_length())
|
||||
}
|
||||
|
||||
func pkSigningPublicKeyLength() uint {
|
||||
return uint(C.olm_pk_signing_public_key_length())
|
||||
}
|
||||
|
||||
func pkSigningSignatureLength() uint {
|
||||
return uint(C.olm_pk_signature_length())
|
||||
}
|
||||
|
||||
func NewBlankPkSigning() *PkSigning {
|
||||
memory := make([]byte, pkSigningSize())
|
||||
return &PkSigning{
|
||||
int: C.olm_pk_signing(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// Clear clears the underlying memory of a PkSigning object.
|
||||
func (p *PkSigning) Clear() {
|
||||
C.olm_clear_pk_signing((*C.OlmPkSigning)(p.int))
|
||||
}
|
||||
|
||||
// NewPkSigningFromSeed creates a new PkSigning object using the given seed.
|
||||
func NewPkSigningFromSeed(seed []byte) (*PkSigning, error) {
|
||||
p := NewBlankPkSigning()
|
||||
p.Clear()
|
||||
pubKey := make([]byte, pkSigningPublicKeyLength())
|
||||
if C.olm_pk_signing_key_from_seed((*C.OlmPkSigning)(p.int),
|
||||
unsafe.Pointer(&pubKey[0]), C.size_t(len(pubKey)),
|
||||
unsafe.Pointer(&seed[0]), C.size_t(len(seed))) == errorVal() {
|
||||
return nil, p.lastError()
|
||||
}
|
||||
p.PublicKey = id.Ed25519(pubKey)
|
||||
p.Seed = seed
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// NewPkSigning creates a new PkSigning object, containing a key pair for signing messages.
|
||||
func NewPkSigning() (*PkSigning, error) {
|
||||
// Generate the seed
|
||||
seed := make([]byte, pkSigningSeedLength())
|
||||
_, err := rand.Read(seed)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
pk, err := NewPkSigningFromSeed(seed)
|
||||
return pk, err
|
||||
}
|
||||
|
||||
// Sign creates a signature for the given message using this key.
|
||||
func (p *PkSigning) Sign(message []byte) ([]byte, error) {
|
||||
signature := make([]byte, pkSigningSignatureLength())
|
||||
if C.olm_pk_sign((*C.OlmPkSigning)(p.int), (*C.uint8_t)(unsafe.Pointer(&message[0])), C.size_t(len(message)),
|
||||
(*C.uint8_t)(unsafe.Pointer(&signature[0])), C.size_t(len(signature))) == errorVal() {
|
||||
return nil, p.lastError()
|
||||
}
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
// SignJSON creates a signature for the given object after encoding it to canonical JSON.
|
||||
func (p *PkSigning) SignJSON(obj interface{}) (string, error) {
|
||||
objJSON, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
objJSON, _ = sjson.DeleteBytes(objJSON, "unsigned")
|
||||
objJSON, _ = sjson.DeleteBytes(objJSON, "signatures")
|
||||
signature, err := p.Sign(canonicaljson.CanonicalJSONAssumeValid(objJSON))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(signature), nil
|
||||
}
|
||||
|
||||
// lastError returns the last error that happened in relation to this PkSigning object.
|
||||
func (p *PkSigning) lastError() error {
|
||||
return convertError(C.GoString(C.olm_pk_signing_last_error((*C.OlmPkSigning)(p.int))))
|
||||
}
|
||||
355
vendor/maunium.net/go/mautrix/crypto/olm/session.go
generated
vendored
Normal file
355
vendor/maunium.net/go/mautrix/crypto/olm/session.go
generated
vendored
Normal file
@@ -0,0 +1,355 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
// #include <stdlib.h>
|
||||
// #include <stdio.h>
|
||||
// void olm_session_describe(OlmSession * session, char *buf, size_t buflen) __attribute__((weak));
|
||||
// void meowlm_session_describe(OlmSession * session, char *buf, size_t buflen) {
|
||||
// if (olm_session_describe) {
|
||||
// olm_session_describe(session, buf, buflen);
|
||||
// } else {
|
||||
// sprintf(buf, "olm_session_describe not supported");
|
||||
// }
|
||||
// }
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"unsafe"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// Session stores an end to end encrypted messaging session.
|
||||
type Session struct {
|
||||
int *C.OlmSession
|
||||
mem []byte
|
||||
}
|
||||
|
||||
// sessionSize is the size of a session object in bytes.
|
||||
func sessionSize() uint {
|
||||
return uint(C.olm_session_size())
|
||||
}
|
||||
|
||||
// SessionFromPickled loads a Session from a pickled base64 string. Decrypts
|
||||
// the Session using the supplied key. Returns error on failure. If the key
|
||||
// doesn't match the one used to encrypt the Session then the error will be
|
||||
// "BAD_SESSION_KEY". If the base64 couldn't be decoded then the error will be
|
||||
// "INVALID_BASE64".
|
||||
func SessionFromPickled(pickled, key []byte) (*Session, error) {
|
||||
if len(pickled) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
s := NewBlankSession()
|
||||
return s, s.Unpickle(pickled, key)
|
||||
}
|
||||
|
||||
func NewBlankSession() *Session {
|
||||
memory := make([]byte, sessionSize())
|
||||
return &Session{
|
||||
int: C.olm_session(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// lastError returns an error describing the most recent error to happen to a
|
||||
// session.
|
||||
func (s *Session) lastError() error {
|
||||
return convertError(C.GoString(C.olm_session_last_error((*C.OlmSession)(s.int))))
|
||||
}
|
||||
|
||||
// Clear clears the memory used to back this Session.
|
||||
func (s *Session) Clear() error {
|
||||
r := C.olm_clear_session((*C.OlmSession)(s.int))
|
||||
if r == errorVal() {
|
||||
return s.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pickleLen returns the number of bytes needed to store a session.
|
||||
func (s *Session) pickleLen() uint {
|
||||
return uint(C.olm_pickle_session_length((*C.OlmSession)(s.int)))
|
||||
}
|
||||
|
||||
// createOutboundRandomLen returns the number of random bytes needed to create
|
||||
// an outbound session.
|
||||
func (s *Session) createOutboundRandomLen() uint {
|
||||
return uint(C.olm_create_outbound_session_random_length((*C.OlmSession)(s.int)))
|
||||
}
|
||||
|
||||
// idLen returns the length of the buffer needed to return the id for this
|
||||
// session.
|
||||
func (s *Session) idLen() uint {
|
||||
return uint(C.olm_session_id_length((*C.OlmSession)(s.int)))
|
||||
}
|
||||
|
||||
// encryptRandomLen returns the number of random bytes needed to encrypt the
|
||||
// next message.
|
||||
func (s *Session) encryptRandomLen() uint {
|
||||
return uint(C.olm_encrypt_random_length((*C.OlmSession)(s.int)))
|
||||
}
|
||||
|
||||
// encryptMsgLen returns the size of the next message in bytes for the given
|
||||
// number of plain-text bytes.
|
||||
func (s *Session) encryptMsgLen(plainTextLen int) uint {
|
||||
return uint(C.olm_encrypt_message_length((*C.OlmSession)(s.int), C.size_t(plainTextLen)))
|
||||
}
|
||||
|
||||
// decryptMaxPlaintextLen returns the maximum number of bytes of plain-text a
|
||||
// given message could decode to. The actual size could be different due to
|
||||
// padding. Returns error on failure. If the message base64 couldn't be
|
||||
// decoded then the error will be "INVALID_BASE64". If the message is for an
|
||||
// unsupported version of the protocol then the error will be
|
||||
// "BAD_MESSAGE_VERSION". If the message couldn't be decoded then the error
|
||||
// will be "BAD_MESSAGE_FORMAT".
|
||||
func (s *Session) decryptMaxPlaintextLen(message string, msgType id.OlmMsgType) (uint, error) {
|
||||
if len(message) == 0 {
|
||||
return 0, EmptyInput
|
||||
}
|
||||
r := C.olm_decrypt_max_plaintext_length(
|
||||
(*C.OlmSession)(s.int),
|
||||
C.size_t(msgType),
|
||||
unsafe.Pointer(C.CString(message)),
|
||||
C.size_t(len(message)))
|
||||
if r == errorVal() {
|
||||
return 0, s.lastError()
|
||||
}
|
||||
return uint(r), nil
|
||||
}
|
||||
|
||||
// Pickle returns a Session as a base64 string. Encrypts the Session using the
|
||||
// supplied key.
|
||||
func (s *Session) Pickle(key []byte) []byte {
|
||||
if len(key) == 0 {
|
||||
panic(NoKeyProvided)
|
||||
}
|
||||
pickled := make([]byte, s.pickleLen())
|
||||
r := C.olm_pickle_session(
|
||||
(*C.OlmSession)(s.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return pickled[:r]
|
||||
}
|
||||
|
||||
func (s *Session) Unpickle(pickled, key []byte) error {
|
||||
if len(key) == 0 {
|
||||
return NoKeyProvided
|
||||
}
|
||||
r := C.olm_unpickle_session(
|
||||
(*C.OlmSession)(s.int),
|
||||
unsafe.Pointer(&key[0]),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&pickled[0]),
|
||||
C.size_t(len(pickled)))
|
||||
if r == errorVal() {
|
||||
return s.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Session) GobEncode() ([]byte, error) {
|
||||
pickled := s.Pickle(pickleKey)
|
||||
length := base64.RawStdEncoding.DecodedLen(len(pickled))
|
||||
rawPickled := make([]byte, length)
|
||||
_, err := base64.RawStdEncoding.Decode(rawPickled, pickled)
|
||||
return rawPickled, err
|
||||
}
|
||||
|
||||
func (s *Session) GobDecode(rawPickled []byte) error {
|
||||
if s == nil || s.int == nil {
|
||||
*s = *NewBlankSession()
|
||||
}
|
||||
length := base64.RawStdEncoding.EncodedLen(len(rawPickled))
|
||||
pickled := make([]byte, length)
|
||||
base64.RawStdEncoding.Encode(pickled, rawPickled)
|
||||
return s.Unpickle(pickled, pickleKey)
|
||||
}
|
||||
|
||||
func (s *Session) MarshalJSON() ([]byte, error) {
|
||||
pickled := s.Pickle(pickleKey)
|
||||
quotes := make([]byte, len(pickled)+2)
|
||||
quotes[0] = '"'
|
||||
quotes[len(quotes)-1] = '"'
|
||||
copy(quotes[1:len(quotes)-1], pickled)
|
||||
return quotes, nil
|
||||
}
|
||||
|
||||
func (s *Session) UnmarshalJSON(data []byte) error {
|
||||
if len(data) == 0 || len(data) == 0 || data[0] != '"' || data[len(data)-1] != '"' {
|
||||
return InputNotJSONString
|
||||
}
|
||||
if s == nil || s.int == nil {
|
||||
*s = *NewBlankSession()
|
||||
}
|
||||
return s.Unpickle(data[1:len(data)-1], pickleKey)
|
||||
}
|
||||
|
||||
// Id returns an identifier for this Session. Will be the same for both ends
|
||||
// of the conversation.
|
||||
func (s *Session) ID() id.SessionID {
|
||||
sessionID := make([]byte, s.idLen())
|
||||
r := C.olm_session_id(
|
||||
(*C.OlmSession)(s.int),
|
||||
unsafe.Pointer(&sessionID[0]),
|
||||
C.size_t(len(sessionID)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return id.SessionID(sessionID)
|
||||
}
|
||||
|
||||
// HasReceivedMessage returns true if this session has received any message.
|
||||
func (s *Session) HasReceivedMessage() bool {
|
||||
switch C.olm_session_has_received_message((*C.OlmSession)(s.int)) {
|
||||
case 0:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MatchesInboundSession checks if the PRE_KEY message is for this in-bound
|
||||
// Session. This can happen if multiple messages are sent to this Account
|
||||
// before this Account sends a message in reply. Returns true if the session
|
||||
// matches. Returns false if the session does not match. Returns error on
|
||||
// failure. If the base64 couldn't be decoded then the error will be
|
||||
// "INVALID_BASE64". If the message was for an unsupported protocol version
|
||||
// then the error will be "BAD_MESSAGE_VERSION". If the message couldn't be
|
||||
// decoded then then the error will be "BAD_MESSAGE_FORMAT".
|
||||
func (s *Session) MatchesInboundSession(oneTimeKeyMsg string) (bool, error) {
|
||||
if len(oneTimeKeyMsg) == 0 {
|
||||
return false, EmptyInput
|
||||
}
|
||||
r := C.olm_matches_inbound_session(
|
||||
(*C.OlmSession)(s.int),
|
||||
unsafe.Pointer(&([]byte(oneTimeKeyMsg))[0]),
|
||||
C.size_t(len(oneTimeKeyMsg)))
|
||||
if r == 1 {
|
||||
return true, nil
|
||||
} else if r == 0 {
|
||||
return false, nil
|
||||
} else { // if r == errorVal()
|
||||
return false, s.lastError()
|
||||
}
|
||||
}
|
||||
|
||||
// MatchesInboundSessionFrom checks if the PRE_KEY message is for this in-bound
|
||||
// Session. This can happen if multiple messages are sent to this Account
|
||||
// before this Account sends a message in reply. Returns true if the session
|
||||
// matches. Returns false if the session does not match. Returns error on
|
||||
// failure. If the base64 couldn't be decoded then the error will be
|
||||
// "INVALID_BASE64". If the message was for an unsupported protocol version
|
||||
// then the error will be "BAD_MESSAGE_VERSION". If the message couldn't be
|
||||
// decoded then then the error will be "BAD_MESSAGE_FORMAT".
|
||||
func (s *Session) MatchesInboundSessionFrom(theirIdentityKey, oneTimeKeyMsg string) (bool, error) {
|
||||
if len(theirIdentityKey) == 0 || len(oneTimeKeyMsg) == 0 {
|
||||
return false, EmptyInput
|
||||
}
|
||||
r := C.olm_matches_inbound_session_from(
|
||||
(*C.OlmSession)(s.int),
|
||||
unsafe.Pointer(&([]byte(theirIdentityKey))[0]),
|
||||
C.size_t(len(theirIdentityKey)),
|
||||
unsafe.Pointer(&([]byte(oneTimeKeyMsg))[0]),
|
||||
C.size_t(len(oneTimeKeyMsg)))
|
||||
if r == 1 {
|
||||
return true, nil
|
||||
} else if r == 0 {
|
||||
return false, nil
|
||||
} else { // if r == errorVal()
|
||||
return false, s.lastError()
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptMsgType returns the type of the next message that Encrypt will
|
||||
// return. Returns MsgTypePreKey if the message will be a PRE_KEY message.
|
||||
// Returns MsgTypeMsg if the message will be a normal message. Returns error
|
||||
// on failure.
|
||||
func (s *Session) EncryptMsgType() id.OlmMsgType {
|
||||
switch C.olm_encrypt_message_type((*C.OlmSession)(s.int)) {
|
||||
case C.size_t(id.OlmMsgTypePreKey):
|
||||
return id.OlmMsgTypePreKey
|
||||
case C.size_t(id.OlmMsgTypeMsg):
|
||||
return id.OlmMsgTypeMsg
|
||||
default:
|
||||
panic("olm_encrypt_message_type returned invalid result")
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts a message using the Session. Returns the encrypted message
|
||||
// as base64.
|
||||
func (s *Session) Encrypt(plaintext []byte) (id.OlmMsgType, []byte) {
|
||||
if len(plaintext) == 0 {
|
||||
panic(EmptyInput)
|
||||
}
|
||||
// Make the slice be at least length 1
|
||||
random := make([]byte, s.encryptRandomLen()+1)
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
messageType := s.EncryptMsgType()
|
||||
message := make([]byte, s.encryptMsgLen(len(plaintext)))
|
||||
r := C.olm_encrypt(
|
||||
(*C.OlmSession)(s.int),
|
||||
unsafe.Pointer(&plaintext[0]),
|
||||
C.size_t(len(plaintext)),
|
||||
unsafe.Pointer(&random[0]),
|
||||
C.size_t(len(random)),
|
||||
unsafe.Pointer(&message[0]),
|
||||
C.size_t(len(message)))
|
||||
if r == errorVal() {
|
||||
panic(s.lastError())
|
||||
}
|
||||
return messageType, message[:r]
|
||||
}
|
||||
|
||||
// Decrypt decrypts a message using the Session. Returns the the plain-text on
|
||||
// success. Returns error on failure. If the base64 couldn't be decoded then
|
||||
// the error will be "INVALID_BASE64". If the message is for an unsupported
|
||||
// version of the protocol then the error will be "BAD_MESSAGE_VERSION". If
|
||||
// the message couldn't be decoded then the error will be BAD_MESSAGE_FORMAT".
|
||||
// If the MAC on the message was invalid then the error will be
|
||||
// "BAD_MESSAGE_MAC".
|
||||
func (s *Session) Decrypt(message string, msgType id.OlmMsgType) ([]byte, error) {
|
||||
if len(message) == 0 {
|
||||
return nil, EmptyInput
|
||||
}
|
||||
decryptMaxPlaintextLen, err := s.decryptMaxPlaintextLen(message, msgType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plaintext := make([]byte, decryptMaxPlaintextLen)
|
||||
r := C.olm_decrypt(
|
||||
(*C.OlmSession)(s.int),
|
||||
C.size_t(msgType),
|
||||
unsafe.Pointer(&([]byte(message))[0]),
|
||||
C.size_t(len(message)),
|
||||
unsafe.Pointer(&plaintext[0]),
|
||||
C.size_t(len(plaintext)))
|
||||
if r == errorVal() {
|
||||
return nil, s.lastError()
|
||||
}
|
||||
return plaintext[:r], nil
|
||||
}
|
||||
|
||||
// https://gitlab.matrix.org/matrix-org/olm/-/blob/3.2.8/include/olm/olm.h#L392-393
|
||||
const maxDescribeSize = 600
|
||||
|
||||
// Describe generates a string describing the internal state of an olm session for debugging and logging purposes.
|
||||
func (s *Session) Describe() string {
|
||||
desc := (*C.char)(C.malloc(C.size_t(maxDescribeSize)))
|
||||
defer C.free(unsafe.Pointer(desc))
|
||||
C.meowlm_session_describe(
|
||||
(*C.OlmSession)(s.int),
|
||||
desc,
|
||||
C.size_t(maxDescribeSize))
|
||||
return C.GoString(desc)
|
||||
}
|
||||
140
vendor/maunium.net/go/mautrix/crypto/olm/utility.go
generated
vendored
Normal file
140
vendor/maunium.net/go/mautrix/crypto/olm/utility.go
generated
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/canonicaljson"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/util"
|
||||
)
|
||||
|
||||
// Utility stores the necessary state to perform hash and signature
|
||||
// verification operations.
|
||||
type Utility struct {
|
||||
int *C.OlmUtility
|
||||
mem []byte
|
||||
}
|
||||
|
||||
// utilitySize returns the size of a utility object in bytes.
|
||||
func utilitySize() uint {
|
||||
return uint(C.olm_utility_size())
|
||||
}
|
||||
|
||||
// sha256Len returns the length of the buffer needed to hold the SHA-256 hash.
|
||||
func (u *Utility) sha256Len() uint {
|
||||
return uint(C.olm_sha256_length((*C.OlmUtility)(u.int)))
|
||||
}
|
||||
|
||||
// lastError returns an error describing the most recent error to happen to a
|
||||
// utility.
|
||||
func (u *Utility) lastError() error {
|
||||
return convertError(C.GoString(C.olm_utility_last_error((*C.OlmUtility)(u.int))))
|
||||
}
|
||||
|
||||
// Clear clears the memory used to back this utility.
|
||||
func (u *Utility) Clear() error {
|
||||
r := C.olm_clear_utility((*C.OlmUtility)(u.int))
|
||||
if r == errorVal() {
|
||||
return u.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewUtility creates a new utility.
|
||||
func NewUtility() *Utility {
|
||||
memory := make([]byte, utilitySize())
|
||||
return &Utility{
|
||||
int: C.olm_utility(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// Sha256 calculates the SHA-256 hash of the input and encodes it as base64.
|
||||
func (u *Utility) Sha256(input string) string {
|
||||
if len(input) == 0 {
|
||||
panic(EmptyInput)
|
||||
}
|
||||
output := make([]byte, u.sha256Len())
|
||||
r := C.olm_sha256(
|
||||
(*C.OlmUtility)(u.int),
|
||||
unsafe.Pointer(&([]byte(input)[0])),
|
||||
C.size_t(len(input)),
|
||||
unsafe.Pointer(&(output[0])),
|
||||
C.size_t(len(output)))
|
||||
if r == errorVal() {
|
||||
panic(u.lastError())
|
||||
}
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// VerifySignature verifies an ed25519 signature. Returns true if the verification
|
||||
// suceeds or false otherwise. Returns error on failure. If the key was too
|
||||
// small then the error will be "INVALID_BASE64".
|
||||
func (u *Utility) VerifySignature(message string, key id.Ed25519, signature string) (ok bool, err error) {
|
||||
if len(message) == 0 || len(key) == 0 || len(signature) == 0 {
|
||||
return false, EmptyInput
|
||||
}
|
||||
r := C.olm_ed25519_verify(
|
||||
(*C.OlmUtility)(u.int),
|
||||
unsafe.Pointer(&([]byte(key)[0])),
|
||||
C.size_t(len(key)),
|
||||
unsafe.Pointer(&([]byte(message)[0])),
|
||||
C.size_t(len(message)),
|
||||
unsafe.Pointer(&([]byte(signature)[0])),
|
||||
C.size_t(len(signature)))
|
||||
if r == errorVal() {
|
||||
err = u.lastError()
|
||||
if err == BadMessageMAC {
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
ok = true
|
||||
}
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// VerifySignatureJSON verifies the signature in the JSON object _obj following
|
||||
// the Matrix specification:
|
||||
// https://matrix.org/speculator/spec/drafts%2Fe2e/appendices.html#signing-json
|
||||
// If the _obj is a struct, the `json` tags will be honored.
|
||||
func (u *Utility) VerifySignatureJSON(obj interface{}, userID id.UserID, keyName string, key id.Ed25519) (bool, error) {
|
||||
objJSON, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
sig := gjson.GetBytes(objJSON, util.GJSONPath("signatures", string(userID), fmt.Sprintf("ed25519:%s", keyName)))
|
||||
if !sig.Exists() || sig.Type != gjson.String {
|
||||
return false, SignatureNotFound
|
||||
}
|
||||
objJSON, err = sjson.DeleteBytes(objJSON, "unsigned")
|
||||
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)
|
||||
}
|
||||
|
||||
// VerifySignatureJSON verifies the signature in the JSON object _obj following
|
||||
// the Matrix specification:
|
||||
// https://matrix.org/speculator/spec/drafts%2Fe2e/appendices.html#signing-json
|
||||
// This function is a wrapper over Utility.VerifySignatureJSON that creates and
|
||||
// destroys the Utility object transparently.
|
||||
// If the _obj is a struct, the `json` tags will be honored.
|
||||
func VerifySignatureJSON(obj interface{}, userID id.UserID, keyName string, key id.Ed25519) (bool, error) {
|
||||
u := NewUtility()
|
||||
defer u.Clear()
|
||||
return u.VerifySignatureJSON(obj, userID, keyName, key)
|
||||
}
|
||||
143
vendor/maunium.net/go/mautrix/crypto/olm/verification.go
generated
vendored
Normal file
143
vendor/maunium.net/go/mautrix/crypto/olm/verification.go
generated
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
//go:build !nosas
|
||||
// +build !nosas
|
||||
|
||||
package olm
|
||||
|
||||
// #cgo LDFLAGS: -lolm -lstdc++
|
||||
// #include <olm/olm.h>
|
||||
// #include <olm/sas.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// SAS stores an Olm Short Authentication String (SAS) object.
|
||||
type SAS struct {
|
||||
int *C.OlmSAS
|
||||
mem []byte
|
||||
}
|
||||
|
||||
// NewBlankSAS initializes an empty SAS object.
|
||||
func NewBlankSAS() *SAS {
|
||||
memory := make([]byte, sasSize())
|
||||
return &SAS{
|
||||
int: C.olm_sas(unsafe.Pointer(&memory[0])),
|
||||
mem: memory,
|
||||
}
|
||||
}
|
||||
|
||||
// sasSize is the size of a SAS object in bytes.
|
||||
func sasSize() uint {
|
||||
return uint(C.olm_sas_size())
|
||||
}
|
||||
|
||||
// sasRandomLength is the number of random bytes needed to create an SAS object.
|
||||
func (sas *SAS) sasRandomLength() uint {
|
||||
return uint(C.olm_create_sas_random_length(sas.int))
|
||||
}
|
||||
|
||||
// NewSAS creates a new SAS object.
|
||||
func NewSAS() *SAS {
|
||||
sas := NewBlankSAS()
|
||||
random := make([]byte, sas.sasRandomLength()+1)
|
||||
_, err := rand.Read(random)
|
||||
if err != nil {
|
||||
panic(NotEnoughGoRandom)
|
||||
}
|
||||
r := C.olm_create_sas(
|
||||
(*C.OlmSAS)(sas.int),
|
||||
unsafe.Pointer(&random[0]),
|
||||
C.size_t(len(random)))
|
||||
if r == errorVal() {
|
||||
panic(sas.lastError())
|
||||
} else {
|
||||
return sas
|
||||
}
|
||||
}
|
||||
|
||||
// clear clears the memory used to back an SAS object.
|
||||
func (sas *SAS) clear() uint {
|
||||
return uint(C.olm_clear_sas(sas.int))
|
||||
}
|
||||
|
||||
// lastError returns the most recent error to happen to an SAS object.
|
||||
func (sas *SAS) lastError() error {
|
||||
return convertError(C.GoString(C.olm_sas_last_error(sas.int)))
|
||||
}
|
||||
|
||||
// pubkeyLength is the size of a public key in bytes.
|
||||
func (sas *SAS) pubkeyLength() uint {
|
||||
return uint(C.olm_sas_pubkey_length((*C.OlmSAS)(sas.int)))
|
||||
}
|
||||
|
||||
// GetPubkey gets the public key for the SAS object.
|
||||
func (sas *SAS) GetPubkey() []byte {
|
||||
pubkey := make([]byte, sas.pubkeyLength())
|
||||
r := C.olm_sas_get_pubkey(
|
||||
(*C.OlmSAS)(sas.int),
|
||||
unsafe.Pointer(&pubkey[0]),
|
||||
C.size_t(len(pubkey)))
|
||||
if r == errorVal() {
|
||||
panic(sas.lastError())
|
||||
}
|
||||
return pubkey
|
||||
}
|
||||
|
||||
// SetTheirKey sets the public key of the other user.
|
||||
func (sas *SAS) SetTheirKey(theirKey []byte) error {
|
||||
theirKeyCopy := make([]byte, len(theirKey))
|
||||
copy(theirKeyCopy, theirKey)
|
||||
r := C.olm_sas_set_their_key(
|
||||
(*C.OlmSAS)(sas.int),
|
||||
unsafe.Pointer(&theirKeyCopy[0]),
|
||||
C.size_t(len(theirKeyCopy)))
|
||||
if r == errorVal() {
|
||||
return sas.lastError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateBytes generates bytes to use for the short authentication string.
|
||||
func (sas *SAS) GenerateBytes(info []byte, count uint) ([]byte, error) {
|
||||
infoCopy := make([]byte, len(info))
|
||||
copy(infoCopy, info)
|
||||
output := make([]byte, count)
|
||||
r := C.olm_sas_generate_bytes(
|
||||
(*C.OlmSAS)(sas.int),
|
||||
unsafe.Pointer(&infoCopy[0]),
|
||||
C.size_t(len(infoCopy)),
|
||||
unsafe.Pointer(&output[0]),
|
||||
C.size_t(len(output)))
|
||||
if r == errorVal() {
|
||||
return nil, sas.lastError()
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// macLength is the size of a message authentication code generated by olm_sas_calculate_mac.
|
||||
func (sas *SAS) macLength() uint {
|
||||
return uint(C.olm_sas_mac_length((*C.OlmSAS)(sas.int)))
|
||||
}
|
||||
|
||||
// CalculateMAC generates a message authentication code (MAC) based on the shared secret.
|
||||
func (sas *SAS) CalculateMAC(input []byte, info []byte) ([]byte, error) {
|
||||
inputCopy := make([]byte, len(input))
|
||||
copy(inputCopy, input)
|
||||
infoCopy := make([]byte, len(info))
|
||||
copy(infoCopy, info)
|
||||
mac := make([]byte, sas.macLength())
|
||||
r := C.olm_sas_calculate_mac(
|
||||
(*C.OlmSAS)(sas.int),
|
||||
unsafe.Pointer(&inputCopy[0]),
|
||||
C.size_t(len(inputCopy)),
|
||||
unsafe.Pointer(&infoCopy[0]),
|
||||
C.size_t(len(infoCopy)),
|
||||
unsafe.Pointer(&mac[0]),
|
||||
C.size_t(len(mac)))
|
||||
if r == errorVal() {
|
||||
return nil, sas.lastError()
|
||||
}
|
||||
return mac, nil
|
||||
}
|
||||
229
vendor/maunium.net/go/mautrix/crypto/sessions.go
generated
vendored
Normal file
229
vendor/maunium.net/go/mautrix/crypto/sessions.go
generated
vendored
Normal file
@@ -0,0 +1,229 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
SessionNotShared = errors.New("session has not been shared")
|
||||
SessionExpired = errors.New("session has expired")
|
||||
)
|
||||
|
||||
// OlmSessionList is a list of OlmSessions.
|
||||
// It implements sort.Interface so that the session with recent successful decryptions comes first.
|
||||
type OlmSessionList []*OlmSession
|
||||
|
||||
func (o OlmSessionList) Len() int {
|
||||
return len(o)
|
||||
}
|
||||
|
||||
func (o OlmSessionList) Less(i, j int) bool {
|
||||
return o[i].LastDecryptedTime.After(o[j].LastEncryptedTime)
|
||||
}
|
||||
|
||||
func (o OlmSessionList) Swap(i, j int) {
|
||||
o[i], o[j] = o[j], o[i]
|
||||
}
|
||||
|
||||
type OlmSession struct {
|
||||
Internal olm.Session
|
||||
ExpirationMixin
|
||||
id id.SessionID
|
||||
}
|
||||
|
||||
func (session *OlmSession) ID() id.SessionID {
|
||||
if session.id == "" {
|
||||
session.id = session.Internal.ID()
|
||||
}
|
||||
return session.id
|
||||
}
|
||||
|
||||
func (session *OlmSession) Describe() string {
|
||||
return session.Internal.Describe()
|
||||
}
|
||||
|
||||
func wrapSession(session *olm.Session) *OlmSession {
|
||||
return &OlmSession{
|
||||
Internal: *session,
|
||||
ExpirationMixin: ExpirationMixin{
|
||||
TimeMixin: TimeMixin{
|
||||
CreationTime: time.Now(),
|
||||
LastEncryptedTime: time.Now(),
|
||||
LastDecryptedTime: time.Now(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (account *OlmAccount) NewInboundSessionFrom(senderKey id.Curve25519, ciphertext string) (*OlmSession, error) {
|
||||
session, err := account.Internal.NewInboundSessionFrom(senderKey, ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = account.Internal.RemoveOneTimeKeys(session)
|
||||
return wrapSession(session), nil
|
||||
}
|
||||
|
||||
func (session *OlmSession) Encrypt(plaintext []byte) (id.OlmMsgType, []byte) {
|
||||
session.LastEncryptedTime = time.Now()
|
||||
return session.Internal.Encrypt(plaintext)
|
||||
}
|
||||
|
||||
func (session *OlmSession) Decrypt(ciphertext string, msgType id.OlmMsgType) ([]byte, error) {
|
||||
msg, err := session.Internal.Decrypt(ciphertext, msgType)
|
||||
if err == nil {
|
||||
session.LastDecryptedTime = time.Now()
|
||||
}
|
||||
return msg, err
|
||||
}
|
||||
|
||||
type InboundGroupSession struct {
|
||||
Internal olm.InboundGroupSession
|
||||
|
||||
SigningKey id.Ed25519
|
||||
SenderKey id.Curve25519
|
||||
RoomID id.RoomID
|
||||
|
||||
ForwardingChains []string
|
||||
|
||||
id id.SessionID
|
||||
}
|
||||
|
||||
func NewInboundGroupSession(senderKey id.SenderKey, signingKey id.Ed25519, roomID id.RoomID, sessionKey string) (*InboundGroupSession, error) {
|
||||
igs, err := olm.NewInboundGroupSession([]byte(sessionKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InboundGroupSession{
|
||||
Internal: *igs,
|
||||
SigningKey: signingKey,
|
||||
SenderKey: senderKey,
|
||||
RoomID: roomID,
|
||||
ForwardingChains: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (igs *InboundGroupSession) ID() id.SessionID {
|
||||
if igs.id == "" {
|
||||
igs.id = igs.Internal.ID()
|
||||
}
|
||||
return igs.id
|
||||
}
|
||||
|
||||
type OGSState int
|
||||
|
||||
const (
|
||||
OGSNotShared OGSState = iota
|
||||
OGSAlreadyShared
|
||||
OGSIgnored
|
||||
)
|
||||
|
||||
type UserDevice struct {
|
||||
UserID id.UserID
|
||||
DeviceID id.DeviceID
|
||||
}
|
||||
|
||||
type OutboundGroupSession struct {
|
||||
Internal olm.OutboundGroupSession
|
||||
|
||||
ExpirationMixin
|
||||
MaxMessages int
|
||||
MessageCount int
|
||||
|
||||
Users map[UserDevice]OGSState
|
||||
RoomID id.RoomID
|
||||
Shared bool
|
||||
|
||||
id id.SessionID
|
||||
content *event.RoomKeyEventContent
|
||||
}
|
||||
|
||||
func NewOutboundGroupSession(roomID id.RoomID, encryptionContent *event.EncryptionEventContent) *OutboundGroupSession {
|
||||
ogs := &OutboundGroupSession{
|
||||
Internal: *olm.NewOutboundGroupSession(),
|
||||
ExpirationMixin: ExpirationMixin{
|
||||
TimeMixin: TimeMixin{
|
||||
CreationTime: time.Now(),
|
||||
LastEncryptedTime: time.Now(),
|
||||
},
|
||||
MaxAge: 7 * 24 * time.Hour,
|
||||
},
|
||||
MaxMessages: 100,
|
||||
Shared: false,
|
||||
Users: make(map[UserDevice]OGSState),
|
||||
RoomID: roomID,
|
||||
}
|
||||
if encryptionContent != nil {
|
||||
if encryptionContent.RotationPeriodMillis != 0 {
|
||||
ogs.MaxAge = time.Duration(encryptionContent.RotationPeriodMillis) * time.Millisecond
|
||||
}
|
||||
if encryptionContent.RotationPeriodMessages != 0 {
|
||||
ogs.MaxMessages = encryptionContent.RotationPeriodMessages
|
||||
}
|
||||
}
|
||||
return ogs
|
||||
}
|
||||
|
||||
func (ogs *OutboundGroupSession) ShareContent() event.Content {
|
||||
if ogs.content == nil {
|
||||
ogs.content = &event.RoomKeyEventContent{
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
RoomID: ogs.RoomID,
|
||||
SessionID: ogs.ID(),
|
||||
SessionKey: ogs.Internal.Key(),
|
||||
}
|
||||
}
|
||||
return event.Content{Parsed: ogs.content}
|
||||
}
|
||||
|
||||
func (ogs *OutboundGroupSession) ID() id.SessionID {
|
||||
if ogs.id == "" {
|
||||
ogs.id = ogs.Internal.ID()
|
||||
}
|
||||
return ogs.id
|
||||
}
|
||||
|
||||
func (ogs *OutboundGroupSession) Expired() bool {
|
||||
return ogs.MessageCount >= ogs.MaxMessages || ogs.ExpirationMixin.Expired()
|
||||
}
|
||||
|
||||
func (ogs *OutboundGroupSession) Encrypt(plaintext []byte) ([]byte, error) {
|
||||
if !ogs.Shared {
|
||||
return nil, SessionNotShared
|
||||
} else if ogs.Expired() {
|
||||
return nil, SessionExpired
|
||||
}
|
||||
ogs.MessageCount++
|
||||
ogs.LastEncryptedTime = time.Now()
|
||||
return ogs.Internal.Encrypt(plaintext), nil
|
||||
}
|
||||
|
||||
type TimeMixin struct {
|
||||
CreationTime time.Time
|
||||
LastEncryptedTime time.Time
|
||||
LastDecryptedTime time.Time
|
||||
}
|
||||
|
||||
type ExpirationMixin struct {
|
||||
TimeMixin
|
||||
MaxAge time.Duration
|
||||
}
|
||||
|
||||
func (exp *ExpirationMixin) Expired() bool {
|
||||
if exp.MaxAge == 0 {
|
||||
return false
|
||||
}
|
||||
return exp.CreationTime.Add(exp.MaxAge).Before(time.Now())
|
||||
}
|
||||
687
vendor/maunium.net/go/mautrix/crypto/sql_store.go
generated
vendored
Normal file
687
vendor/maunium.net/go/mautrix/crypto/sql_store.go
generated
vendored
Normal file
@@ -0,0 +1,687 @@
|
||||
// Copyright (c) 2022 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 (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/crypto/sql_store_upgrade"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
"maunium.net/go/mautrix/util/dbutil"
|
||||
)
|
||||
|
||||
var PostgresArrayWrapper func(interface{}) interface {
|
||||
driver.Valuer
|
||||
sql.Scanner
|
||||
}
|
||||
|
||||
// SQLCryptoStore is an implementation of a crypto Store for a database backend.
|
||||
type SQLCryptoStore struct {
|
||||
DB *dbutil.Database
|
||||
|
||||
AccountID string
|
||||
DeviceID id.DeviceID
|
||||
SyncToken string
|
||||
PickleKey []byte
|
||||
Account *OlmAccount
|
||||
|
||||
olmSessionCache map[id.SenderKey]map[id.SessionID]*OlmSession
|
||||
olmSessionCacheLock sync.Mutex
|
||||
}
|
||||
|
||||
var _ Store = (*SQLCryptoStore)(nil)
|
||||
|
||||
// NewSQLCryptoStore initializes a new crypto Store using the given database, for a device's crypto material.
|
||||
// The stored material will be encrypted with the given key.
|
||||
func NewSQLCryptoStore(db *dbutil.Database, log dbutil.DatabaseLogger, accountID string, deviceID id.DeviceID, pickleKey []byte) *SQLCryptoStore {
|
||||
return &SQLCryptoStore{
|
||||
DB: db.Child(sql_store_upgrade.VersionTableName, sql_store_upgrade.Table, log),
|
||||
PickleKey: pickleKey,
|
||||
AccountID: accountID,
|
||||
DeviceID: deviceID,
|
||||
|
||||
olmSessionCache: make(map[id.SenderKey]map[id.SessionID]*OlmSession),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) Upgrade() error {
|
||||
return store.DB.Upgrade()
|
||||
}
|
||||
|
||||
// Flush does nothing for this implementation as data is already persisted in the database.
|
||||
func (store *SQLCryptoStore) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PutNextBatch stores the next sync batch token for the current account.
|
||||
func (store *SQLCryptoStore) PutNextBatch(nextBatch string) error {
|
||||
store.SyncToken = nextBatch
|
||||
_, err := store.DB.Exec(`UPDATE crypto_account SET sync_token=$1 WHERE account_id=$2`, store.SyncToken, store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetNextBatch retrieves the next sync batch token for the current account.
|
||||
func (store *SQLCryptoStore) GetNextBatch() (string, error) {
|
||||
if store.SyncToken == "" {
|
||||
err := store.DB.
|
||||
QueryRow("SELECT sync_token FROM crypto_account WHERE account_id=$1", store.AccountID).
|
||||
Scan(&store.SyncToken)
|
||||
if !errors.Is(err, sql.ErrNoRows) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return store.SyncToken, nil
|
||||
}
|
||||
|
||||
// PutAccount stores an OlmAccount in the database.
|
||||
func (store *SQLCryptoStore) PutAccount(account *OlmAccount) error {
|
||||
store.Account = account
|
||||
bytes := account.Internal.Pickle(store.PickleKey)
|
||||
_, err := store.DB.Exec(`
|
||||
INSERT INTO crypto_account (device_id, shared, sync_token, account, account_id) VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (account_id) DO UPDATE SET shared=excluded.shared, sync_token=excluded.sync_token,
|
||||
account=excluded.account, account_id=excluded.account_id
|
||||
`, store.DeviceID, account.Shared, store.SyncToken, bytes, store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetAccount retrieves an OlmAccount from the database.
|
||||
func (store *SQLCryptoStore) GetAccount() (*OlmAccount, error) {
|
||||
if store.Account == nil {
|
||||
row := store.DB.QueryRow("SELECT shared, sync_token, account FROM crypto_account WHERE account_id=$1", store.AccountID)
|
||||
acc := &OlmAccount{Internal: *olm.NewBlankAccount()}
|
||||
var accountBytes []byte
|
||||
err := row.Scan(&acc.Shared, &store.SyncToken, &accountBytes)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = acc.Internal.Unpickle(accountBytes, store.PickleKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store.Account = acc
|
||||
}
|
||||
return store.Account, nil
|
||||
}
|
||||
|
||||
// HasSession returns whether there is an Olm session for the given sender key.
|
||||
func (store *SQLCryptoStore) HasSession(key id.SenderKey) bool {
|
||||
store.olmSessionCacheLock.Lock()
|
||||
cache, ok := store.olmSessionCache[key]
|
||||
store.olmSessionCacheLock.Unlock()
|
||||
if ok && len(cache) > 0 {
|
||||
return true
|
||||
}
|
||||
var sessionID id.SessionID
|
||||
err := store.DB.QueryRow("SELECT session_id FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 LIMIT 1",
|
||||
key, store.AccountID).Scan(&sessionID)
|
||||
if err == sql.ErrNoRows {
|
||||
return false
|
||||
}
|
||||
return len(sessionID) > 0
|
||||
}
|
||||
|
||||
// GetSessions returns all the known Olm sessions for a sender key.
|
||||
func (store *SQLCryptoStore) GetSessions(key id.SenderKey) (OlmSessionList, error) {
|
||||
rows, err := store.DB.Query("SELECT session_id, session, created_at, last_encrypted, last_decrypted FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY last_decrypted DESC",
|
||||
key, store.AccountID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := OlmSessionList{}
|
||||
store.olmSessionCacheLock.Lock()
|
||||
defer store.olmSessionCacheLock.Unlock()
|
||||
cache := store.getOlmSessionCache(key)
|
||||
for rows.Next() {
|
||||
sess := OlmSession{Internal: *olm.NewBlankSession()}
|
||||
var sessionBytes []byte
|
||||
var sessionID id.SessionID
|
||||
err = rows.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.LastEncryptedTime, &sess.LastDecryptedTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if existing, ok := cache[sessionID]; ok {
|
||||
list = append(list, existing)
|
||||
} else {
|
||||
err = sess.Internal.Unpickle(sessionBytes, store.PickleKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list = append(list, &sess)
|
||||
cache[sess.ID()] = &sess
|
||||
}
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) getOlmSessionCache(key id.SenderKey) map[id.SessionID]*OlmSession {
|
||||
data, ok := store.olmSessionCache[key]
|
||||
if !ok {
|
||||
data = make(map[id.SessionID]*OlmSession)
|
||||
store.olmSessionCache[key] = data
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// GetLatestSession retrieves the Olm session for a given sender key from the database that has the largest ID.
|
||||
func (store *SQLCryptoStore) GetLatestSession(key id.SenderKey) (*OlmSession, error) {
|
||||
store.olmSessionCacheLock.Lock()
|
||||
defer store.olmSessionCacheLock.Unlock()
|
||||
|
||||
row := store.DB.QueryRow("SELECT session_id, session, created_at, last_encrypted, last_decrypted FROM crypto_olm_session WHERE sender_key=$1 AND account_id=$2 ORDER BY last_decrypted DESC LIMIT 1",
|
||||
key, store.AccountID)
|
||||
|
||||
sess := OlmSession{Internal: *olm.NewBlankSession()}
|
||||
var sessionBytes []byte
|
||||
var sessionID id.SessionID
|
||||
|
||||
err := row.Scan(&sessionID, &sessionBytes, &sess.CreationTime, &sess.LastEncryptedTime, &sess.LastDecryptedTime)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cache := store.getOlmSessionCache(key)
|
||||
if oldSess, ok := cache[sessionID]; ok {
|
||||
return oldSess, nil
|
||||
} else if err = sess.Internal.Unpickle(sessionBytes, store.PickleKey); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
cache[sessionID] = &sess
|
||||
return &sess, nil
|
||||
}
|
||||
}
|
||||
|
||||
// AddSession persists an Olm session for a sender in the database.
|
||||
func (store *SQLCryptoStore) AddSession(key id.SenderKey, session *OlmSession) error {
|
||||
store.olmSessionCacheLock.Lock()
|
||||
defer store.olmSessionCacheLock.Unlock()
|
||||
sessionBytes := session.Internal.Pickle(store.PickleKey)
|
||||
_, err := store.DB.Exec("INSERT INTO crypto_olm_session (session_id, sender_key, session, created_at, last_encrypted, last_decrypted, account_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
||||
session.ID(), key, sessionBytes, session.CreationTime, session.LastEncryptedTime, session.LastDecryptedTime, store.AccountID)
|
||||
store.getOlmSessionCache(key)[session.ID()] = session
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateSession replaces the Olm session for a sender in the database.
|
||||
func (store *SQLCryptoStore) UpdateSession(_ id.SenderKey, session *OlmSession) error {
|
||||
sessionBytes := session.Internal.Pickle(store.PickleKey)
|
||||
_, err := store.DB.Exec("UPDATE crypto_olm_session SET session=$1, last_encrypted=$2, last_decrypted=$3 WHERE session_id=$4 AND account_id=$5",
|
||||
sessionBytes, session.LastEncryptedTime, session.LastDecryptedTime, session.ID(), store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// PutGroupSession stores an inbound Megolm group session for a room, sender and session.
|
||||
func (store *SQLCryptoStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, session *InboundGroupSession) error {
|
||||
sessionBytes := session.Internal.Pickle(store.PickleKey)
|
||||
forwardingChains := strings.Join(session.ForwardingChains, ",")
|
||||
_, err := store.DB.Exec(`
|
||||
INSERT INTO crypto_megolm_inbound_session
|
||||
(session_id, sender_key, signing_key, room_id, session, forwarding_chains, account_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (session_id, account_id) DO UPDATE
|
||||
SET withheld_code=NULL, withheld_reason=NULL, sender_key=excluded.sender_key, signing_key=excluded.signing_key,
|
||||
room_id=excluded.room_id, session=excluded.session, forwarding_chains=excluded.forwarding_chains
|
||||
`, sessionID, senderKey, session.SigningKey, roomID, sessionBytes, forwardingChains, store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetGroupSession retrieves an inbound Megolm group session for a room, sender and session.
|
||||
func (store *SQLCryptoStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*InboundGroupSession, error) {
|
||||
var signingKey, forwardingChains, withheldCode sql.NullString
|
||||
var sessionBytes []byte
|
||||
err := store.DB.QueryRow(`
|
||||
SELECT signing_key, session, forwarding_chains, withheld_code
|
||||
FROM crypto_megolm_inbound_session
|
||||
WHERE room_id=$1 AND sender_key=$2 AND session_id=$3 AND account_id=$4`,
|
||||
roomID, senderKey, sessionID, store.AccountID,
|
||||
).Scan(&signingKey, &sessionBytes, &forwardingChains, &withheldCode)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else if withheldCode.Valid {
|
||||
return nil, fmt.Errorf("%w (%s)", ErrGroupSessionWithheld, withheldCode.String)
|
||||
}
|
||||
igs := olm.NewBlankInboundGroupSession()
|
||||
err = igs.Unpickle(sessionBytes, store.PickleKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var chains []string
|
||||
if forwardingChains.String != "" {
|
||||
chains = strings.Split(forwardingChains.String, ",")
|
||||
}
|
||||
return &InboundGroupSession{
|
||||
Internal: *igs,
|
||||
SigningKey: id.Ed25519(signingKey.String),
|
||||
SenderKey: senderKey,
|
||||
RoomID: roomID,
|
||||
ForwardingChains: chains,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) PutWithheldGroupSession(content event.RoomKeyWithheldEventContent) error {
|
||||
_, err := store.DB.Exec("INSERT INTO crypto_megolm_inbound_session (session_id, sender_key, room_id, withheld_code, withheld_reason, account_id) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
content.SessionID, content.SenderKey, content.RoomID, content.Code, content.Reason, store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) GetWithheldGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*event.RoomKeyWithheldEventContent, error) {
|
||||
var code, reason sql.NullString
|
||||
err := store.DB.QueryRow(`
|
||||
SELECT withheld_code, withheld_reason FROM crypto_megolm_inbound_session
|
||||
WHERE room_id=$1 AND sender_key=$2 AND session_id=$3 AND account_id=$4`,
|
||||
roomID, senderKey, sessionID, store.AccountID,
|
||||
).Scan(&code, &reason)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil || !code.Valid {
|
||||
return nil, err
|
||||
}
|
||||
return &event.RoomKeyWithheldEventContent{
|
||||
RoomID: roomID,
|
||||
Algorithm: id.AlgorithmMegolmV1,
|
||||
SessionID: sessionID,
|
||||
SenderKey: senderKey,
|
||||
Code: event.RoomKeyWithheldCode(code.String),
|
||||
Reason: reason.String,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) scanGroupSessionList(rows *sql.Rows) (result []*InboundGroupSession, err error) {
|
||||
for rows.Next() {
|
||||
var roomID id.RoomID
|
||||
var signingKey, senderKey, forwardingChains sql.NullString
|
||||
var sessionBytes []byte
|
||||
err = rows.Scan(&roomID, &signingKey, &senderKey, &sessionBytes, &forwardingChains)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
igs := olm.NewBlankInboundGroupSession()
|
||||
err = igs.Unpickle(sessionBytes, store.PickleKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var chains []string
|
||||
if forwardingChains.String != "" {
|
||||
chains = strings.Split(forwardingChains.String, ",")
|
||||
}
|
||||
result = append(result, &InboundGroupSession{
|
||||
Internal: *igs,
|
||||
SigningKey: id.Ed25519(signingKey.String),
|
||||
SenderKey: id.Curve25519(senderKey.String),
|
||||
RoomID: roomID,
|
||||
ForwardingChains: chains,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) GetGroupSessionsForRoom(roomID id.RoomID) ([]*InboundGroupSession, error) {
|
||||
rows, err := store.DB.Query(`
|
||||
SELECT room_id, signing_key, sender_key, session, forwarding_chains
|
||||
FROM crypto_megolm_inbound_session WHERE room_id=$1 AND account_id=$2 AND session IS NOT NULL`,
|
||||
roomID, store.AccountID,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return []*InboundGroupSession{}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store.scanGroupSessionList(rows)
|
||||
}
|
||||
|
||||
func (store *SQLCryptoStore) GetAllGroupSessions() ([]*InboundGroupSession, error) {
|
||||
rows, err := store.DB.Query(`
|
||||
SELECT room_id, signing_key, sender_key, session, forwarding_chains
|
||||
FROM crypto_megolm_inbound_session WHERE account_id=$2 AND session IS NOT NULL`,
|
||||
store.AccountID,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return []*InboundGroupSession{}, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return store.scanGroupSessionList(rows)
|
||||
}
|
||||
|
||||
// AddOutboundGroupSession stores an outbound Megolm session, along with the information about the room and involved devices.
|
||||
func (store *SQLCryptoStore) AddOutboundGroupSession(session *OutboundGroupSession) error {
|
||||
sessionBytes := session.Internal.Pickle(store.PickleKey)
|
||||
_, err := store.DB.Exec(`
|
||||
INSERT INTO crypto_megolm_outbound_session
|
||||
(room_id, session_id, session, shared, max_messages, message_count, max_age, created_at, last_used, account_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
ON CONFLICT (account_id, room_id) DO UPDATE
|
||||
SET session_id=excluded.session_id, session=excluded.session, shared=excluded.shared,
|
||||
max_messages=excluded.max_messages, message_count=excluded.message_count, max_age=excluded.max_age,
|
||||
created_at=excluded.created_at, last_used=excluded.last_used, account_id=excluded.account_id
|
||||
`, session.RoomID, session.ID(), sessionBytes, session.Shared, session.MaxMessages, session.MessageCount,
|
||||
session.MaxAge, session.CreationTime, session.LastEncryptedTime, store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateOutboundGroupSession replaces an outbound Megolm session with for same room and session ID.
|
||||
func (store *SQLCryptoStore) UpdateOutboundGroupSession(session *OutboundGroupSession) error {
|
||||
sessionBytes := session.Internal.Pickle(store.PickleKey)
|
||||
_, err := store.DB.Exec("UPDATE crypto_megolm_outbound_session SET session=$1, message_count=$2, last_used=$3 WHERE room_id=$4 AND session_id=$5 AND account_id=$6",
|
||||
sessionBytes, session.MessageCount, session.LastEncryptedTime, session.RoomID, session.ID(), store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetOutboundGroupSession retrieves the outbound Megolm session for the given room ID.
|
||||
func (store *SQLCryptoStore) GetOutboundGroupSession(roomID id.RoomID) (*OutboundGroupSession, error) {
|
||||
var ogs OutboundGroupSession
|
||||
var sessionBytes []byte
|
||||
err := store.DB.QueryRow(`
|
||||
SELECT session, shared, max_messages, message_count, max_age, created_at, last_used
|
||||
FROM crypto_megolm_outbound_session WHERE room_id=$1 AND account_id=$2`,
|
||||
roomID, store.AccountID,
|
||||
).Scan(&sessionBytes, &ogs.Shared, &ogs.MaxMessages, &ogs.MessageCount, &ogs.MaxAge, &ogs.CreationTime, &ogs.LastEncryptedTime)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
intOGS := olm.NewBlankOutboundGroupSession()
|
||||
err = intOGS.Unpickle(sessionBytes, store.PickleKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ogs.Internal = *intOGS
|
||||
ogs.RoomID = roomID
|
||||
return &ogs, nil
|
||||
}
|
||||
|
||||
// RemoveOutboundGroupSession removes the outbound Megolm session for the given room ID.
|
||||
func (store *SQLCryptoStore) RemoveOutboundGroupSession(roomID id.RoomID) error {
|
||||
_, err := store.DB.Exec("DELETE FROM crypto_megolm_outbound_session WHERE room_id=$1 AND account_id=$2",
|
||||
roomID, store.AccountID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateMessageIndex returns whether the given event information match the ones stored in the database
|
||||
// for the given sender key, session ID and index. If the index hasn't been stored, this will store it.
|
||||
func (store *SQLCryptoStore) ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
|
||||
const validateQuery = `
|
||||
INSERT INTO crypto_message_index (sender_key, session_id, "index", event_id, timestamp)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
-- have to update something so that RETURNING * always returns the row
|
||||
ON CONFLICT (sender_key, session_id, "index") DO UPDATE SET sender_key=excluded.sender_key
|
||||
RETURNING event_id, timestamp
|
||||
`
|
||||
var expectedEventID id.EventID
|
||||
var expectedTimestamp int64
|
||||
err := store.DB.QueryRow(validateQuery, senderKey, sessionID, index, eventID, timestamp).Scan(&expectedEventID, &expectedTimestamp)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return expectedEventID == eventID && expectedTimestamp == timestamp, nil
|
||||
}
|
||||
|
||||
// GetDevices returns a map of device IDs to device identities, including the identity and signing keys, for a given user ID.
|
||||
func (store *SQLCryptoStore) GetDevices(userID id.UserID) (map[id.DeviceID]*id.Device, error) {
|
||||
var ignore id.UserID
|
||||
err := store.DB.QueryRow("SELECT user_id FROM crypto_tracked_user WHERE user_id=$1", userID).Scan(&ignore)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := store.DB.Query("SELECT device_id, identity_key, signing_key, trust, deleted, name FROM crypto_device WHERE user_id=$1 AND deleted=false", userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make(map[id.DeviceID]*id.Device)
|
||||
for rows.Next() {
|
||||
var identity id.Device
|
||||
err := rows.Scan(&identity.DeviceID, &identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
identity.UserID = userID
|
||||
data[identity.DeviceID] = &identity
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetDevice returns the device dentity for a given user and device ID.
|
||||
func (store *SQLCryptoStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*id.Device, error) {
|
||||
var identity id.Device
|
||||
err := store.DB.QueryRow(`
|
||||
SELECT identity_key, signing_key, trust, deleted, name
|
||||
FROM crypto_device WHERE user_id=$1 AND device_id=$2`,
|
||||
userID, deviceID,
|
||||
).Scan(&identity.IdentityKey, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
identity.UserID = userID
|
||||
identity.DeviceID = deviceID
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
// FindDeviceByKey finds a specific device by its sender key.
|
||||
func (store *SQLCryptoStore) FindDeviceByKey(userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) {
|
||||
var identity id.Device
|
||||
err := store.DB.QueryRow(`
|
||||
SELECT device_id, signing_key, trust, deleted, name
|
||||
FROM crypto_device WHERE user_id=$1 AND identity_key=$2`,
|
||||
userID, identityKey,
|
||||
).Scan(&identity.DeviceID, &identity.SigningKey, &identity.Trust, &identity.Deleted, &identity.Name)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
identity.UserID = userID
|
||||
identity.IdentityKey = identityKey
|
||||
return &identity, nil
|
||||
}
|
||||
|
||||
const deviceInsertQuery = `
|
||||
INSERT INTO crypto_device (user_id, device_id, identity_key, signing_key, trust, deleted, name)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (user_id, device_id) DO UPDATE
|
||||
SET identity_key=excluded.identity_key, deleted=excluded.deleted, trust=excluded.trust, name=excluded.name
|
||||
`
|
||||
|
||||
var deviceMassInsertTemplate = strings.ReplaceAll(deviceInsertQuery, "($1, $2, $3, $4, $5, $6, $7)", "%s")
|
||||
|
||||
// PutDevice stores a single device for a user, replacing it if it exists already.
|
||||
func (store *SQLCryptoStore) PutDevice(userID id.UserID, device *id.Device) error {
|
||||
_, err := store.DB.Exec(deviceInsertQuery,
|
||||
userID, device.DeviceID, device.IdentityKey, device.SigningKey, device.Trust, device.Deleted, device.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
// PutDevices stores the device identity information for the given user ID.
|
||||
func (store *SQLCryptoStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*id.Device) error {
|
||||
tx, err := store.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec("INSERT INTO crypto_tracked_user (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user to tracked users list: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec("UPDATE crypto_device SET deleted=true WHERE user_id=$1", userID)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("failed to delete old devices: %w", err)
|
||||
}
|
||||
if len(devices) == 0 {
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit changes (no devices added): %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
deviceBatchLen := 5 // how many devices will be inserted per query
|
||||
deviceIDs := make([]id.DeviceID, 0, len(devices))
|
||||
for deviceID := range devices {
|
||||
deviceIDs = append(deviceIDs, deviceID)
|
||||
}
|
||||
const valueStringFormat = "($1, $%d, $%d, $%d, $%d, $%d, $%d)"
|
||||
for batchDeviceIdx := 0; batchDeviceIdx < len(deviceIDs); batchDeviceIdx += deviceBatchLen {
|
||||
var batchDevices []id.DeviceID
|
||||
if batchDeviceIdx+deviceBatchLen < len(deviceIDs) {
|
||||
batchDevices = deviceIDs[batchDeviceIdx : batchDeviceIdx+deviceBatchLen]
|
||||
} else {
|
||||
batchDevices = deviceIDs[batchDeviceIdx:]
|
||||
}
|
||||
values := make([]interface{}, 1, len(devices)*6+1)
|
||||
values[0] = userID
|
||||
valueStrings := make([]string, 0, len(devices))
|
||||
i := 2
|
||||
for _, deviceID := range batchDevices {
|
||||
identity := devices[deviceID]
|
||||
values = append(values, deviceID, identity.IdentityKey, identity.SigningKey, identity.Trust, identity.Deleted, identity.Name)
|
||||
valueStrings = append(valueStrings, fmt.Sprintf(valueStringFormat, i, i+1, i+2, i+3, i+4, i+5))
|
||||
i += 6
|
||||
}
|
||||
valueString := strings.Join(valueStrings, ",")
|
||||
_, err = tx.Exec(fmt.Sprintf(deviceMassInsertTemplate, valueString), values...)
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
return fmt.Errorf("failed to insert new devices: %w", err)
|
||||
}
|
||||
}
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit changes: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterTrackedUsers finds all the user IDs out of the given ones for which the database contains identity information.
|
||||
func (store *SQLCryptoStore) FilterTrackedUsers(users []id.UserID) ([]id.UserID, error) {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if store.DB.Dialect == dbutil.Postgres && PostgresArrayWrapper != nil {
|
||||
rows, err = store.DB.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id = ANY($1)", PostgresArrayWrapper(users))
|
||||
} else {
|
||||
queryString := make([]string, len(users))
|
||||
params := make([]interface{}, len(users))
|
||||
for i, user := range users {
|
||||
queryString[i] = fmt.Sprintf("$%d", i+1)
|
||||
params[i] = user
|
||||
}
|
||||
rows, err = store.DB.Query("SELECT user_id FROM crypto_tracked_user WHERE user_id IN ("+strings.Join(queryString, ",")+")", params...)
|
||||
}
|
||||
if err != nil {
|
||||
return users, err
|
||||
}
|
||||
var ptr int
|
||||
for rows.Next() {
|
||||
err = rows.Scan(&users[ptr])
|
||||
if err != nil {
|
||||
return users, err
|
||||
} else {
|
||||
ptr++
|
||||
}
|
||||
}
|
||||
return users[:ptr], nil
|
||||
}
|
||||
|
||||
// PutCrossSigningKey stores a cross-signing key of some user along with its usage.
|
||||
func (store *SQLCryptoStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error {
|
||||
_, err := store.DB.Exec(`
|
||||
INSERT INTO crypto_cross_signing_keys (user_id, usage, key, first_seen_key) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (user_id, usage) DO UPDATE SET key=excluded.key
|
||||
`, userID, usage, key, key)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetCrossSigningKeys retrieves a user's stored cross-signing keys.
|
||||
func (store *SQLCryptoStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) {
|
||||
rows, err := store.DB.Query("SELECT usage, key, first_seen_key FROM crypto_cross_signing_keys WHERE user_id=$1", userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make(map[id.CrossSigningUsage]id.CrossSigningKey)
|
||||
for rows.Next() {
|
||||
var usage id.CrossSigningUsage
|
||||
var key, first id.Ed25519
|
||||
err = rows.Scan(&usage, &key, &first)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data[usage] = id.CrossSigningKey{Key: key, First: first}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// PutSignature stores a signature of a cross-signing or device key along with the signer's user ID and key.
|
||||
func (store *SQLCryptoStore) PutSignature(signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519, signature string) error {
|
||||
_, err := store.DB.Exec(`
|
||||
INSERT INTO crypto_cross_signing_signatures (signed_user_id, signed_key, signer_user_id, signer_key, signature) VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (signed_user_id, signed_key, signer_user_id, signer_key) DO UPDATE SET signature=excluded.signature
|
||||
`, signedUserID, signedKey, signerUserID, signerKey, signature)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSignaturesForKeyBy retrieves the stored signatures for a given cross-signing or device key, by the given signer.
|
||||
func (store *SQLCryptoStore) GetSignaturesForKeyBy(userID id.UserID, key id.Ed25519, signerID id.UserID) (map[id.Ed25519]string, error) {
|
||||
rows, err := store.DB.Query("SELECT signer_key, signature FROM crypto_cross_signing_signatures WHERE signed_user_id=$1 AND signed_key=$2 AND signer_user_id=$3", userID, key, signerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := make(map[id.Ed25519]string)
|
||||
for rows.Next() {
|
||||
var signerKey id.Ed25519
|
||||
var signature string
|
||||
err = rows.Scan(&signerKey, &signature)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data[signerKey] = signature
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// IsKeySignedBy returns whether a cross-signing or device key is signed by the given signer.
|
||||
func (store *SQLCryptoStore) IsKeySignedBy(signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519) (isSigned bool, err error) {
|
||||
q := `SELECT EXISTS(
|
||||
SELECT 1 FROM crypto_cross_signing_signatures
|
||||
WHERE signed_user_id=$1 AND signed_key=$2 AND signer_user_id=$3 AND signer_key=$4
|
||||
)`
|
||||
err = store.DB.QueryRow(q, signedUserID, signedKey, signerUserID, signerKey).Scan(&isSigned)
|
||||
return
|
||||
}
|
||||
|
||||
// DropSignaturesByKey deletes the signatures made by the given user and key from the store. It returns the number of signatures deleted.
|
||||
func (store *SQLCryptoStore) DropSignaturesByKey(userID id.UserID, key id.Ed25519) (int64, error) {
|
||||
res, err := store.DB.Exec("DELETE FROM crypto_cross_signing_signatures WHERE signer_user_id=$1 AND signer_key=$2", userID, key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
count, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
89
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/00-latest-revision.sql
generated
vendored
Normal file
89
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/00-latest-revision.sql
generated
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
-- v0 -> v8: Latest revision
|
||||
CREATE TABLE IF NOT EXISTS crypto_account (
|
||||
account_id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
shared BOOLEAN NOT NULL,
|
||||
sync_token TEXT NOT NULL,
|
||||
account bytea NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_message_index (
|
||||
sender_key CHAR(43),
|
||||
session_id CHAR(43),
|
||||
"index" INTEGER,
|
||||
event_id TEXT NOT NULL,
|
||||
timestamp BIGINT NOT NULL,
|
||||
PRIMARY KEY (sender_key, session_id, "index")
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_tracked_user (
|
||||
user_id TEXT PRIMARY KEY
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_device (
|
||||
user_id TEXT,
|
||||
device_id TEXT,
|
||||
identity_key CHAR(43) NOT NULL,
|
||||
signing_key CHAR(43) NOT NULL,
|
||||
trust SMALLINT NOT NULL,
|
||||
deleted BOOLEAN NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
PRIMARY KEY (user_id, device_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_olm_session (
|
||||
account_id TEXT,
|
||||
session_id CHAR(43),
|
||||
sender_key CHAR(43) NOT NULL,
|
||||
session bytea NOT NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
last_decrypted timestamp NOT NULL,
|
||||
last_encrypted timestamp NOT NULL,
|
||||
PRIMARY KEY (account_id, session_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_megolm_inbound_session (
|
||||
account_id TEXT,
|
||||
session_id CHAR(43),
|
||||
sender_key CHAR(43) NOT NULL,
|
||||
signing_key CHAR(43),
|
||||
room_id TEXT NOT NULL,
|
||||
session bytea,
|
||||
forwarding_chains bytea,
|
||||
withheld_code TEXT,
|
||||
withheld_reason TEXT,
|
||||
PRIMARY KEY (account_id, session_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session (
|
||||
account_id TEXT,
|
||||
room_id TEXT,
|
||||
session_id CHAR(43) NOT NULL UNIQUE,
|
||||
session bytea NOT NULL,
|
||||
shared BOOLEAN NOT NULL,
|
||||
max_messages INTEGER NOT NULL,
|
||||
message_count INTEGER NOT NULL,
|
||||
max_age BIGINT NOT NULL,
|
||||
created_at timestamp NOT NULL,
|
||||
last_used timestamp NOT NULL,
|
||||
PRIMARY KEY (account_id, room_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_cross_signing_keys (
|
||||
user_id TEXT,
|
||||
usage TEXT,
|
||||
key CHAR(43) NOT NULL,
|
||||
|
||||
first_seen_key CHAR(43) NOT NULL,
|
||||
|
||||
PRIMARY KEY (user_id, usage)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_cross_signing_signatures (
|
||||
signed_user_id TEXT,
|
||||
signed_key TEXT,
|
||||
signer_user_id TEXT,
|
||||
signer_key TEXT,
|
||||
signature CHAR(88) NOT NULL,
|
||||
PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key)
|
||||
);
|
||||
16
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/04-cross-signing-keys.sql
generated
vendored
Normal file
16
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/04-cross-signing-keys.sql
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
-- v4: Add tables for cross-signing keys
|
||||
CREATE TABLE IF NOT EXISTS crypto_cross_signing_keys (
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
usage VARCHAR(20) NOT NULL,
|
||||
key CHAR(43) NOT NULL,
|
||||
PRIMARY KEY (user_id, usage)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS crypto_cross_signing_signatures (
|
||||
signed_user_id VARCHAR(255) NOT NULL,
|
||||
signed_key VARCHAR(255) NOT NULL,
|
||||
signer_user_id VARCHAR(255) NOT NULL,
|
||||
signer_key VARCHAR(255) NOT NULL,
|
||||
signature CHAR(88) NOT NULL,
|
||||
PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key)
|
||||
)
|
||||
31
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/05-varchar-to-text.sql
generated
vendored
Normal file
31
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/05-varchar-to-text.sql
generated
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
-- v5: Switch from VARCHAR(255) to TEXT
|
||||
-- only: postgres
|
||||
|
||||
ALTER TABLE crypto_account ALTER COLUMN device_id TYPE TEXT;
|
||||
ALTER TABLE crypto_account ALTER COLUMN account_id TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_device ALTER COLUMN user_id TYPE TEXT;
|
||||
ALTER TABLE crypto_device ALTER COLUMN device_id TYPE TEXT;
|
||||
ALTER TABLE crypto_device ALTER COLUMN name TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_megolm_inbound_session ALTER COLUMN room_id TYPE TEXT;
|
||||
ALTER TABLE crypto_megolm_inbound_session ALTER COLUMN account_id TYPE TEXT;
|
||||
ALTER TABLE crypto_megolm_inbound_session ALTER COLUMN withheld_code TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_megolm_outbound_session ALTER COLUMN room_id TYPE TEXT;
|
||||
ALTER TABLE crypto_megolm_outbound_session ALTER COLUMN account_id TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_message_index ALTER COLUMN event_id TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_olm_session ALTER COLUMN account_id TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_tracked_user ALTER COLUMN user_id TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_cross_signing_keys ALTER COLUMN user_id TYPE TEXT;
|
||||
ALTER TABLE crypto_cross_signing_keys ALTER COLUMN usage TYPE TEXT;
|
||||
|
||||
ALTER TABLE crypto_cross_signing_signatures ALTER COLUMN signed_user_id TYPE TEXT;
|
||||
ALTER TABLE crypto_cross_signing_signatures ALTER COLUMN signed_key TYPE TEXT;
|
||||
ALTER TABLE crypto_cross_signing_signatures ALTER COLUMN signer_user_id TYPE TEXT;
|
||||
ALTER TABLE crypto_cross_signing_signatures ALTER COLUMN signer_key TYPE TEXT;
|
||||
ALTER TABLE crypto_cross_signing_signatures ALTER COLUMN signature TYPE TEXT;
|
||||
6
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/06-olm-session-last-used-split.sql
generated
vendored
Normal file
6
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/06-olm-session-last-used-split.sql
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
-- v6: Split last_used into last_encrypted and last_decrypted for Olm sessions
|
||||
ALTER TABLE crypto_olm_session RENAME COLUMN last_used TO last_decrypted;
|
||||
ALTER TABLE crypto_olm_session ADD COLUMN last_encrypted timestamp;
|
||||
UPDATE crypto_olm_session SET last_encrypted=last_decrypted;
|
||||
-- only: postgres (too complicated on SQLite)
|
||||
ALTER TABLE crypto_olm_session ALTER COLUMN last_encrypted SET NOT NULL;
|
||||
4
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/07-trust-state-value-change.sql
generated
vendored
Normal file
4
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/07-trust-state-value-change.sql
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
-- v7: Update trust state values
|
||||
UPDATE crypto_device SET trust=300 WHERE trust=1; -- verified
|
||||
UPDATE crypto_device SET trust=-100 WHERE trust=2; -- blacklisted
|
||||
UPDATE crypto_device SET trust=0 WHERE trust=3; -- ignored -> unset
|
||||
5
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/08-cs-key-expired-field.sql
generated
vendored
Normal file
5
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/08-cs-key-expired-field.sql
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
-- v8: Add expired field to cross signing keys
|
||||
ALTER TABLE crypto_cross_signing_keys ADD COLUMN first_seen_key CHAR(43);
|
||||
UPDATE crypto_cross_signing_keys SET first_seen_key=key;
|
||||
-- only: postgres
|
||||
ALTER TABLE crypto_cross_signing_keys ALTER COLUMN first_seen_key SET NOT NULL;
|
||||
40
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/upgrade.go
generated
vendored
Normal file
40
vendor/maunium.net/go/mautrix/crypto/sql_store_upgrade/upgrade.go
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2022 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 sql_store_upgrade
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"embed"
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix/util/dbutil"
|
||||
)
|
||||
|
||||
var Table dbutil.UpgradeTable
|
||||
|
||||
const VersionTableName = "crypto_version"
|
||||
|
||||
//go:embed *.sql
|
||||
var fs embed.FS
|
||||
|
||||
func init() {
|
||||
Table.Register(-1, 3, "Unsupported version", func(tx dbutil.Transaction, database *dbutil.Database) error {
|
||||
return fmt.Errorf("upgrading from versions 1 and 2 of the crypto store is no longer supported in mautrix-go v0.12+")
|
||||
})
|
||||
Table.RegisterFS(fs)
|
||||
}
|
||||
|
||||
// Upgrade upgrades the database from the current to the latest version available.
|
||||
func Upgrade(sqlDB *sql.DB, dialect string) error {
|
||||
db, err := dbutil.NewWithDB(sqlDB, dialect)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
db.VersionTable = VersionTableName
|
||||
db.UpgradeTable = Table
|
||||
return db.Upgrade()
|
||||
}
|
||||
108
vendor/maunium.net/go/mautrix/crypto/ssss/client.go
generated
vendored
Normal file
108
vendor/maunium.net/go/mautrix/crypto/ssss/client.go
generated
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
// 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 ssss
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
// Machine contains utility methods for interacting with SSSS data on the server.
|
||||
type Machine struct {
|
||||
Client *mautrix.Client
|
||||
}
|
||||
|
||||
func NewSSSSMachine(client *mautrix.Client) *Machine {
|
||||
return &Machine{
|
||||
Client: client,
|
||||
}
|
||||
}
|
||||
|
||||
type DefaultSecretStorageKeyContent struct {
|
||||
KeyID string `json:"key"`
|
||||
}
|
||||
|
||||
// GetDefaultKeyID retrieves the default key ID for this account from SSSS.
|
||||
func (mach *Machine) GetDefaultKeyID() (string, error) {
|
||||
var data DefaultSecretStorageKeyContent
|
||||
err := mach.Client.GetAccountData(event.AccountDataSecretStorageDefaultKey.Type, &data)
|
||||
if err != nil {
|
||||
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_NOT_FOUND" {
|
||||
return "", ErrNoDefaultKeyAccountDataEvent
|
||||
}
|
||||
return "", fmt.Errorf("failed to get default key account data from server: %w", err)
|
||||
}
|
||||
if len(data.KeyID) == 0 {
|
||||
return "", ErrNoKeyFieldInAccountDataEvent
|
||||
}
|
||||
return data.KeyID, nil
|
||||
}
|
||||
|
||||
// SetDefaultKeyID sets the default key ID for this account on the server.
|
||||
func (mach *Machine) SetDefaultKeyID(keyID string) error {
|
||||
return mach.Client.SetAccountData(event.AccountDataSecretStorageDefaultKey.Type, &DefaultSecretStorageKeyContent{keyID})
|
||||
}
|
||||
|
||||
// GetKeyData gets the details about the given key ID.
|
||||
func (mach *Machine) GetKeyData(keyID string) (keyData *KeyMetadata, err error) {
|
||||
keyData = &KeyMetadata{id: keyID}
|
||||
err = mach.Client.GetAccountData(fmt.Sprintf("%s.%s", event.AccountDataSecretStorageKey.Type, keyID), keyData)
|
||||
return
|
||||
}
|
||||
|
||||
// SetKeyData stores SSSS key metadata on the server.
|
||||
func (mach *Machine) SetKeyData(keyID string, keyData *KeyMetadata) error {
|
||||
return mach.Client.SetAccountData(fmt.Sprintf("%s.%s", event.AccountDataSecretStorageKey.Type, keyID), keyData)
|
||||
}
|
||||
|
||||
// GetDefaultKeyData gets the details about the default key ID (see GetDefaultKeyID).
|
||||
func (mach *Machine) GetDefaultKeyData() (keyID string, keyData *KeyMetadata, err error) {
|
||||
keyID, err = mach.GetDefaultKeyID()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
keyData, err = mach.GetKeyData(keyID)
|
||||
return
|
||||
}
|
||||
|
||||
// GetDecryptedAccountData gets the account data event with the given event type and decrypts it using the given key.
|
||||
func (mach *Machine) GetDecryptedAccountData(eventType event.Type, key *Key) ([]byte, error) {
|
||||
var encData EncryptedAccountDataEventContent
|
||||
err := mach.Client.GetAccountData(eventType.Type, &encData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encData.Decrypt(eventType.Type, key)
|
||||
}
|
||||
|
||||
// SetEncryptedAccountData encrypts the given data with the given keys and stores it on the server.
|
||||
func (mach *Machine) SetEncryptedAccountData(eventType event.Type, data []byte, keys ...*Key) error {
|
||||
if len(keys) == 0 {
|
||||
return ErrNoKeyGiven
|
||||
}
|
||||
encrypted := make(map[string]EncryptedKeyData, len(keys))
|
||||
for _, key := range keys {
|
||||
encrypted[key.ID] = key.Encrypt(eventType.Type, data)
|
||||
}
|
||||
return mach.Client.SetAccountData(eventType.Type, &EncryptedAccountDataEventContent{Encrypted: encrypted})
|
||||
}
|
||||
|
||||
// GenerateAndUploadKey generates a new SSSS key and stores the metadata on the server.
|
||||
func (mach *Machine) GenerateAndUploadKey(passphrase string) (key *Key, err error) {
|
||||
key, err = NewKey(passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate new key: %w", err)
|
||||
}
|
||||
|
||||
err = mach.SetKeyData(key.ID, key.Metadata)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to upload key: %w", err)
|
||||
}
|
||||
return key, err
|
||||
}
|
||||
124
vendor/maunium.net/go/mautrix/crypto/ssss/key.go
generated
vendored
Normal file
124
vendor/maunium.net/go/mautrix/crypto/ssss/key.go
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
// 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 ssss
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/utils"
|
||||
)
|
||||
|
||||
// Key represents a SSSS private key and related metadata.
|
||||
type Key struct {
|
||||
ID string `json:"-"`
|
||||
Key []byte `json:"-"`
|
||||
Metadata *KeyMetadata `json:"-"`
|
||||
}
|
||||
|
||||
// NewKey generates a new SSSS key, optionally based on the given passphrase.
|
||||
//
|
||||
// Errors are only returned if crypto/rand runs out of randomness.
|
||||
func NewKey(passphrase string) (*Key, error) {
|
||||
// We don't support any other algorithms currently.
|
||||
keyData := KeyMetadata{Algorithm: AlgorithmAESHMACSHA2}
|
||||
|
||||
var ssssKey []byte
|
||||
if len(passphrase) > 0 {
|
||||
// There's a passphrase. We need to generate a salt for it, set the metadata
|
||||
// and then compute the key using the passphrase and the metadata.
|
||||
saltBytes := make([]byte, 24)
|
||||
if _, err := rand.Read(saltBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to get random bytes for salt: %w", err)
|
||||
}
|
||||
keyData.Passphrase = &PassphraseMetadata{
|
||||
Algorithm: PassphraseAlgorithmPBKDF2,
|
||||
Iterations: 500000,
|
||||
Salt: base64.StdEncoding.EncodeToString(saltBytes),
|
||||
Bits: 256,
|
||||
}
|
||||
var err error
|
||||
ssssKey, err = keyData.Passphrase.GetKey(passphrase)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get key from passphrase: %w", err)
|
||||
}
|
||||
} else {
|
||||
// No passphrase, just generate a random key
|
||||
ssssKey = make([]byte, 32)
|
||||
if _, err := rand.Read(ssssKey); err != nil {
|
||||
return nil, fmt.Errorf("failed to get random bytes for key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a random ID for the key. It's what identifies the key in account data.
|
||||
keyIDBytes := make([]byte, 24)
|
||||
if _, err := rand.Read(keyIDBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed to get random bytes for key ID: %w", err)
|
||||
}
|
||||
|
||||
// We store a certain hash in the key metadata so that clients can check if the user entered the correct key.
|
||||
var ivBytes [utils.AESCTRIVLength]byte
|
||||
if _, err := rand.Read(ivBytes[:]); err != nil {
|
||||
return nil, fmt.Errorf("failed to get random bytes for IV: %w", err)
|
||||
}
|
||||
keyData.IV = base64.StdEncoding.EncodeToString(ivBytes[:])
|
||||
keyData.MAC = keyData.calculateHash(ssssKey)
|
||||
|
||||
return &Key{
|
||||
Key: ssssKey,
|
||||
ID: base64.StdEncoding.EncodeToString(keyIDBytes),
|
||||
Metadata: &keyData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RecoveryKey gets the recovery key for this SSSS key.
|
||||
func (key *Key) RecoveryKey() string {
|
||||
return utils.EncodeBase58RecoveryKey(key.Key)
|
||||
}
|
||||
|
||||
// Encrypt encrypts the given data with this key.
|
||||
func (key *Key) Encrypt(eventType string, data []byte) EncryptedKeyData {
|
||||
aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)
|
||||
|
||||
iv := utils.GenA256CTRIV()
|
||||
payload := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
|
||||
base64.StdEncoding.Encode(payload, data)
|
||||
utils.XorA256CTR(payload, aesKey, iv)
|
||||
|
||||
return EncryptedKeyData{
|
||||
Ciphertext: base64.StdEncoding.EncodeToString(payload),
|
||||
IV: base64.StdEncoding.EncodeToString(iv[:]),
|
||||
MAC: utils.HMACSHA256B64(payload, hmacKey),
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt decrypts the given encrypted data with this key.
|
||||
func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error) {
|
||||
var ivBytes [utils.AESCTRIVLength]byte
|
||||
decodedIV, _ := base64.StdEncoding.DecodeString(data.IV)
|
||||
copy(ivBytes[:], decodedIV)
|
||||
|
||||
payload, err := base64.StdEncoding.DecodeString(data.Ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// derive the AES and HMAC keys for the requested event type using the SSSS key
|
||||
aesKey, hmacKey := utils.DeriveKeysSHA256(key.Key, eventType)
|
||||
|
||||
// compare the stored MAC with the one we calculated from the ciphertext
|
||||
calcMac := utils.HMACSHA256B64(payload, hmacKey)
|
||||
if strings.ReplaceAll(data.MAC, "=", "") != strings.ReplaceAll(calcMac, "=", "") {
|
||||
return nil, ErrKeyDataMACMismatch
|
||||
}
|
||||
|
||||
utils.XorA256CTR(payload, aesKey, ivBytes)
|
||||
decryptedDecoded, err := base64.StdEncoding.DecodeString(string(payload))
|
||||
return decryptedDecoded, err
|
||||
}
|
||||
102
vendor/maunium.net/go/mautrix/crypto/ssss/meta.go
generated
vendored
Normal file
102
vendor/maunium.net/go/mautrix/crypto/ssss/meta.go
generated
vendored
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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 ssss
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/utils"
|
||||
)
|
||||
|
||||
// KeyMetadata represents server-side metadata about a SSSS key. The metadata can be used to get
|
||||
// the actual SSSS key from a passphrase or recovery key.
|
||||
type KeyMetadata struct {
|
||||
id string
|
||||
|
||||
Algorithm Algorithm `json:"algorithm"`
|
||||
IV string `json:"iv"`
|
||||
MAC string `json:"mac"`
|
||||
Passphrase *PassphraseMetadata `json:"passphrase,omitempty"`
|
||||
}
|
||||
|
||||
// VerifyRecoveryKey verifies that the given passphrase is valid and returns the computed SSSS key.
|
||||
func (kd *KeyMetadata) VerifyPassphrase(passphrase string) (*Key, error) {
|
||||
ssssKey, err := kd.Passphrase.GetKey(passphrase)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !kd.VerifyKey(ssssKey) {
|
||||
return nil, ErrIncorrectSSSSKey
|
||||
}
|
||||
|
||||
return &Key{
|
||||
ID: kd.id,
|
||||
Key: ssssKey,
|
||||
Metadata: kd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyRecoveryKey verifies that the given recovery key is valid and returns the decoded SSSS key.
|
||||
func (kd *KeyMetadata) VerifyRecoveryKey(recoverKey string) (*Key, error) {
|
||||
ssssKey := utils.DecodeBase58RecoveryKey(recoverKey)
|
||||
if ssssKey == nil {
|
||||
return nil, ErrInvalidRecoveryKey
|
||||
} else if !kd.VerifyKey(ssssKey) {
|
||||
return nil, ErrIncorrectSSSSKey
|
||||
}
|
||||
|
||||
return &Key{
|
||||
ID: kd.id,
|
||||
Key: ssssKey,
|
||||
Metadata: kd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyKey verifies the SSSS key is valid by calculating and comparing its MAC.
|
||||
func (kd *KeyMetadata) VerifyKey(key []byte) bool {
|
||||
return strings.ReplaceAll(kd.MAC, "=", "") == strings.ReplaceAll(kd.calculateHash(key), "=", "")
|
||||
}
|
||||
|
||||
// calculateHash calculates the hash used for checking if the key is entered correctly as described
|
||||
// in the spec: https://matrix.org/docs/spec/client_server/unstable#m-secret-storage-v1-aes-hmac-sha2
|
||||
func (kd *KeyMetadata) calculateHash(key []byte) string {
|
||||
aesKey, hmacKey := utils.DeriveKeysSHA256(key, "")
|
||||
|
||||
var ivBytes [utils.AESCTRIVLength]byte
|
||||
_, _ = base64.StdEncoding.Decode(ivBytes[:], []byte(kd.IV))
|
||||
|
||||
cipher := utils.XorA256CTR(make([]byte, utils.AESCTRKeyLength), aesKey, ivBytes)
|
||||
|
||||
return utils.HMACSHA256B64(cipher, hmacKey)
|
||||
}
|
||||
|
||||
// PassphraseMetadata represents server-side metadata about a SSSS key passphrase.
|
||||
type PassphraseMetadata struct {
|
||||
Algorithm PassphraseAlgorithm `json:"algorithm"`
|
||||
Iterations int `json:"iterations"`
|
||||
Salt string `json:"salt"`
|
||||
Bits int `json:"bits"`
|
||||
}
|
||||
|
||||
// GetKey gets the SSSS key from the passphrase.
|
||||
func (pd *PassphraseMetadata) GetKey(passphrase string) ([]byte, error) {
|
||||
if pd == nil {
|
||||
return nil, ErrNoPassphrase
|
||||
}
|
||||
|
||||
if pd.Algorithm != PassphraseAlgorithmPBKDF2 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrUnsupportedPassphraseAlgorithm, pd.Algorithm)
|
||||
}
|
||||
|
||||
bits := 256
|
||||
if pd.Bits != 0 {
|
||||
bits = pd.Bits
|
||||
}
|
||||
|
||||
return utils.PBKDF2SHA512([]byte(passphrase), []byte(pd.Salt), pd.Iterations, bits), nil
|
||||
}
|
||||
75
vendor/maunium.net/go/mautrix/crypto/ssss/types.go
generated
vendored
Normal file
75
vendor/maunium.net/go/mautrix/crypto/ssss/types.go
generated
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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 ssss
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoDefaultKeyID = errors.New("could not find default key ID")
|
||||
ErrNoDefaultKeyAccountDataEvent = fmt.Errorf("%w: no %s event in account data", ErrNoDefaultKeyID, event.AccountDataSecretStorageDefaultKey.Type)
|
||||
ErrNoKeyFieldInAccountDataEvent = fmt.Errorf("%w: missing key field in account data event", ErrNoDefaultKeyID)
|
||||
ErrNoKeyGiven = errors.New("must provide at least one key to encrypt for")
|
||||
|
||||
ErrNotEncryptedForKey = errors.New("data is not encrypted for given key ID")
|
||||
ErrKeyDataMACMismatch = errors.New("key data MAC mismatch")
|
||||
ErrNoPassphrase = errors.New("no passphrase data has been set for the default key")
|
||||
ErrUnsupportedPassphraseAlgorithm = errors.New("unsupported passphrase KDF algorithm")
|
||||
ErrIncorrectSSSSKey = errors.New("incorrect SSSS key")
|
||||
ErrInvalidRecoveryKey = errors.New("invalid recovery key")
|
||||
)
|
||||
|
||||
// Algorithm is the identifier for an SSSS encryption algorithm.
|
||||
type Algorithm string
|
||||
|
||||
const (
|
||||
// AlgorithmAESHMACSHA2 is the current main algorithm.
|
||||
AlgorithmAESHMACSHA2 Algorithm = "m.secret_storage.v1.aes-hmac-sha2"
|
||||
// AlgorithmCurve25519AESSHA2 is the old algorithm
|
||||
AlgorithmCurve25519AESSHA2 Algorithm = "m.secret_storage.v1.curve25519-aes-sha2"
|
||||
)
|
||||
|
||||
// PassphraseAlgorithm is the identifier for an algorithm used to derive a key from a passphrase for SSSS.
|
||||
type PassphraseAlgorithm string
|
||||
|
||||
const (
|
||||
// PassphraseAlgorithmPBKDF2 is the current main algorithm
|
||||
PassphraseAlgorithmPBKDF2 PassphraseAlgorithm = "m.pbkdf2"
|
||||
)
|
||||
|
||||
type EncryptedKeyData struct {
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
IV string `json:"iv"`
|
||||
MAC string `json:"mac"`
|
||||
}
|
||||
|
||||
type EncryptedAccountDataEventContent struct {
|
||||
Encrypted map[string]EncryptedKeyData `json:"encrypted"`
|
||||
}
|
||||
|
||||
func (ed *EncryptedAccountDataEventContent) Decrypt(eventType string, key *Key) ([]byte, error) {
|
||||
keyEncData, ok := ed.Encrypted[key.ID]
|
||||
if !ok {
|
||||
return nil, ErrNotEncryptedForKey
|
||||
}
|
||||
|
||||
return key.Decrypt(eventType, keyEncData)
|
||||
}
|
||||
|
||||
func init() {
|
||||
encryptedContent := reflect.TypeOf(&EncryptedAccountDataEventContent{})
|
||||
event.TypeMap[event.AccountDataCrossSigningMaster] = encryptedContent
|
||||
event.TypeMap[event.AccountDataCrossSigningSelf] = encryptedContent
|
||||
event.TypeMap[event.AccountDataCrossSigningUser] = encryptedContent
|
||||
event.TypeMap[event.AccountDataSecretStorageDefaultKey] = reflect.TypeOf(&DefaultSecretStorageKeyContent{})
|
||||
event.TypeMap[event.AccountDataSecretStorageKey] = reflect.TypeOf(&KeyMetadata{})
|
||||
}
|
||||
583
vendor/maunium.net/go/mautrix/crypto/store.go
generated
vendored
Normal file
583
vendor/maunium.net/go/mautrix/crypto/store.go
generated
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
// Copyright (c) 2022 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/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// Deprecated: moved to id.Device
|
||||
type DeviceIdentity = id.Device
|
||||
|
||||
var ErrGroupSessionWithheld = errors.New("group session has been withheld")
|
||||
|
||||
// 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)
|
||||
|
||||
// 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. If an earlier withhold event has been inserted
|
||||
// with PutWithheldGroupSession, this call should replace that. However, PutWithheldGroupSession must not replace
|
||||
// sessions inserted with this call.
|
||||
PutGroupSession(id.RoomID, id.SenderKey, id.SessionID, *InboundGroupSession) error
|
||||
// GetGroupSession gets an inbound Megolm session from the store. If the group session has been withheld
|
||||
// (i.e. a room key withheld event has been saved with PutWithheldGroupSession), this should return the
|
||||
// ErrGroupSessionWithheld error. The caller may use GetWithheldGroupSession to find more details.
|
||||
GetGroupSession(id.RoomID, id.SenderKey, id.SessionID) (*InboundGroupSession, error)
|
||||
// PutWithheldGroupSession tells the store that a specific Megolm session was withheld.
|
||||
PutWithheldGroupSession(event.RoomKeyWithheldEventContent) error
|
||||
// GetWithheldGroupSession gets the event content that was previously inserted with PutWithheldGroupSession.
|
||||
GetWithheldGroupSession(id.RoomID, id.SenderKey, id.SessionID) (*event.RoomKeyWithheldEventContent, error)
|
||||
|
||||
// GetGroupSessionsForRoom gets all the inbound Megolm sessions for a specific room. This is used for creating key
|
||||
// export files. Unlike GetGroupSession, this should not return any errors about withheld keys.
|
||||
GetGroupSessionsForRoom(id.RoomID) ([]*InboundGroupSession, error)
|
||||
// GetAllGroupSessions gets all the inbound Megolm sessions in the store. This is used for creating key export
|
||||
// files. Unlike GetGroupSession, this should not return any errors about withheld keys.
|
||||
GetAllGroupSessions() ([]*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, error)
|
||||
|
||||
// GetDevices returns a map from device ID to DeviceIdentity containing all devices of a given user.
|
||||
GetDevices(id.UserID) (map[id.DeviceID]*id.Device, error)
|
||||
// GetDevice returns a specific device of a given user.
|
||||
GetDevice(id.UserID, id.DeviceID) (*id.Device, error)
|
||||
// PutDevice stores a single device for a user, replacing it if it exists already.
|
||||
PutDevice(id.UserID, *id.Device) error
|
||||
// PutDevices overrides the stored device list for the given user with the given list.
|
||||
PutDevices(id.UserID, map[id.DeviceID]*id.Device) error
|
||||
// FindDeviceByKey finds a specific device by its identity key.
|
||||
FindDeviceByKey(id.UserID, id.IdentityKey) (*id.Device, 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, error)
|
||||
|
||||
// PutCrossSigningKey stores a cross-signing key of some user along with its usage.
|
||||
PutCrossSigningKey(id.UserID, id.CrossSigningUsage, id.Ed25519) error
|
||||
// GetCrossSigningKeys retrieves a user's stored cross-signing keys.
|
||||
GetCrossSigningKeys(id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error)
|
||||
// PutSignature stores a signature of a cross-signing or device key along with the signer's user ID and key.
|
||||
PutSignature(signedUser id.UserID, signedKey id.Ed25519, signerUser id.UserID, signerKey id.Ed25519, signature string) error
|
||||
// IsKeySignedBy returns whether a cross-signing or device key is signed by the given signer.
|
||||
IsKeySignedBy(userID id.UserID, key id.Ed25519, signedByUser id.UserID, signedByKey id.Ed25519) (bool, error)
|
||||
// DropSignaturesByKey deletes the signatures made by the given user and key from the store. It returns the number of signatures deleted.
|
||||
DropSignaturesByKey(id.UserID, id.Ed25519) (int64, error)
|
||||
}
|
||||
|
||||
type messageIndexKey struct {
|
||||
SenderKey id.SenderKey
|
||||
SessionID id.SessionID
|
||||
Index uint
|
||||
}
|
||||
|
||||
type messageIndexValue struct {
|
||||
EventID id.EventID
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// GobStore is a simple Store implementation that dumps everything into a .gob file.
|
||||
//
|
||||
// Deprecated: this is not atomic and can lose data. Using SQLCryptoStore or a custom implementation is recommended.
|
||||
type GobStore struct {
|
||||
lock sync.RWMutex
|
||||
path string
|
||||
|
||||
Account *OlmAccount
|
||||
Sessions map[id.SenderKey]OlmSessionList
|
||||
GroupSessions map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession
|
||||
WithheldGroupSessions map[id.RoomID]map[id.SenderKey]map[id.SessionID]*event.RoomKeyWithheldEventContent
|
||||
OutGroupSessions map[id.RoomID]*OutboundGroupSession
|
||||
MessageIndices map[messageIndexKey]messageIndexValue
|
||||
Devices map[id.UserID]map[id.DeviceID]*id.Device
|
||||
CrossSigningKeys map[id.UserID]map[id.CrossSigningUsage]id.CrossSigningKey
|
||||
KeySignatures map[id.UserID]map[id.Ed25519]map[id.UserID]map[id.Ed25519]string
|
||||
}
|
||||
|
||||
var _ Store = (*GobStore)(nil)
|
||||
|
||||
// NewGobStore creates a new GobStore that saves everything to the given file.
|
||||
//
|
||||
// Deprecated: this is not atomic and can lose data. Using SQLCryptoStore or a custom implementation is recommended.
|
||||
func NewGobStore(path string) (*GobStore, error) {
|
||||
gs := &GobStore{
|
||||
path: path,
|
||||
Sessions: make(map[id.SenderKey]OlmSessionList),
|
||||
GroupSessions: make(map[id.RoomID]map[id.SenderKey]map[id.SessionID]*InboundGroupSession),
|
||||
WithheldGroupSessions: make(map[id.RoomID]map[id.SenderKey]map[id.SessionID]*event.RoomKeyWithheldEventContent),
|
||||
OutGroupSessions: make(map[id.RoomID]*OutboundGroupSession),
|
||||
MessageIndices: make(map[messageIndexKey]messageIndexValue),
|
||||
Devices: make(map[id.UserID]map[id.DeviceID]*id.Device),
|
||||
CrossSigningKeys: make(map[id.UserID]map[id.CrossSigningUsage]id.CrossSigningKey),
|
||||
KeySignatures: make(map[id.UserID]map[id.Ed25519]map[id.UserID]map[id.Ed25519]string),
|
||||
}
|
||||
return gs, gs.load()
|
||||
}
|
||||
|
||||
func (gs *GobStore) save() error {
|
||||
file, err := os.OpenFile(gs.path, os.O_CREATE|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = gob.NewEncoder(file).Encode(gs)
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) load() error {
|
||||
file, err := os.OpenFile(gs.path, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
err = gob.NewDecoder(file).Decode(gs)
|
||||
_ = file.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) Flush() error {
|
||||
gs.lock.Lock()
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetAccount() (*OlmAccount, error) {
|
||||
return gs.Account, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutAccount(account *OlmAccount) error {
|
||||
gs.lock.Lock()
|
||||
gs.Account = account
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetSessions(senderKey id.SenderKey) (OlmSessionList, error) {
|
||||
gs.lock.Lock()
|
||||
sessions, ok := gs.Sessions[senderKey]
|
||||
if !ok {
|
||||
sessions = []*OlmSession{}
|
||||
gs.Sessions[senderKey] = sessions
|
||||
}
|
||||
gs.lock.Unlock()
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) AddSession(senderKey id.SenderKey, session *OlmSession) error {
|
||||
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) UpdateSession(_ id.SenderKey, _ *OlmSession) error {
|
||||
// we don't need to do anything here because the session is a pointer and already stored in our map
|
||||
return gs.save()
|
||||
}
|
||||
|
||||
func (gs *GobStore) HasSession(senderKey id.SenderKey) bool {
|
||||
gs.lock.RLock()
|
||||
sessions, ok := gs.Sessions[senderKey]
|
||||
gs.lock.RUnlock()
|
||||
return ok && len(sessions) > 0 && !sessions[0].Expired()
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetLatestSession(senderKey id.SenderKey) (*OlmSession, error) {
|
||||
gs.lock.RLock()
|
||||
sessions, ok := gs.Sessions[senderKey]
|
||||
gs.lock.RUnlock()
|
||||
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 {
|
||||
room = make(map[id.SenderKey]map[id.SessionID]*InboundGroupSession)
|
||||
gs.GroupSessions[roomID] = room
|
||||
}
|
||||
sender, ok := room[senderKey]
|
||||
if !ok {
|
||||
sender = make(map[id.SessionID]*InboundGroupSession)
|
||||
room[senderKey] = sender
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, igs *InboundGroupSession) error {
|
||||
gs.lock.Lock()
|
||||
gs.getGroupSessions(roomID, senderKey)[sessionID] = igs
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*InboundGroupSession, error) {
|
||||
gs.lock.Lock()
|
||||
session, ok := gs.getGroupSessions(roomID, senderKey)[sessionID]
|
||||
if !ok {
|
||||
withheld, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID]
|
||||
gs.lock.Unlock()
|
||||
if ok {
|
||||
return nil, fmt.Errorf("%w (%s)", ErrGroupSessionWithheld, withheld.Code)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
gs.lock.Unlock()
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) getWithheldGroupSessions(roomID id.RoomID, senderKey id.SenderKey) map[id.SessionID]*event.RoomKeyWithheldEventContent {
|
||||
room, ok := gs.WithheldGroupSessions[roomID]
|
||||
if !ok {
|
||||
room = make(map[id.SenderKey]map[id.SessionID]*event.RoomKeyWithheldEventContent)
|
||||
gs.WithheldGroupSessions[roomID] = room
|
||||
}
|
||||
sender, ok := room[senderKey]
|
||||
if !ok {
|
||||
sender = make(map[id.SessionID]*event.RoomKeyWithheldEventContent)
|
||||
room[senderKey] = sender
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutWithheldGroupSession(content event.RoomKeyWithheldEventContent) error {
|
||||
gs.lock.Lock()
|
||||
gs.getWithheldGroupSessions(content.RoomID, content.SenderKey)[content.SessionID] = &content
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetWithheldGroupSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID) (*event.RoomKeyWithheldEventContent, error) {
|
||||
gs.lock.Lock()
|
||||
session, ok := gs.getWithheldGroupSessions(roomID, senderKey)[sessionID]
|
||||
gs.lock.Unlock()
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetGroupSessionsForRoom(roomID id.RoomID) ([]*InboundGroupSession, error) {
|
||||
gs.lock.Lock()
|
||||
defer gs.lock.Unlock()
|
||||
room, ok := gs.GroupSessions[roomID]
|
||||
if !ok {
|
||||
return []*InboundGroupSession{}, nil
|
||||
}
|
||||
var result []*InboundGroupSession
|
||||
for _, sessions := range room {
|
||||
for _, session := range sessions {
|
||||
result = append(result, session)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetAllGroupSessions() ([]*InboundGroupSession, error) {
|
||||
gs.lock.Lock()
|
||||
var result []*InboundGroupSession
|
||||
for _, room := range gs.GroupSessions {
|
||||
for _, sessions := range room {
|
||||
for _, session := range sessions {
|
||||
result = append(result, session)
|
||||
}
|
||||
}
|
||||
}
|
||||
gs.lock.Unlock()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) AddOutboundGroupSession(session *OutboundGroupSession) error {
|
||||
gs.lock.Lock()
|
||||
gs.OutGroupSessions[session.RoomID] = session
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) UpdateOutboundGroupSession(_ *OutboundGroupSession) error {
|
||||
// we don't need to do anything here because the session is a pointer and already stored in our map
|
||||
return gs.save()
|
||||
}
|
||||
|
||||
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) RemoveOutboundGroupSession(roomID id.RoomID) error {
|
||||
gs.lock.Lock()
|
||||
session, ok := gs.OutGroupSessions[roomID]
|
||||
if !ok || session == nil {
|
||||
gs.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
delete(gs.OutGroupSessions, roomID)
|
||||
gs.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) ValidateMessageIndex(senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
|
||||
gs.lock.Lock()
|
||||
defer gs.lock.Unlock()
|
||||
key := messageIndexKey{
|
||||
SenderKey: senderKey,
|
||||
SessionID: sessionID,
|
||||
Index: index,
|
||||
}
|
||||
val, ok := gs.MessageIndices[key]
|
||||
if !ok {
|
||||
gs.MessageIndices[key] = messageIndexValue{
|
||||
EventID: eventID,
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
_ = gs.save()
|
||||
return true, nil
|
||||
}
|
||||
if val.EventID != eventID || val.Timestamp != timestamp {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetDevices(userID id.UserID) (map[id.DeviceID]*id.Device, error) {
|
||||
gs.lock.RLock()
|
||||
devices, ok := gs.Devices[userID]
|
||||
if !ok {
|
||||
devices = nil
|
||||
}
|
||||
gs.lock.RUnlock()
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetDevice(userID id.UserID, deviceID id.DeviceID) (*id.Device, error) {
|
||||
gs.lock.RLock()
|
||||
defer gs.lock.RUnlock()
|
||||
devices, ok := gs.Devices[userID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
device, ok := devices[deviceID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return device, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) FindDeviceByKey(userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) {
|
||||
gs.lock.RLock()
|
||||
defer gs.lock.RUnlock()
|
||||
devices, ok := gs.Devices[userID]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
for _, device := range devices {
|
||||
if device.IdentityKey == identityKey {
|
||||
return device, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutDevice(userID id.UserID, device *id.Device) error {
|
||||
gs.lock.Lock()
|
||||
devices, ok := gs.Devices[userID]
|
||||
if !ok {
|
||||
devices = make(map[id.DeviceID]*id.Device)
|
||||
gs.Devices[userID] = devices
|
||||
}
|
||||
devices[device.DeviceID] = device
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutDevices(userID id.UserID, devices map[id.DeviceID]*id.Device) error {
|
||||
gs.lock.Lock()
|
||||
gs.Devices[userID] = devices
|
||||
err := gs.save()
|
||||
gs.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) FilterTrackedUsers(users []id.UserID) ([]id.UserID, error) {
|
||||
gs.lock.RLock()
|
||||
var ptr int
|
||||
for _, userID := range users {
|
||||
_, ok := gs.Devices[userID]
|
||||
if ok {
|
||||
users[ptr] = userID
|
||||
ptr++
|
||||
}
|
||||
}
|
||||
gs.lock.RUnlock()
|
||||
return users[:ptr], nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error {
|
||||
gs.lock.RLock()
|
||||
userKeys, ok := gs.CrossSigningKeys[userID]
|
||||
if !ok {
|
||||
userKeys = make(map[id.CrossSigningUsage]id.CrossSigningKey)
|
||||
gs.CrossSigningKeys[userID] = userKeys
|
||||
}
|
||||
existing, ok := userKeys[usage]
|
||||
if ok {
|
||||
existing.Key = key
|
||||
userKeys[usage] = existing
|
||||
} else {
|
||||
userKeys[usage] = id.CrossSigningKey{
|
||||
Key: key,
|
||||
First: key,
|
||||
}
|
||||
}
|
||||
err := gs.save()
|
||||
gs.lock.RUnlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]id.CrossSigningKey, error) {
|
||||
gs.lock.RLock()
|
||||
defer gs.lock.RUnlock()
|
||||
keys, ok := gs.CrossSigningKeys[userID]
|
||||
if !ok {
|
||||
return map[id.CrossSigningUsage]id.CrossSigningKey{}, nil
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) PutSignature(signedUserID id.UserID, signedKey id.Ed25519, signerUserID id.UserID, signerKey id.Ed25519, signature string) error {
|
||||
gs.lock.RLock()
|
||||
signedUserSigs, ok := gs.KeySignatures[signedUserID]
|
||||
if !ok {
|
||||
signedUserSigs = make(map[id.Ed25519]map[id.UserID]map[id.Ed25519]string)
|
||||
gs.KeySignatures[signedUserID] = signedUserSigs
|
||||
}
|
||||
signaturesForKey, ok := signedUserSigs[signedKey]
|
||||
if !ok {
|
||||
signaturesForKey = make(map[id.UserID]map[id.Ed25519]string)
|
||||
signedUserSigs[signedKey] = signaturesForKey
|
||||
}
|
||||
signedByUser, ok := signaturesForKey[signerUserID]
|
||||
if !ok {
|
||||
signedByUser = make(map[id.Ed25519]string)
|
||||
signaturesForKey[signerUserID] = signedByUser
|
||||
}
|
||||
signedByUser[signerKey] = signature
|
||||
err := gs.save()
|
||||
gs.lock.RUnlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (gs *GobStore) GetSignaturesForKeyBy(userID id.UserID, key id.Ed25519, signerID id.UserID) (map[id.Ed25519]string, error) {
|
||||
gs.lock.RLock()
|
||||
defer gs.lock.RUnlock()
|
||||
userKeys, ok := gs.KeySignatures[userID]
|
||||
if !ok {
|
||||
return map[id.Ed25519]string{}, nil
|
||||
}
|
||||
sigsForKey, ok := userKeys[key]
|
||||
if !ok {
|
||||
return map[id.Ed25519]string{}, nil
|
||||
}
|
||||
sigsBySigner, ok := sigsForKey[signerID]
|
||||
if !ok {
|
||||
return map[id.Ed25519]string{}, nil
|
||||
}
|
||||
return sigsBySigner, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) IsKeySignedBy(userID id.UserID, key id.Ed25519, signerID id.UserID, signerKey id.Ed25519) (bool, error) {
|
||||
sigs, err := gs.GetSignaturesForKeyBy(userID, key, signerID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
_, ok := sigs[signerKey]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (gs *GobStore) DropSignaturesByKey(userID id.UserID, key id.Ed25519) (int64, error) {
|
||||
var count int64
|
||||
gs.lock.RLock()
|
||||
for _, userSigs := range gs.KeySignatures {
|
||||
for _, keySigs := range userSigs {
|
||||
if signedBySigner, ok := keySigs[userID]; ok {
|
||||
if _, ok := signedBySigner[key]; ok {
|
||||
count++
|
||||
delete(signedBySigner, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gs.lock.RUnlock()
|
||||
return count, nil
|
||||
}
|
||||
133
vendor/maunium.net/go/mautrix/crypto/utils/utils.go
generated
vendored
Normal file
133
vendor/maunium.net/go/mautrix/crypto/utils/utils.go
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// 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 utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
||||
"maunium.net/go/mautrix/util/base58"
|
||||
)
|
||||
|
||||
const (
|
||||
// AESCTRKeyLength is the length of the AES256-CTR key used.
|
||||
AESCTRKeyLength = 32
|
||||
// AESCTRIVLength is the length of the AES256-CTR IV used.
|
||||
AESCTRIVLength = 16
|
||||
// HMACKeyLength is the length of the HMAC key used.
|
||||
HMACKeyLength = 32
|
||||
// SHAHashLength is the length of the SHA hash used.
|
||||
SHAHashLength = 32
|
||||
)
|
||||
|
||||
// XorA256CTR encrypts the input with the keystream generated by the AES256-CTR algorithm with the given arguments.
|
||||
func XorA256CTR(source []byte, key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) []byte {
|
||||
block, _ := aes.NewCipher(key[:])
|
||||
cipher.NewCTR(block, iv[:]).XORKeyStream(source, source)
|
||||
return source
|
||||
}
|
||||
|
||||
// GenAttachmentA256CTR generates a new random AES256-CTR key and IV suitable for encrypting attachments.
|
||||
func GenAttachmentA256CTR() (key [AESCTRKeyLength]byte, iv [AESCTRIVLength]byte) {
|
||||
_, err := rand.Read(key[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// The last 8 bytes of the IV act as the counter in AES-CTR, which means they're left empty here
|
||||
_, err = rand.Read(iv[:8])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GenA256CTRIV generates a random IV for AES256-CTR with the last bit set to zero.
|
||||
func GenA256CTRIV() (iv [AESCTRIVLength]byte) {
|
||||
_, err := rand.Read(iv[:])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
iv[8] &= 0x7F
|
||||
return
|
||||
}
|
||||
|
||||
// DeriveKeysSHA256 derives an AES and a HMAC key from the given recovery key.
|
||||
func DeriveKeysSHA256(key []byte, name string) ([AESCTRKeyLength]byte, [HMACKeyLength]byte) {
|
||||
var zeroBytes [32]byte
|
||||
|
||||
derivedHkdf := hkdf.New(sha256.New, key[:], zeroBytes[:], []byte(name))
|
||||
|
||||
var aesKey [AESCTRKeyLength]byte
|
||||
var hmacKey [HMACKeyLength]byte
|
||||
derivedHkdf.Read(aesKey[:])
|
||||
derivedHkdf.Read(hmacKey[:])
|
||||
|
||||
return aesKey, hmacKey
|
||||
}
|
||||
|
||||
// PBKDF2SHA512 generates a key of the given bit-length using the given passphrase, salt and iteration count.
|
||||
func PBKDF2SHA512(password []byte, salt []byte, iters int, keyLenBits int) []byte {
|
||||
return pbkdf2.Key(password, salt, iters, keyLenBits/8, sha512.New)
|
||||
}
|
||||
|
||||
// DecodeBase58RecoveryKey recovers the secret storage from a recovery key.
|
||||
func DecodeBase58RecoveryKey(recoveryKey string) []byte {
|
||||
noSpaces := strings.ReplaceAll(recoveryKey, " ", "")
|
||||
decoded := base58.Decode(noSpaces)
|
||||
if len(decoded) != AESCTRKeyLength+3 { // AESCTRKeyLength bytes key and 3 bytes prefix / parity
|
||||
return nil
|
||||
}
|
||||
var parity byte
|
||||
for _, b := range decoded[:34] {
|
||||
parity ^= b
|
||||
}
|
||||
if parity != decoded[34] || decoded[0] != 0x8B || decoded[1] != 1 {
|
||||
return nil
|
||||
}
|
||||
return decoded[2:34]
|
||||
}
|
||||
|
||||
// EncodeBase58RecoveryKey recovers the secret storage from a recovery key.
|
||||
func EncodeBase58RecoveryKey(key []byte) string {
|
||||
var inputBytes [35]byte
|
||||
copy(inputBytes[2:34], key[:])
|
||||
inputBytes[0] = 0x8B
|
||||
inputBytes[1] = 1
|
||||
|
||||
var parity byte
|
||||
for _, b := range inputBytes[:34] {
|
||||
parity ^= b
|
||||
}
|
||||
inputBytes[34] = parity
|
||||
recoveryKey := base58.Encode(inputBytes[:])
|
||||
|
||||
var spacedKey string
|
||||
for i, c := range recoveryKey {
|
||||
if i > 0 && i%4 == 0 {
|
||||
spacedKey += " "
|
||||
}
|
||||
spacedKey += string(c)
|
||||
}
|
||||
return spacedKey
|
||||
}
|
||||
|
||||
// HMACSHA256B64 calculates the base64 of the SHA256 hmac of the input with the given key.
|
||||
func HMACSHA256B64(input []byte, hmacKey [HMACKeyLength]byte) string {
|
||||
h := hmac.New(sha256.New, hmacKey[:])
|
||||
h.Write(input)
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
804
vendor/maunium.net/go/mautrix/crypto/verification.go
generated
vendored
Normal file
804
vendor/maunium.net/go/mautrix/crypto/verification.go
generated
vendored
Normal file
@@ -0,0 +1,804 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// 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/.
|
||||
|
||||
//go:build !nosas
|
||||
// +build !nosas
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto/canonicaljson"
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnknownUserForTransaction = errors.New("unknown user for transaction")
|
||||
ErrTransactionAlreadyExists = errors.New("transaction already exists")
|
||||
// ErrUnknownTransaction is returned when a key verification message is received with an unknown transaction ID.
|
||||
ErrUnknownTransaction = errors.New("unknown transaction")
|
||||
// ErrUnknownVerificationMethod is returned when the verification method in a received m.key.verification.start is unknown.
|
||||
ErrUnknownVerificationMethod = errors.New("unknown verification method")
|
||||
)
|
||||
|
||||
type VerificationHooks interface {
|
||||
// VerifySASMatch receives the generated SAS and its method, as well as the device that is being verified.
|
||||
// It returns whether the given SAS match with the SAS displayed on other device.
|
||||
VerifySASMatch(otherDevice *id.Device, sas SASData) bool
|
||||
// VerificationMethods returns the list of supported verification methods in order of preference.
|
||||
// It must contain at least the decimal method.
|
||||
VerificationMethods() []VerificationMethod
|
||||
OnCancel(cancelledByUs bool, reason string, reasonCode event.VerificationCancelCode)
|
||||
OnSuccess()
|
||||
}
|
||||
|
||||
type VerificationRequestResponse int
|
||||
|
||||
const (
|
||||
AcceptRequest VerificationRequestResponse = iota
|
||||
RejectRequest
|
||||
IgnoreRequest
|
||||
)
|
||||
|
||||
// sendToOneDevice sends a to-device event to a single device.
|
||||
func (mach *OlmMachine) sendToOneDevice(userID id.UserID, deviceID id.DeviceID, eventType event.Type, content interface{}) error {
|
||||
_, err := mach.Client.SendToDevice(eventType, &mautrix.ReqSendToDevice{
|
||||
Messages: map[id.UserID]map[id.DeviceID]*event.Content{
|
||||
userID: {
|
||||
deviceID: {
|
||||
Parsed: content,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) getPKAndKeysMAC(sas *olm.SAS, sendingUser id.UserID, sendingDevice id.DeviceID, receivingUser id.UserID, receivingDevice id.DeviceID,
|
||||
transactionID string, signingKey id.SigningKey, mainKeyID id.KeyID, keys map[id.KeyID]string) (string, string, error) {
|
||||
sasInfo := "MATRIX_KEY_VERIFICATION_MAC" +
|
||||
sendingUser.String() + sendingDevice.String() +
|
||||
receivingUser.String() + receivingDevice.String() +
|
||||
transactionID
|
||||
|
||||
// get key IDs from key map
|
||||
keyIDStrings := make([]string, len(keys))
|
||||
i := 0
|
||||
for keyID := range keys {
|
||||
keyIDStrings[i] = keyID.String()
|
||||
i++
|
||||
}
|
||||
sort.Sort(sort.StringSlice(keyIDStrings))
|
||||
keyIDString := strings.Join(keyIDStrings, ",")
|
||||
|
||||
pubKeyMac, err := sas.CalculateMAC([]byte(signingKey), []byte(sasInfo+mainKeyID.String()))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
mach.Log.Trace("sas.CalculateMAC(\"%s\", \"%s\") -> \"%s\"", signingKey, sasInfo+mainKeyID.String(), string(pubKeyMac))
|
||||
|
||||
keysMac, err := sas.CalculateMAC([]byte(keyIDString), []byte(sasInfo+"KEY_IDS"))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
mach.Log.Trace("sas.CalculateMAC(\"%s\", \"%s\") -> \"%s\"", keyIDString, sasInfo+"KEY_IDS", string(keysMac))
|
||||
|
||||
return string(pubKeyMac), string(keysMac), nil
|
||||
}
|
||||
|
||||
// verificationState holds all the information needed for the state of a SAS verification with another device.
|
||||
type verificationState struct {
|
||||
sas *olm.SAS
|
||||
otherDevice *id.Device
|
||||
initiatedByUs bool
|
||||
verificationStarted bool
|
||||
keyReceived bool
|
||||
sasMatched chan bool
|
||||
commitment string
|
||||
startEventCanonical string
|
||||
chosenSASMethod VerificationMethod
|
||||
hooks VerificationHooks
|
||||
extendTimeout context.CancelFunc
|
||||
inRoomID id.RoomID
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// getTransactionState retrieves the given transaction's state, or cancels the transaction if it cannot be found or there is a mismatch.
|
||||
func (mach *OlmMachine) getTransactionState(transactionID string, userID id.UserID) (*verificationState, error) {
|
||||
verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID)
|
||||
if !ok {
|
||||
_ = mach.SendSASVerificationCancel(userID, id.DeviceID("*"), transactionID, "Unknown transaction: "+transactionID, event.VerificationCancelUnknownTransaction)
|
||||
return nil, ErrUnknownTransaction
|
||||
}
|
||||
verState := verStateInterface.(*verificationState)
|
||||
if verState.otherDevice.UserID != userID {
|
||||
reason := fmt.Sprintf("Unknown user for transaction %v: %v", transactionID, userID)
|
||||
if verState.inRoomID == "" {
|
||||
_ = mach.SendSASVerificationCancel(userID, id.DeviceID("*"), transactionID, reason, event.VerificationCancelUserMismatch)
|
||||
} else {
|
||||
_ = mach.SendInRoomSASVerificationCancel(verState.inRoomID, userID, transactionID, reason, event.VerificationCancelUserMismatch)
|
||||
}
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
return nil, fmt.Errorf("%w %s: %s", ErrUnknownUserForTransaction, transactionID, userID)
|
||||
}
|
||||
return verState, nil
|
||||
}
|
||||
|
||||
// handleVerificationStart handles an incoming m.key.verification.start message.
|
||||
// It initializes the state for this SAS verification process and stores it.
|
||||
func (mach *OlmMachine) handleVerificationStart(userID id.UserID, content *event.VerificationStartEventContent, transactionID string, timeout time.Duration, inRoomID id.RoomID) {
|
||||
mach.Log.Debug("Received verification start from %v", content.FromDevice)
|
||||
otherDevice, err := mach.GetOrFetchDevice(userID, content.FromDevice)
|
||||
if err != nil {
|
||||
mach.Log.Error("Could not find device %v of user %v", content.FromDevice, userID)
|
||||
return
|
||||
}
|
||||
warnAndCancel := func(logReason, cancelReason string) {
|
||||
mach.Log.Warn("Canceling verification transaction %v as it %s", transactionID, logReason)
|
||||
if inRoomID == "" {
|
||||
_ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, cancelReason, event.VerificationCancelUnknownMethod)
|
||||
} else {
|
||||
_ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, cancelReason, event.VerificationCancelUnknownMethod)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case content.Method != event.VerificationMethodSAS:
|
||||
warnAndCancel("is not SAS", "Only SAS method is supported")
|
||||
case !content.SupportsKeyAgreementProtocol(event.KeyAgreementCurve25519HKDFSHA256):
|
||||
warnAndCancel("does not support key agreement protocol curve25519-hkdf-sha256",
|
||||
"Only curve25519-hkdf-sha256 key agreement protocol is supported")
|
||||
case !content.SupportsHashMethod(event.VerificationHashSHA256):
|
||||
warnAndCancel("does not support SHA256 hashing", "Only SHA256 hashing is supported")
|
||||
case !content.SupportsMACMethod(event.HKDFHMACSHA256):
|
||||
warnAndCancel("does not support MAC method hkdf-hmac-sha256", "Only hkdf-hmac-sha256 MAC method is supported")
|
||||
case !content.SupportsSASMethod(event.SASDecimal):
|
||||
warnAndCancel("does not support decimal SAS", "Decimal SAS method must be supported")
|
||||
default:
|
||||
mach.actuallyStartVerification(userID, content, otherDevice, transactionID, timeout, inRoomID)
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) actuallyStartVerification(userID id.UserID, content *event.VerificationStartEventContent, otherDevice *id.Device, transactionID string, timeout time.Duration, inRoomID id.RoomID) {
|
||||
if inRoomID != "" && transactionID != "" {
|
||||
verState, err := mach.getTransactionState(transactionID, userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to get transaction state for in-room verification %s start: %v", transactionID, err)
|
||||
_ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Internal state error in gomuks :(", "net.maunium.internal_error")
|
||||
return
|
||||
}
|
||||
mach.timeoutAfter(verState, transactionID, timeout)
|
||||
sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString)
|
||||
err = mach.SendInRoomSASVerificationAccept(inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error accepting in-room SAS verification: %v", err)
|
||||
}
|
||||
verState.chosenSASMethod = sasMethods[0]
|
||||
verState.verificationStarted = true
|
||||
return
|
||||
}
|
||||
resp, hooks := mach.AcceptVerificationFrom(transactionID, otherDevice, inRoomID)
|
||||
if resp == AcceptRequest {
|
||||
sasMethods := commonSASMethods(hooks, content.ShortAuthenticationString)
|
||||
if len(sasMethods) == 0 {
|
||||
mach.Log.Error("No common SAS methods: %v", content.ShortAuthenticationString)
|
||||
if inRoomID == "" {
|
||||
_ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod)
|
||||
} else {
|
||||
_ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod)
|
||||
}
|
||||
return
|
||||
}
|
||||
verState := &verificationState{
|
||||
sas: olm.NewSAS(),
|
||||
otherDevice: otherDevice,
|
||||
initiatedByUs: false,
|
||||
verificationStarted: true,
|
||||
keyReceived: false,
|
||||
sasMatched: make(chan bool, 1),
|
||||
hooks: hooks,
|
||||
chosenSASMethod: sasMethods[0],
|
||||
inRoomID: inRoomID,
|
||||
}
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
|
||||
_, loaded := mach.keyVerificationTransactionState.LoadOrStore(userID.String()+":"+transactionID, verState)
|
||||
if loaded {
|
||||
// transaction already exists
|
||||
mach.Log.Error("Transaction %v already exists, canceling", transactionID)
|
||||
if inRoomID == "" {
|
||||
_ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage)
|
||||
} else {
|
||||
_ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mach.timeoutAfter(verState, transactionID, timeout)
|
||||
|
||||
var err error
|
||||
if inRoomID == "" {
|
||||
err = mach.SendSASVerificationAccept(userID, content, verState.sas.GetPubkey(), sasMethods)
|
||||
} else {
|
||||
err = mach.SendInRoomSASVerificationAccept(inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods)
|
||||
}
|
||||
if err != nil {
|
||||
mach.Log.Error("Error accepting SAS verification: %v", err)
|
||||
}
|
||||
} else if resp == RejectRequest {
|
||||
mach.Log.Debug("Not accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
|
||||
var err error
|
||||
if inRoomID == "" {
|
||||
err = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
|
||||
} else {
|
||||
err = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
|
||||
}
|
||||
if err != nil {
|
||||
mach.Log.Error("Error canceling SAS verification: %v", err)
|
||||
}
|
||||
} else {
|
||||
mach.Log.Debug("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) timeoutAfter(verState *verificationState, transactionID string, timeout time.Duration) {
|
||||
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), timeout)
|
||||
verState.extendTimeout = timeoutCancel
|
||||
go func() {
|
||||
mapKey := verState.otherDevice.UserID.String() + ":" + transactionID
|
||||
for {
|
||||
<-timeoutCtx.Done()
|
||||
// when timeout context is done
|
||||
verState.lock.Lock()
|
||||
// if transaction not active anymore, return
|
||||
if _, ok := mach.keyVerificationTransactionState.Load(mapKey); !ok {
|
||||
verState.lock.Unlock()
|
||||
return
|
||||
}
|
||||
if timeoutCtx.Err() == context.DeadlineExceeded {
|
||||
// if deadline exceeded cancel due to timeout
|
||||
mach.keyVerificationTransactionState.Delete(mapKey)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Timed out", event.VerificationCancelByTimeout)
|
||||
mach.Log.Warn("Verification transaction %v is canceled due to timing out", transactionID)
|
||||
verState.lock.Unlock()
|
||||
return
|
||||
}
|
||||
// otherwise the cancel func was called, so the timeout is reset
|
||||
mach.Log.Debug("Extending timeout for transaction %v", transactionID)
|
||||
timeoutCtx, timeoutCancel = context.WithTimeout(context.Background(), timeout)
|
||||
verState.extendTimeout = timeoutCancel
|
||||
verState.lock.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// handleVerificationAccept handles an incoming m.key.verification.accept message.
|
||||
// It continues the SAS verification process by sending the SAS key message to the other device.
|
||||
func (mach *OlmMachine) handleVerificationAccept(userID id.UserID, content *event.VerificationAcceptEventContent, transactionID string) {
|
||||
mach.Log.Debug("Received verification accept for transaction %v", transactionID)
|
||||
verState, err := mach.getTransactionState(transactionID, userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error getting transaction state: %v", err)
|
||||
return
|
||||
}
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
verState.extendTimeout()
|
||||
|
||||
if !verState.initiatedByUs || verState.verificationStarted {
|
||||
// unexpected accept at this point
|
||||
mach.Log.Warn("Unexpected verification accept message for transaction %v", transactionID)
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected accept message", event.VerificationCancelUnexpectedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString)
|
||||
if content.KeyAgreementProtocol != event.KeyAgreementCurve25519HKDFSHA256 ||
|
||||
content.Hash != event.VerificationHashSHA256 ||
|
||||
content.MessageAuthenticationCode != event.HKDFHMACSHA256 ||
|
||||
len(sasMethods) == 0 {
|
||||
|
||||
mach.Log.Warn("Canceling verification transaction %v due to unknown parameter", transactionID)
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Verification uses unknown method", event.VerificationCancelUnknownMethod)
|
||||
return
|
||||
}
|
||||
|
||||
key := verState.sas.GetPubkey()
|
||||
verState.commitment = content.Commitment
|
||||
verState.chosenSASMethod = sasMethods[0]
|
||||
verState.verificationStarted = true
|
||||
|
||||
if verState.inRoomID == "" {
|
||||
err = mach.SendSASVerificationKey(userID, verState.otherDevice.DeviceID, transactionID, string(key))
|
||||
} else {
|
||||
err = mach.SendInRoomSASVerificationKey(verState.inRoomID, userID, transactionID, string(key))
|
||||
}
|
||||
if err != nil {
|
||||
mach.Log.Error("Error sending SAS key to other device: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleVerificationKey handles an incoming m.key.verification.key message.
|
||||
// It stores the other device's public key in order to acquire the SAS shared secret.
|
||||
func (mach *OlmMachine) handleVerificationKey(userID id.UserID, content *event.VerificationKeyEventContent, transactionID string) {
|
||||
mach.Log.Debug("Got verification key for transaction %v: %v", transactionID, content.Key)
|
||||
verState, err := mach.getTransactionState(transactionID, userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error getting transaction state: %v", err)
|
||||
return
|
||||
}
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
verState.extendTimeout()
|
||||
|
||||
device := verState.otherDevice
|
||||
|
||||
if !verState.verificationStarted || verState.keyReceived {
|
||||
// unexpected key at this point
|
||||
mach.Log.Warn("Unexpected verification key message for transaction %v", transactionID)
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected key message", event.VerificationCancelUnexpectedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
if err := verState.sas.SetTheirKey([]byte(content.Key)); err != nil {
|
||||
mach.Log.Error("Error setting other device's key: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
verState.keyReceived = true
|
||||
|
||||
if verState.initiatedByUs {
|
||||
// verify commitment string from accept message now
|
||||
expectedCommitment := olm.NewUtility().Sha256(content.Key + verState.startEventCanonical)
|
||||
mach.Log.Debug("Received commitment: %v Expected: %v", verState.commitment, expectedCommitment)
|
||||
if expectedCommitment != verState.commitment {
|
||||
mach.Log.Warn("Canceling verification transaction %v due to commitment mismatch", transactionID)
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Commitment mismatch", event.VerificationCancelCommitmentMismatch)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// if verification was initiated by other device, send out our key now
|
||||
key := verState.sas.GetPubkey()
|
||||
|
||||
if verState.inRoomID == "" {
|
||||
err = mach.SendSASVerificationKey(userID, device.DeviceID, transactionID, string(key))
|
||||
} else {
|
||||
err = mach.SendInRoomSASVerificationKey(verState.inRoomID, userID, transactionID, string(key))
|
||||
}
|
||||
if err != nil {
|
||||
mach.Log.Error("Error sending SAS key to other device: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// compare the SAS keys in a new goroutine and, when the verification is complete, send out the MAC
|
||||
var initUserID, acceptUserID id.UserID
|
||||
var initDeviceID, acceptDeviceID id.DeviceID
|
||||
var initKey, acceptKey string
|
||||
if verState.initiatedByUs {
|
||||
initUserID = mach.Client.UserID
|
||||
initDeviceID = mach.Client.DeviceID
|
||||
initKey = string(verState.sas.GetPubkey())
|
||||
acceptUserID = device.UserID
|
||||
acceptDeviceID = device.DeviceID
|
||||
acceptKey = content.Key
|
||||
} else {
|
||||
initUserID = device.UserID
|
||||
initDeviceID = device.DeviceID
|
||||
initKey = content.Key
|
||||
acceptUserID = mach.Client.UserID
|
||||
acceptDeviceID = mach.Client.DeviceID
|
||||
acceptKey = string(verState.sas.GetPubkey())
|
||||
}
|
||||
// use the prefered SAS method to generate a SAS
|
||||
sasMethod := verState.chosenSASMethod
|
||||
sas, err := sasMethod.GetVerificationSAS(initUserID, initDeviceID, initKey, acceptUserID, acceptDeviceID, acceptKey, transactionID, verState.sas)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error generating SAS (method %v): %v", sasMethod.Type(), err)
|
||||
return
|
||||
}
|
||||
mach.Log.Debug("Generated SAS (%v): %v", sasMethod.Type(), sas)
|
||||
go func() {
|
||||
result := verState.hooks.VerifySASMatch(device, sas)
|
||||
mach.sasCompared(result, transactionID, verState)
|
||||
}()
|
||||
}
|
||||
|
||||
// sasCompared is called asynchronously. It waits for the SAS to be compared for the verification to proceed.
|
||||
// If the SAS match, then our MAC is sent out. Otherwise the transaction is canceled.
|
||||
func (mach *OlmMachine) sasCompared(didMatch bool, transactionID string, verState *verificationState) {
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
verState.extendTimeout()
|
||||
if didMatch {
|
||||
verState.sasMatched <- true
|
||||
var err error
|
||||
if verState.inRoomID == "" {
|
||||
err = mach.SendSASVerificationMAC(verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas)
|
||||
} else {
|
||||
err = mach.SendInRoomSASVerificationMAC(verState.inRoomID, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas)
|
||||
}
|
||||
if err != nil {
|
||||
mach.Log.Error("Error sending verification MAC to other device: %v", err)
|
||||
}
|
||||
} else {
|
||||
verState.sasMatched <- false
|
||||
}
|
||||
}
|
||||
|
||||
// handleVerificationMAC handles an incoming m.key.verification.mac message.
|
||||
// It verifies the other device's MAC and if the MAC is valid it marks the device as trusted.
|
||||
func (mach *OlmMachine) handleVerificationMAC(userID id.UserID, content *event.VerificationMacEventContent, transactionID string) {
|
||||
mach.Log.Debug("Got MAC for verification %v: %v, MAC for keys: %v", transactionID, content.Mac, content.Keys)
|
||||
verState, err := mach.getTransactionState(transactionID, userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error getting transaction state: %v", err)
|
||||
return
|
||||
}
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
verState.extendTimeout()
|
||||
|
||||
device := verState.otherDevice
|
||||
|
||||
// we are done with this SAS verification in all cases so we forget about it
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
|
||||
if !verState.verificationStarted || !verState.keyReceived {
|
||||
// unexpected MAC at this point
|
||||
mach.Log.Warn("Unexpected MAC message for transaction %v", transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Unexpected MAC message", event.VerificationCancelUnexpectedMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// do this in another goroutine as the match result might take a long time to arrive
|
||||
go func() {
|
||||
matched := <-verState.sasMatched
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
|
||||
if !matched {
|
||||
mach.Log.Warn("SAS do not match! Canceling transaction %v", transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "SAS do not match", event.VerificationCancelSASMismatch)
|
||||
return
|
||||
}
|
||||
|
||||
keyID := id.NewKeyID(id.KeyAlgorithmEd25519, device.DeviceID.String())
|
||||
|
||||
expectedPKMAC, expectedKeysMAC, err := mach.getPKAndKeysMAC(verState.sas, device.UserID, device.DeviceID,
|
||||
mach.Client.UserID, mach.Client.DeviceID, transactionID, device.SigningKey, keyID, content.Mac)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error generating MAC to match with received MAC: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
mach.Log.Debug("Expected %s keys MAC, got %s", expectedKeysMAC, content.Keys)
|
||||
if content.Keys != expectedKeysMAC {
|
||||
mach.Log.Warn("Canceling verification transaction %v due to mismatched keys MAC", transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Mismatched keys MACs", event.VerificationCancelKeyMismatch)
|
||||
return
|
||||
}
|
||||
|
||||
mach.Log.Debug("Expected %s PK MAC, got %s", expectedPKMAC, content.Mac[keyID])
|
||||
if content.Mac[keyID] != expectedPKMAC {
|
||||
mach.Log.Warn("Canceling verification transaction %v due to mismatched PK MAC", transactionID)
|
||||
_ = mach.callbackAndCancelSASVerification(verState, transactionID, "Mismatched PK MACs", event.VerificationCancelKeyMismatch)
|
||||
return
|
||||
}
|
||||
|
||||
// we can finally trust this device
|
||||
device.Trust = id.TrustStateVerified
|
||||
err = mach.CryptoStore.PutDevice(device.UserID, device)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to put device after verifying: %v", err)
|
||||
}
|
||||
|
||||
if mach.CrossSigningKeys != nil {
|
||||
if device.UserID == mach.Client.UserID {
|
||||
err := mach.SignOwnDevice(device)
|
||||
if err != nil {
|
||||
mach.Log.Error("Failed to cross-sign own device %s: %v", device.DeviceID, err)
|
||||
} else {
|
||||
mach.Log.Debug("Cross-signed own device %v after SAS verification", device.DeviceID)
|
||||
}
|
||||
} else {
|
||||
masterKey, err := mach.fetchMasterKey(device, content, verState, transactionID)
|
||||
if err != nil {
|
||||
mach.Log.Warn("Failed to fetch %s's master key: %v", device.UserID, err)
|
||||
} else {
|
||||
if err := mach.SignUser(device.UserID, masterKey); err != nil {
|
||||
mach.Log.Error("Failed to cross-sign master key of %s: %v", device.UserID, err)
|
||||
} else {
|
||||
mach.Log.Debug("Cross-signed master key of %v after SAS verification", device.UserID)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO ask user to unlock cross-signing keys?
|
||||
mach.Log.Debug("Cross-signing keys not cached, not signing %s/%s", device.UserID, device.DeviceID)
|
||||
}
|
||||
|
||||
mach.Log.Debug("Device %v of user %v verified successfully!", device.DeviceID, device.UserID)
|
||||
|
||||
verState.hooks.OnSuccess()
|
||||
}()
|
||||
}
|
||||
|
||||
// handleVerificationCancel handles an incoming m.key.verification.cancel message.
|
||||
// It cancels the verification process for the given reason.
|
||||
func (mach *OlmMachine) handleVerificationCancel(userID id.UserID, content *event.VerificationCancelEventContent, transactionID string) {
|
||||
// make sure to not reply with a cancel to not cause a loop of cancel messages
|
||||
// this verification will get canceled even if the senders do not match
|
||||
verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID)
|
||||
if ok {
|
||||
go verStateInterface.(*verificationState).hooks.OnCancel(false, content.Reason, content.Code)
|
||||
}
|
||||
|
||||
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
mach.Log.Warn("SAS verification %v was canceled by %v with reason: %v (%v)",
|
||||
transactionID, userID, content.Reason, content.Code)
|
||||
}
|
||||
|
||||
// handleVerificationRequest handles an incoming m.key.verification.request message.
|
||||
func (mach *OlmMachine) handleVerificationRequest(userID id.UserID, content *event.VerificationRequestEventContent, transactionID string, inRoomID id.RoomID) {
|
||||
mach.Log.Debug("Received verification request from %v", content.FromDevice)
|
||||
otherDevice, err := mach.GetOrFetchDevice(userID, content.FromDevice)
|
||||
if err != nil {
|
||||
mach.Log.Error("Could not find device %v of user %v", content.FromDevice, userID)
|
||||
return
|
||||
}
|
||||
if !content.SupportsVerificationMethod(event.VerificationMethodSAS) {
|
||||
mach.Log.Warn("Canceling verification transaction %v as SAS is not supported", transactionID)
|
||||
if inRoomID == "" {
|
||||
_ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod)
|
||||
} else {
|
||||
_ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod)
|
||||
}
|
||||
return
|
||||
}
|
||||
resp, hooks := mach.AcceptVerificationFrom(transactionID, otherDevice, inRoomID)
|
||||
if resp == AcceptRequest {
|
||||
mach.Log.Debug("Accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
|
||||
if inRoomID == "" {
|
||||
_, err = mach.NewSASVerificationWith(otherDevice, hooks, transactionID, mach.DefaultSASTimeout)
|
||||
} else {
|
||||
if err := mach.SendInRoomSASVerificationReady(inRoomID, transactionID); err != nil {
|
||||
mach.Log.Error("Error sending in-room SAS verification ready: %v", err)
|
||||
}
|
||||
if mach.Client.UserID < otherDevice.UserID {
|
||||
// up to us to send the start message
|
||||
_, err = mach.newInRoomSASVerificationWithInner(inRoomID, otherDevice, hooks, transactionID, mach.DefaultSASTimeout)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
mach.Log.Error("Error accepting SAS verification request: %v", err)
|
||||
}
|
||||
} else if resp == RejectRequest {
|
||||
mach.Log.Debug("Rejecting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
|
||||
if inRoomID == "" {
|
||||
_ = mach.SendSASVerificationCancel(otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
|
||||
} else {
|
||||
_ = mach.SendInRoomSASVerificationCancel(inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
|
||||
}
|
||||
} else {
|
||||
mach.Log.Debug("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
// NewSimpleSASVerificationWith starts the SAS verification process with another device with a default timeout,
|
||||
// a generated transaction ID and support for both emoji and decimal SAS methods.
|
||||
func (mach *OlmMachine) NewSimpleSASVerificationWith(device *id.Device, hooks VerificationHooks) (string, error) {
|
||||
return mach.NewSASVerificationWith(device, hooks, "", mach.DefaultSASTimeout)
|
||||
}
|
||||
|
||||
// NewSASVerificationWith starts the SAS verification process with another device.
|
||||
// If the other device accepts the verification transaction, the methods in `hooks` will be used to verify the SAS match and to complete the transaction..
|
||||
// If the transaction ID is empty, a new one is generated.
|
||||
func (mach *OlmMachine) NewSASVerificationWith(device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) {
|
||||
if transactionID == "" {
|
||||
transactionID = strconv.Itoa(rand.Int())
|
||||
}
|
||||
mach.Log.Debug("Starting new verification transaction %v with device %v of user %v", transactionID, device.DeviceID, device.UserID)
|
||||
|
||||
verState := &verificationState{
|
||||
sas: olm.NewSAS(),
|
||||
otherDevice: device,
|
||||
initiatedByUs: true,
|
||||
verificationStarted: false,
|
||||
keyReceived: false,
|
||||
sasMatched: make(chan bool, 1),
|
||||
hooks: hooks,
|
||||
}
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
|
||||
startEvent, err := mach.SendSASVerificationStart(device.UserID, device.DeviceID, transactionID, hooks.VerificationMethods())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(startEvent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
canonical, err := canonicaljson.CanonicalJSON(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
verState.startEventCanonical = string(canonical)
|
||||
_, loaded := mach.keyVerificationTransactionState.LoadOrStore(device.UserID.String()+":"+transactionID, verState)
|
||||
if loaded {
|
||||
return "", ErrTransactionAlreadyExists
|
||||
}
|
||||
|
||||
mach.timeoutAfter(verState, transactionID, timeout)
|
||||
|
||||
return transactionID, nil
|
||||
}
|
||||
|
||||
// CancelSASVerification is used by the user to cancel a SAS verification process with the given reason.
|
||||
func (mach *OlmMachine) CancelSASVerification(userID id.UserID, transactionID, reason string) error {
|
||||
mapKey := userID.String() + ":" + transactionID
|
||||
verStateInterface, ok := mach.keyVerificationTransactionState.Load(mapKey)
|
||||
if !ok {
|
||||
return ErrUnknownTransaction
|
||||
}
|
||||
verState := verStateInterface.(*verificationState)
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
mach.Log.Trace("User canceled verification transaction %v with reason: %v", transactionID, reason)
|
||||
mach.keyVerificationTransactionState.Delete(mapKey)
|
||||
return mach.callbackAndCancelSASVerification(verState, transactionID, reason, event.VerificationCancelByUser)
|
||||
}
|
||||
|
||||
// SendSASVerificationCancel is used to manually send a SAS cancel message process with the given reason and cancellation code.
|
||||
func (mach *OlmMachine) SendSASVerificationCancel(userID id.UserID, deviceID id.DeviceID, transactionID string, reason string, code event.VerificationCancelCode) error {
|
||||
content := &event.VerificationCancelEventContent{
|
||||
TransactionID: transactionID,
|
||||
Reason: reason,
|
||||
Code: code,
|
||||
}
|
||||
return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationCancel, content)
|
||||
}
|
||||
|
||||
// SendSASVerificationStart is used to manually send the SAS verification start message to another device.
|
||||
func (mach *OlmMachine) SendSASVerificationStart(toUserID id.UserID, toDeviceID id.DeviceID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) {
|
||||
sasMethods := make([]event.SASMethod, len(methods))
|
||||
for i, method := range methods {
|
||||
sasMethods[i] = method.Type()
|
||||
}
|
||||
content := &event.VerificationStartEventContent{
|
||||
FromDevice: mach.Client.DeviceID,
|
||||
TransactionID: transactionID,
|
||||
Method: event.VerificationMethodSAS,
|
||||
KeyAgreementProtocols: []event.KeyAgreementProtocol{event.KeyAgreementCurve25519HKDFSHA256},
|
||||
Hashes: []event.VerificationHashMethod{event.VerificationHashSHA256},
|
||||
MessageAuthenticationCodes: []event.MACMethod{event.HKDFHMACSHA256},
|
||||
ShortAuthenticationString: sasMethods,
|
||||
}
|
||||
return content, mach.sendToOneDevice(toUserID, toDeviceID, event.ToDeviceVerificationStart, content)
|
||||
}
|
||||
|
||||
// SendSASVerificationAccept is used to manually send an accept for a SAS verification process from a received m.key.verification.start event.
|
||||
func (mach *OlmMachine) SendSASVerificationAccept(fromUser id.UserID, startEvent *event.VerificationStartEventContent, publicKey []byte, methods []VerificationMethod) error {
|
||||
if startEvent.Method != event.VerificationMethodSAS {
|
||||
reason := "Unknown verification method: " + string(startEvent.Method)
|
||||
if err := mach.SendSASVerificationCancel(fromUser, startEvent.FromDevice, startEvent.TransactionID, reason, event.VerificationCancelUnknownMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
return ErrUnknownVerificationMethod
|
||||
}
|
||||
payload, err := json.Marshal(startEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canonical, err := canonicaljson.CanonicalJSON(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash := olm.NewUtility().Sha256(string(publicKey) + string(canonical))
|
||||
sasMethods := make([]event.SASMethod, len(methods))
|
||||
for i, method := range methods {
|
||||
sasMethods[i] = method.Type()
|
||||
}
|
||||
content := &event.VerificationAcceptEventContent{
|
||||
TransactionID: startEvent.TransactionID,
|
||||
Method: event.VerificationMethodSAS,
|
||||
KeyAgreementProtocol: event.KeyAgreementCurve25519HKDFSHA256,
|
||||
Hash: event.VerificationHashSHA256,
|
||||
MessageAuthenticationCode: event.HKDFHMACSHA256,
|
||||
ShortAuthenticationString: sasMethods,
|
||||
Commitment: hash,
|
||||
}
|
||||
return mach.sendToOneDevice(fromUser, startEvent.FromDevice, event.ToDeviceVerificationAccept, content)
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) callbackAndCancelSASVerification(verState *verificationState, transactionID, reason string, code event.VerificationCancelCode) error {
|
||||
go verState.hooks.OnCancel(true, reason, code)
|
||||
return mach.SendSASVerificationCancel(verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, reason, code)
|
||||
}
|
||||
|
||||
// SendSASVerificationKey sends the ephemeral public key for a device to the partner device.
|
||||
func (mach *OlmMachine) SendSASVerificationKey(userID id.UserID, deviceID id.DeviceID, transactionID string, key string) error {
|
||||
content := &event.VerificationKeyEventContent{
|
||||
TransactionID: transactionID,
|
||||
Key: key,
|
||||
}
|
||||
return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationKey, content)
|
||||
}
|
||||
|
||||
// SendSASVerificationMAC is use the MAC of a device's key to the partner device.
|
||||
func (mach *OlmMachine) SendSASVerificationMAC(userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error {
|
||||
keyID := id.NewKeyID(id.KeyAlgorithmEd25519, mach.Client.DeviceID.String())
|
||||
|
||||
signingKey := mach.account.SigningKey()
|
||||
keyIDsMap := map[id.KeyID]string{keyID: ""}
|
||||
macMap := make(map[id.KeyID]string)
|
||||
|
||||
if mach.CrossSigningKeys != nil {
|
||||
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
|
||||
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String())
|
||||
// add master key ID to key map
|
||||
keyIDsMap[masterKeyID] = ""
|
||||
masterKeyMAC, _, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID,
|
||||
userID, deviceID, transactionID, masterKey, masterKeyID, keyIDsMap)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error generating master key MAC: %v", err)
|
||||
} else {
|
||||
mach.Log.Debug("Generated master key `%v` MAC: %v", masterKey, masterKeyMAC)
|
||||
macMap[masterKeyID] = masterKeyMAC
|
||||
}
|
||||
}
|
||||
|
||||
pubKeyMac, keysMac, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID, userID, deviceID, transactionID, signingKey, keyID, keyIDsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mach.Log.Debug("MAC of key %s is: %s", signingKey, pubKeyMac)
|
||||
mach.Log.Debug("MAC of key ID(s) %s is: %s", keyID, keysMac)
|
||||
macMap[keyID] = pubKeyMac
|
||||
|
||||
content := &event.VerificationMacEventContent{
|
||||
TransactionID: transactionID,
|
||||
Keys: keysMac,
|
||||
Mac: macMap,
|
||||
}
|
||||
|
||||
return mach.sendToOneDevice(userID, deviceID, event.ToDeviceVerificationMAC, content)
|
||||
}
|
||||
|
||||
func commonSASMethods(hooks VerificationHooks, otherDeviceMethods []event.SASMethod) []VerificationMethod {
|
||||
methods := make([]VerificationMethod, 0)
|
||||
for _, hookMethod := range hooks.VerificationMethods() {
|
||||
for _, otherMethod := range otherDeviceMethods {
|
||||
if hookMethod.Type() == otherMethod {
|
||||
methods = append(methods, hookMethod)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return methods
|
||||
}
|
||||
332
vendor/maunium.net/go/mautrix/crypto/verification_in_room.go
generated
vendored
Normal file
332
vendor/maunium.net/go/mautrix/crypto/verification_in_room.go
generated
vendored
Normal file
@@ -0,0 +1,332 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/canonicaljson"
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoVerificationFromDevice = errors.New("from_device field is empty")
|
||||
ErrNoVerificationMethods = errors.New("verification method list is empty")
|
||||
ErrNoRelatesTo = errors.New("missing m.relates_to info")
|
||||
)
|
||||
|
||||
// ProcessInRoomVerification is a callback that is to be called when a client receives a message
|
||||
// related to in-room verification.
|
||||
//
|
||||
// Currently this is not automatically called, so you must add the listener yourself.
|
||||
// Note that in-room verification events are wrapped in m.room.encrypted, but this expects the decrypted events.
|
||||
func (mach *OlmMachine) ProcessInRoomVerification(evt *event.Event) error {
|
||||
if evt.Sender == mach.Client.UserID {
|
||||
// nothing to do if the message is our own
|
||||
return nil
|
||||
}
|
||||
if relatable, ok := evt.Content.Parsed.(event.Relatable); !ok || relatable.OptionalGetRelatesTo() == nil {
|
||||
return ErrNoRelatesTo
|
||||
}
|
||||
|
||||
switch content := evt.Content.Parsed.(type) {
|
||||
case *event.MessageEventContent:
|
||||
if content.MsgType == event.MsgVerificationRequest {
|
||||
if content.FromDevice == "" {
|
||||
return ErrNoVerificationFromDevice
|
||||
}
|
||||
if content.Methods == nil {
|
||||
return ErrNoVerificationMethods
|
||||
}
|
||||
|
||||
newContent := &event.VerificationRequestEventContent{
|
||||
FromDevice: content.FromDevice,
|
||||
Methods: content.Methods,
|
||||
Timestamp: evt.Timestamp,
|
||||
TransactionID: evt.ID.String(),
|
||||
}
|
||||
mach.handleVerificationRequest(evt.Sender, newContent, evt.ID.String(), evt.RoomID)
|
||||
}
|
||||
case *event.VerificationStartEventContent:
|
||||
mach.handleVerificationStart(evt.Sender, content, content.RelatesTo.EventID.String(), 10*time.Minute, evt.RoomID)
|
||||
case *event.VerificationReadyEventContent:
|
||||
mach.handleInRoomVerificationReady(evt.Sender, evt.RoomID, content, content.RelatesTo.EventID.String())
|
||||
case *event.VerificationAcceptEventContent:
|
||||
mach.handleVerificationAccept(evt.Sender, content, content.RelatesTo.EventID.String())
|
||||
case *event.VerificationKeyEventContent:
|
||||
mach.handleVerificationKey(evt.Sender, content, content.RelatesTo.EventID.String())
|
||||
case *event.VerificationMacEventContent:
|
||||
mach.handleVerificationMAC(evt.Sender, content, content.RelatesTo.EventID.String())
|
||||
case *event.VerificationCancelEventContent:
|
||||
mach.handleVerificationCancel(evt.Sender, content, content.RelatesTo.EventID.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationCancel is used to manually send an in-room SAS cancel message process with the given reason and cancellation code.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationCancel(roomID id.RoomID, userID id.UserID, transactionID string, reason string, code event.VerificationCancelCode) error {
|
||||
content := &event.VerificationCancelEventContent{
|
||||
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
|
||||
Reason: reason,
|
||||
Code: code,
|
||||
To: userID,
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.InRoomVerificationCancel, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationRequest is used to manually send an in-room SAS verification request message to another user.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationRequest(roomID id.RoomID, toUserID id.UserID, methods []VerificationMethod) (string, error) {
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgVerificationRequest,
|
||||
FromDevice: mach.Client.DeviceID,
|
||||
Methods: []event.VerificationMethod{event.VerificationMethodSAS},
|
||||
To: toUserID,
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.EventMessage, content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.EventID.String(), nil
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationReady is used to manually send an in-room SAS verification ready message to another user.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationReady(roomID id.RoomID, transactionID string) error {
|
||||
content := &event.VerificationReadyEventContent{
|
||||
FromDevice: mach.Client.DeviceID,
|
||||
Methods: []event.VerificationMethod{event.VerificationMethodSAS},
|
||||
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.InRoomVerificationReady, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationStart is used to manually send the in-room SAS verification start message to another user.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationStart(roomID id.RoomID, toUserID id.UserID, transactionID string, methods []VerificationMethod) (*event.VerificationStartEventContent, error) {
|
||||
sasMethods := make([]event.SASMethod, len(methods))
|
||||
for i, method := range methods {
|
||||
sasMethods[i] = method.Type()
|
||||
}
|
||||
content := &event.VerificationStartEventContent{
|
||||
FromDevice: mach.Client.DeviceID,
|
||||
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
|
||||
Method: event.VerificationMethodSAS,
|
||||
KeyAgreementProtocols: []event.KeyAgreementProtocol{event.KeyAgreementCurve25519HKDFSHA256},
|
||||
Hashes: []event.VerificationHashMethod{event.VerificationHashSHA256},
|
||||
MessageAuthenticationCodes: []event.MACMethod{event.HKDFHMACSHA256},
|
||||
ShortAuthenticationString: sasMethods,
|
||||
To: toUserID,
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.InRoomVerificationStart, content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
return content, err
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationAccept is used to manually send an accept for an in-room SAS verification process from a received m.key.verification.start event.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationAccept(roomID id.RoomID, fromUser id.UserID, startEvent *event.VerificationStartEventContent, transactionID string, publicKey []byte, methods []VerificationMethod) error {
|
||||
if startEvent.Method != event.VerificationMethodSAS {
|
||||
reason := "Unknown verification method: " + string(startEvent.Method)
|
||||
if err := mach.SendInRoomSASVerificationCancel(roomID, fromUser, transactionID, reason, event.VerificationCancelUnknownMethod); err != nil {
|
||||
return err
|
||||
}
|
||||
return ErrUnknownVerificationMethod
|
||||
}
|
||||
payload, err := json.Marshal(startEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canonical, err := canonicaljson.CanonicalJSON(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash := olm.NewUtility().Sha256(string(publicKey) + string(canonical))
|
||||
sasMethods := make([]event.SASMethod, len(methods))
|
||||
for i, method := range methods {
|
||||
sasMethods[i] = method.Type()
|
||||
}
|
||||
content := &event.VerificationAcceptEventContent{
|
||||
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
|
||||
Method: event.VerificationMethodSAS,
|
||||
KeyAgreementProtocol: event.KeyAgreementCurve25519HKDFSHA256,
|
||||
Hash: event.VerificationHashSHA256,
|
||||
MessageAuthenticationCode: event.HKDFHMACSHA256,
|
||||
ShortAuthenticationString: sasMethods,
|
||||
Commitment: hash,
|
||||
To: fromUser,
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.InRoomVerificationAccept, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationKey sends the ephemeral public key for a device to the partner device for an in-room verification.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationKey(roomID id.RoomID, userID id.UserID, transactionID string, key string) error {
|
||||
content := &event.VerificationKeyEventContent{
|
||||
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
|
||||
Key: key,
|
||||
To: userID,
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.InRoomVerificationKey, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
return err
|
||||
}
|
||||
|
||||
// SendInRoomSASVerificationMAC sends the MAC of a device's key to the partner device for an in-room verification.
|
||||
func (mach *OlmMachine) SendInRoomSASVerificationMAC(roomID id.RoomID, userID id.UserID, deviceID id.DeviceID, transactionID string, sas *olm.SAS) error {
|
||||
keyID := id.NewKeyID(id.KeyAlgorithmEd25519, mach.Client.DeviceID.String())
|
||||
|
||||
signingKey := mach.account.SigningKey()
|
||||
keyIDsMap := map[id.KeyID]string{keyID: ""}
|
||||
macMap := make(map[id.KeyID]string)
|
||||
|
||||
if mach.CrossSigningKeys != nil {
|
||||
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
|
||||
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String())
|
||||
// add master key ID to key map
|
||||
keyIDsMap[masterKeyID] = ""
|
||||
masterKeyMAC, _, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID,
|
||||
userID, deviceID, transactionID, masterKey, masterKeyID, keyIDsMap)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error generating master key MAC: %v", err)
|
||||
} else {
|
||||
mach.Log.Debug("Generated master key `%v` MAC: %v", masterKey, masterKeyMAC)
|
||||
macMap[masterKeyID] = masterKeyMAC
|
||||
}
|
||||
}
|
||||
|
||||
pubKeyMac, keysMac, err := mach.getPKAndKeysMAC(sas, mach.Client.UserID, mach.Client.DeviceID, userID, deviceID, transactionID, signingKey, keyID, keyIDsMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mach.Log.Debug("MAC of key %s is: %s", signingKey, pubKeyMac)
|
||||
mach.Log.Debug("MAC of key ID(s) %s is: %s", keyID, keysMac)
|
||||
macMap[keyID] = pubKeyMac
|
||||
|
||||
content := &event.VerificationMacEventContent{
|
||||
RelatesTo: &event.RelatesTo{Type: event.RelReference, EventID: id.EventID(transactionID)},
|
||||
Keys: keysMac,
|
||||
Mac: macMap,
|
||||
To: userID,
|
||||
}
|
||||
|
||||
encrypted, err := mach.EncryptMegolmEvent(roomID, event.InRoomVerificationMAC, content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = mach.Client.SendMessageEvent(roomID, event.EventEncrypted, encrypted)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewInRoomSASVerificationWith starts the in-room SAS verification process with another user in the given room.
|
||||
// It returns the generated transaction ID.
|
||||
func (mach *OlmMachine) NewInRoomSASVerificationWith(inRoomID id.RoomID, userID id.UserID, hooks VerificationHooks, timeout time.Duration) (string, error) {
|
||||
return mach.newInRoomSASVerificationWithInner(inRoomID, &id.Device{UserID: userID}, hooks, "", timeout)
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) newInRoomSASVerificationWithInner(inRoomID id.RoomID, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) {
|
||||
mach.Log.Debug("Starting new in-room verification transaction user %v", device.UserID)
|
||||
|
||||
request := transactionID == ""
|
||||
if request {
|
||||
var err error
|
||||
// get new transaction ID from the request message event ID
|
||||
transactionID, err = mach.SendInRoomSASVerificationRequest(inRoomID, device.UserID, hooks.VerificationMethods())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
verState := &verificationState{
|
||||
sas: olm.NewSAS(),
|
||||
otherDevice: device,
|
||||
initiatedByUs: true,
|
||||
verificationStarted: false,
|
||||
keyReceived: false,
|
||||
sasMatched: make(chan bool, 1),
|
||||
hooks: hooks,
|
||||
inRoomID: inRoomID,
|
||||
}
|
||||
verState.lock.Lock()
|
||||
defer verState.lock.Unlock()
|
||||
|
||||
if !request {
|
||||
// start in-room verification
|
||||
startEvent, err := mach.SendInRoomSASVerificationStart(inRoomID, device.UserID, transactionID, hooks.VerificationMethods())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(startEvent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
canonical, err := canonicaljson.CanonicalJSON(payload)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
verState.startEventCanonical = string(canonical)
|
||||
}
|
||||
|
||||
mach.keyVerificationTransactionState.Store(device.UserID.String()+":"+transactionID, verState)
|
||||
|
||||
mach.timeoutAfter(verState, transactionID, timeout)
|
||||
|
||||
return transactionID, nil
|
||||
}
|
||||
|
||||
func (mach *OlmMachine) handleInRoomVerificationReady(userID id.UserID, roomID id.RoomID, content *event.VerificationReadyEventContent, transactionID string) {
|
||||
device, err := mach.GetOrFetchDevice(userID, content.FromDevice)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error fetching device %v of user %v: %v", content.FromDevice, userID, err)
|
||||
return
|
||||
}
|
||||
|
||||
verState, err := mach.getTransactionState(transactionID, userID)
|
||||
if err != nil {
|
||||
mach.Log.Error("Error getting transaction state: %v", err)
|
||||
return
|
||||
}
|
||||
//mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
|
||||
|
||||
if mach.Client.UserID < userID {
|
||||
// up to us to send the start message
|
||||
verState.lock.Lock()
|
||||
mach.newInRoomSASVerificationWithInner(roomID, device, verState.hooks, transactionID, 10*time.Minute)
|
||||
verState.lock.Unlock()
|
||||
}
|
||||
}
|
||||
204
vendor/maunium.net/go/mautrix/crypto/verification_sas_methods.go
generated
vendored
Normal file
204
vendor/maunium.net/go/mautrix/crypto/verification_sas_methods.go
generated
vendored
Normal file
@@ -0,0 +1,204 @@
|
||||
// Copyright (c) 2020 Nikos Filippakis
|
||||
//
|
||||
// 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/.
|
||||
|
||||
//go:build !nosas
|
||||
// +build !nosas
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"maunium.net/go/mautrix/crypto/olm"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// SASData contains the data that users need to verify.
|
||||
type SASData interface {
|
||||
Type() event.SASMethod
|
||||
}
|
||||
|
||||
// VerificationMethod describes a method for generating a SAS.
|
||||
type VerificationMethod interface {
|
||||
// GetVerificationSAS uses the user, device ID and key of the user who initiated the verification transaction,
|
||||
// the user, device ID and key of the user who accepted, the transaction ID and the SAS object to generate a SAS.
|
||||
// The SAS can be any type, such as an array of numbers or emojis.
|
||||
GetVerificationSAS(initUserID id.UserID, initDeviceID id.DeviceID, initKey string,
|
||||
acceptUserID id.UserID, acceptDeviceID id.DeviceID, acceptKey string,
|
||||
transactionID string, sas *olm.SAS) (SASData, error)
|
||||
// Type returns the type of this SAS method
|
||||
Type() event.SASMethod
|
||||
}
|
||||
|
||||
const sasInfoFormat = "MATRIX_KEY_VERIFICATION_SAS|%s|%s|%s|%s|%s|%s|%s"
|
||||
|
||||
// VerificationMethodDecimal describes the decimal SAS method.
|
||||
type VerificationMethodDecimal struct{}
|
||||
|
||||
// DecimalSASData contains the verification numbers for the decimal SAS method.
|
||||
type DecimalSASData [3]uint
|
||||
|
||||
// Type returns the decimal SAS method type.
|
||||
func (DecimalSASData) Type() event.SASMethod {
|
||||
return event.SASDecimal
|
||||
}
|
||||
|
||||
// GetVerificationSAS generates the three numbers that need to match with the other device for a verification to be valid.
|
||||
func (VerificationMethodDecimal) GetVerificationSAS(initUserID id.UserID, initDeviceID id.DeviceID, initKey string,
|
||||
acceptUserID id.UserID, acceptDeviceID id.DeviceID, acceptKey string,
|
||||
transactionID string, sas *olm.SAS) (SASData, error) {
|
||||
|
||||
sasInfo := fmt.Sprintf(sasInfoFormat,
|
||||
initUserID, initDeviceID, initKey,
|
||||
acceptUserID, acceptDeviceID, acceptKey,
|
||||
transactionID)
|
||||
|
||||
sasBytes, err := sas.GenerateBytes([]byte(sasInfo), 5)
|
||||
if err != nil {
|
||||
return DecimalSASData{0, 0, 0}, err
|
||||
}
|
||||
|
||||
numbers := DecimalSASData{
|
||||
(uint(sasBytes[0])<<5 | uint(sasBytes[1])>>3) + 1000,
|
||||
(uint(sasBytes[1]&0x7)<<10 | uint(sasBytes[2])<<2 | uint(sasBytes[3]>>6)) + 1000,
|
||||
(uint(sasBytes[3]&0x3F)<<7 | uint(sasBytes[4])>>1) + 1000,
|
||||
}
|
||||
|
||||
return numbers, nil
|
||||
}
|
||||
|
||||
// Type returns the decimal SAS method type.
|
||||
func (VerificationMethodDecimal) Type() event.SASMethod {
|
||||
return event.SASDecimal
|
||||
}
|
||||
|
||||
var allEmojis = [...]VerificationEmoji{
|
||||
{'🐶', "Dog"},
|
||||
{'🐱', "Cat"},
|
||||
{'🦁', "Lion"},
|
||||
{'🐎', "Horse"},
|
||||
{'🦄', "Unicorn"},
|
||||
{'🐷', "Pig"},
|
||||
{'🐘', "Elephant"},
|
||||
{'🐰', "Rabbit"},
|
||||
{'🐼', "Panda"},
|
||||
{'🐓', "Rooster"},
|
||||
{'🐧', "Penguin"},
|
||||
{'🐢', "Turtle"},
|
||||
{'🐟', "Fish"},
|
||||
{'🐙', "Octopus"},
|
||||
{'🦋', "Butterfly"},
|
||||
{'🌷', "Flower"},
|
||||
{'🌳', "Tree"},
|
||||
{'🌵', "Cactus"},
|
||||
{'🍄', "Mushroom"},
|
||||
{'🌏', "Globe"},
|
||||
{'🌙', "Moon"},
|
||||
{'☁', "Cloud"},
|
||||
{'🔥', "Fire"},
|
||||
{'🍌', "Banana"},
|
||||
{'🍎', "Apple"},
|
||||
{'🍓', "Strawberry"},
|
||||
{'🌽', "Corn"},
|
||||
{'🍕', "Pizza"},
|
||||
{'🎂', "Cake"},
|
||||
{'❤', "Heart"},
|
||||
{'😀', "Smiley"},
|
||||
{'🤖', "Robot"},
|
||||
{'🎩', "Hat"},
|
||||
{'👓', "Glasses"},
|
||||
{'🔧', "Spanner"},
|
||||
{'🎅', "Santa"},
|
||||
{'👍', "Thumbs Up"},
|
||||
{'☂', "Umbrella"},
|
||||
{'⌛', "Hourglass"},
|
||||
{'⏰', "Clock"},
|
||||
{'🎁', "Gift"},
|
||||
{'💡', "Light Bulb"},
|
||||
{'📕', "Book"},
|
||||
{'✏', "Pencil"},
|
||||
{'📎', "Paperclip"},
|
||||
{'✂', "Scissors"},
|
||||
{'🔒', "Lock"},
|
||||
{'🔑', "Key"},
|
||||
{'🔨', "Hammer"},
|
||||
{'☎', "Telephone"},
|
||||
{'🏁', "Flag"},
|
||||
{'🚂', "Train"},
|
||||
{'🚲', "Bicycle"},
|
||||
{'✈', "Aeroplane"},
|
||||
{'🚀', "Rocket"},
|
||||
{'🏆', "Trophy"},
|
||||
{'⚽', "Ball"},
|
||||
{'🎸', "Guitar"},
|
||||
{'🎺', "Trumpet"},
|
||||
{'🔔', "Bell"},
|
||||
{'⚓', "Anchor"},
|
||||
{'🎧', "Headphones"},
|
||||
{'📁', "Folder"},
|
||||
{'📌', "Pin"},
|
||||
}
|
||||
|
||||
// VerificationEmoji describes an emoji that might be sent for verifying devices.
|
||||
type VerificationEmoji struct {
|
||||
Emoji rune
|
||||
Description string
|
||||
}
|
||||
|
||||
func (vm VerificationEmoji) GetEmoji() rune {
|
||||
return vm.Emoji
|
||||
}
|
||||
|
||||
func (vm VerificationEmoji) GetDescription() string {
|
||||
return vm.Description
|
||||
}
|
||||
|
||||
// EmojiSASData contains the verification emojis for the emoji SAS method.
|
||||
type EmojiSASData [7]VerificationEmoji
|
||||
|
||||
// Type returns the emoji SAS method type.
|
||||
func (EmojiSASData) Type() event.SASMethod {
|
||||
return event.SASEmoji
|
||||
}
|
||||
|
||||
// VerificationMethodEmoji describes the emoji SAS method.
|
||||
type VerificationMethodEmoji struct{}
|
||||
|
||||
// GetVerificationSAS generates the three numbers that need to match with the other device for a verification to be valid.
|
||||
func (VerificationMethodEmoji) GetVerificationSAS(initUserID id.UserID, initDeviceID id.DeviceID, initKey string,
|
||||
acceptUserID id.UserID, acceptDeviceID id.DeviceID, acceptKey string,
|
||||
transactionID string, sas *olm.SAS) (SASData, error) {
|
||||
|
||||
sasInfo := fmt.Sprintf(sasInfoFormat,
|
||||
initUserID, initDeviceID, initKey,
|
||||
acceptUserID, acceptDeviceID, acceptKey,
|
||||
transactionID)
|
||||
|
||||
var emojis EmojiSASData
|
||||
sasBytes, err := sas.GenerateBytes([]byte(sasInfo), 6)
|
||||
|
||||
if err != nil {
|
||||
return emojis, err
|
||||
}
|
||||
|
||||
sasNum := uint64(sasBytes[0])<<40 | uint64(sasBytes[1])<<32 | uint64(sasBytes[2])<<24 |
|
||||
uint64(sasBytes[3])<<16 | uint64(sasBytes[4])<<8 | uint64(sasBytes[5])
|
||||
|
||||
for i := 0; i < len(emojis); i++ {
|
||||
// take nth group of 6 bits
|
||||
emojiIdx := (sasNum >> uint(48-(i+1)*6)) & 0x3F
|
||||
emoji := allEmojis[emojiIdx]
|
||||
emojis[i] = emoji
|
||||
}
|
||||
|
||||
return emojis, nil
|
||||
}
|
||||
|
||||
// Type returns the emoji SAS method type.
|
||||
func (VerificationMethodEmoji) Type() event.SASMethod {
|
||||
return event.SASEmoji
|
||||
}
|
||||
Reference in New Issue
Block a user