add vendoring

This commit is contained in:
Aine
2022-11-16 12:08:51 +02:00
parent 14751cbf3a
commit c1d33fe3cb
1104 changed files with 759066 additions and 0 deletions

92
vendor/maunium.net/go/mautrix/crypto/account.go generated vendored Normal file
View 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
}

View 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,
}
}

View 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

View 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.

View 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
View 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
}

View 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
}

View 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
View 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
}

View 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)
}
}
}
}
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
}

View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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
}

View 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)
);

View 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)
)

View 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;

View 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;

View 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

View 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;

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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()
}
}

View 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
}