updated deps; updated healthchecks.io integration

This commit is contained in:
Aine
2024-04-07 14:42:12 +03:00
parent 271a4a0e31
commit 15d61f174e
122 changed files with 3432 additions and 4613 deletions

View File

@@ -9,14 +9,16 @@ package crypto
import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
type OlmAccount struct {
Internal olm.Account
signingKey id.SigningKey
identityKey id.IdentityKey
Shared bool
Internal olm.Account
signingKey id.SigningKey
identityKey id.IdentityKey
Shared bool
KeyBackupVersion id.KeyBackupVersion
}
func NewOlmAccount() *OlmAccount {
@@ -62,11 +64,7 @@ func (account *OlmAccount) getInitialKeys(userID id.UserID, deviceID id.DeviceID
panic(err)
}
deviceKeys.Signatures = mautrix.Signatures{
userID: {
id.NewKeyID(id.KeyAlgorithmEd25519, deviceID.String()): signature,
},
}
deviceKeys.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
return deviceKeys
}
@@ -79,11 +77,7 @@ func (account *OlmAccount) getOneTimeKeys(userID id.UserID, deviceID id.DeviceID
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.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
key.IsSigned = true
oneTimeKeys[id.NewKeyID(id.KeyAlgorithmSignedCurve25519, keyID)] = key
}

60
vendor/maunium.net/go/mautrix/crypto/aescbc/aes_cbc.go generated vendored Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 aescbc
import (
"crypto/aes"
"crypto/cipher"
"maunium.net/go/mautrix/crypto/pkcs7"
)
// Encrypt encrypts the plaintext with the key and IV. The IV length must be
// equal to the AES block size.
//
// This function might mutate the plaintext.
func Encrypt(key, iv, plaintext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, ErrNoKeyProvided
}
if len(iv) != aes.BlockSize {
return nil, ErrIVNotBlockSize
}
plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cipher.NewCBCEncrypter(block, iv).CryptBlocks(plaintext, plaintext)
return plaintext, nil
}
// Decrypt decrypts the ciphertext with the key and IV. The IV length must be
// equal to the block size.
//
// This function mutates the ciphertext.
func Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, ErrNoKeyProvided
}
if len(iv) != aes.BlockSize {
return nil, ErrIVNotBlockSize
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < aes.BlockSize {
return nil, ErrNotMultipleBlockSize
}
cipher.NewCBCDecrypter(block, iv).CryptBlocks(ciphertext, ciphertext)
return pkcs7.Unpad(ciphertext), nil
}

15
vendor/maunium.net/go/mautrix/crypto/aescbc/errors.go generated vendored Normal file
View File

@@ -0,0 +1,15 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 aescbc
import "errors"
var (
ErrNoKeyProvided = errors.New("no key")
ErrIVNotBlockSize = errors.New("IV length does not match AES block size")
ErrNotMultipleBlockSize = errors.New("ciphertext length is not a multiple of the AES block size")
)

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 backup
import (
"bytes"
"crypto/ecdh"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"go.mau.fi/util/jsonbytes"
"golang.org/x/crypto/hkdf"
"maunium.net/go/mautrix/crypto/aescbc"
)
var ErrInvalidMAC = errors.New("invalid MAC")
// EncryptedSessionData is the encrypted session_data field of a key backup as
// defined in [Section 11.12.3.2.2 of the Spec].
//
// The type parameter T represents the format of the session data contained in
// the encrypted payload.
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type EncryptedSessionData[T any] struct {
Ciphertext jsonbytes.UnpaddedBytes `json:"ciphertext"`
Ephemeral EphemeralKey `json:"ephemeral"`
MAC jsonbytes.UnpaddedBytes `json:"mac"`
}
func calculateEncryptionParameters(sharedSecret []byte) (key, macKey, iv []byte, err error) {
hkdfReader := hkdf.New(sha256.New, sharedSecret, nil, nil)
encryptionParams := make([]byte, 80)
_, err = hkdfReader.Read(encryptionParams)
if err != nil {
return nil, nil, nil, err
}
return encryptionParams[:32], encryptionParams[32:64], encryptionParams[64:], nil
}
// calculateCompatMAC calculates the MAC for compatibility with Olm and
// Vodozemac which do not actually write the ciphertext when computing the MAC.
//
// Deprecated: Use [calculateMAC] instead.
func calculateCompatMAC(macKey []byte) []byte {
hash := hmac.New(sha256.New, macKey)
return hash.Sum(nil)[:8]
}
// calculateMAC calculates the MAC as described in step 5 of according to
// [Section 11.12.3.2.2] of the Spec.
//
// [Section 11.12.3.2.2]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func calculateMAC(macKey, ciphertext []byte) []byte {
hash := hmac.New(sha256.New, macKey)
_, err := hash.Write(ciphertext)
if err != nil {
panic(err)
}
return hash.Sum(nil)[:8]
}
// EncryptSessionData encrypts the given session data with the given recovery
// key as defined in [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func EncryptSessionData[T any](backupKey *MegolmBackupKey, sessionData T) (*EncryptedSessionData[T], error) {
sessionJSON, err := json.Marshal(sessionData)
if err != nil {
return nil, err
}
ephemeralKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
sharedSecret, err := ephemeralKey.ECDH(backupKey.PublicKey())
if err != nil {
return nil, err
}
key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
if err != nil {
return nil, err
}
ciphertext, err := aescbc.Encrypt(key, iv, sessionJSON)
if err != nil {
return nil, err
}
return &EncryptedSessionData[T]{
Ciphertext: ciphertext,
Ephemeral: EphemeralKey{ephemeralKey.PublicKey()},
MAC: calculateCompatMAC(macKey),
}, nil
}
// Decrypt decrypts the [EncryptedSessionData] into a *T using the recovery key
// by reversing the process described in [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
func (esd *EncryptedSessionData[T]) Decrypt(backupKey *MegolmBackupKey) (*T, error) {
sharedSecret, err := backupKey.ECDH(esd.Ephemeral.PublicKey)
if err != nil {
return nil, err
}
key, macKey, iv, err := calculateEncryptionParameters(sharedSecret)
if err != nil {
return nil, err
}
// Verify the MAC before decrypting.
if !bytes.Equal(calculateCompatMAC(macKey), esd.MAC) {
return nil, ErrInvalidMAC
}
plaintext, err := aescbc.Decrypt(key, iv, esd.Ciphertext)
if err != nil {
return nil, err
}
var sessionData T
err = json.Unmarshal(plaintext, &sessionData)
return &sessionData, err
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 backup
import (
"crypto/ecdh"
"encoding/base64"
"encoding/json"
)
// EphemeralKey is a wrapper around an ECDH X25519 public key that implements
// JSON marshalling and unmarshalling.
type EphemeralKey struct {
*ecdh.PublicKey
}
func (k *EphemeralKey) MarshalJSON() ([]byte, error) {
if k == nil || k.PublicKey == nil {
return json.Marshal(nil)
}
return json.Marshal(base64.RawStdEncoding.EncodeToString(k.Bytes()))
}
func (k *EphemeralKey) UnmarshalJSON(data []byte) error {
var keyStr string
err := json.Unmarshal(data, &keyStr)
if err != nil {
return err
}
keyBytes, err := base64.RawStdEncoding.DecodeString(keyStr)
if err != nil {
return err
}
k.PublicKey, err = ecdh.X25519().NewPublicKey(keyBytes)
return err
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 backup
import (
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
// MegolmAuthData is the auth_data when the key backup is created with
// the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in
// [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type MegolmAuthData struct {
PublicKey id.Ed25519 `json:"public_key"`
Signatures signatures.Signatures `json:"signatures"`
}
type SenderClaimedKeys struct {
Ed25519 id.Ed25519 `json:"ed25519"`
}
// MegolmSessionData is the decrypted session_data when the key backup is created
// with the [id.KeyBackupAlgorithmMegolmBackupV1] algorithm as defined in
// [Section 11.12.3.2.2 of the Spec].
//
// [Section 11.12.3.2.2 of the Spec]: https://spec.matrix.org/v1.9/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
type MegolmSessionData struct {
Algorithm id.Algorithm `json:"algorithm"`
ForwardingKeyChain []string `json:"forwarding_curve25519_key_chain"`
SenderClaimedKeys SenderClaimedKeys `json:"sender_claimed_keys"`
SenderKey id.SenderKey `json:"sender_key"`
SessionKey string `json:"session_key"`
}

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 backup
import (
"crypto/ecdh"
"crypto/rand"
)
// MegolmBackupKey is a wrapper around an ECDH X25519 private key that is used
// to decrypt a megolm key backup.
type MegolmBackupKey struct {
*ecdh.PrivateKey
}
func NewMegolmBackupKey() (*MegolmBackupKey, error) {
key, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, err
}
return &MegolmBackupKey{key}, nil
}
func MegolmBackupKeyFromBytes(bytes []byte) (*MegolmBackupKey, error) {
key, err := ecdh.X25519().NewPrivateKey(bytes)
if err != nil {
return nil, err
}
return &MegolmBackupKey{key}, nil
}

View File

@@ -13,21 +13,22 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"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
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,
MasterKey: cskc.MasterKey.PublicKey(),
SelfSigningKey: cskc.SelfSigningKey.PublicKey(),
UserSigningKey: cskc.UserSigningKey.PublicKey(),
}
}
@@ -39,28 +40,28 @@ type CrossSigningSeeds struct {
func (mach *OlmMachine) ExportCrossSigningKeys() CrossSigningSeeds {
return CrossSigningSeeds{
MasterKey: mach.CrossSigningKeys.MasterKey.Seed,
SelfSigningKey: mach.CrossSigningKeys.SelfSigningKey.Seed,
UserSigningKey: mach.CrossSigningKeys.UserSigningKey.Seed,
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 {
if keysCache.MasterKey, err = olm.NewPKSigningFromSeed(keys.MasterKey); err != nil {
return
}
if keysCache.SelfSigningKey, err = olm.NewPkSigningFromSeed(keys.SelfSigningKey); err != nil {
if keysCache.SelfSigningKey, err = olm.NewPKSigningFromSeed(keys.SelfSigningKey); err != nil {
return
}
if keysCache.UserSigningKey, err = olm.NewPkSigningFromSeed(keys.UserSigningKey); err != nil {
if keysCache.UserSigningKey, err = olm.NewPKSigningFromSeed(keys.UserSigningKey); err != nil {
return
}
mach.Log.Debug().
Str("master", keysCache.MasterKey.PublicKey.String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey.String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey.String()).
Str("master", keysCache.MasterKey.PublicKey().String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey().String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey().String()).
Msg("Imported own cross-signing keys")
mach.CrossSigningKeys = &keysCache
@@ -72,19 +73,19 @@ func (mach *OlmMachine) ImportCrossSigningKeys(keys CrossSigningSeeds) (err erro
func (mach *OlmMachine) GenerateCrossSigningKeys() (*CrossSigningKeysCache, error) {
var keysCache CrossSigningKeysCache
var err error
if keysCache.MasterKey, err = olm.NewPkSigning(); err != nil {
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 {
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 {
if keysCache.UserSigningKey, err = olm.NewPKSigning(); err != nil {
return nil, fmt.Errorf("failed to generate user-signing key: %w", err)
}
mach.Log.Debug().
Str("master", keysCache.MasterKey.PublicKey.String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey.String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey.String()).
Str("master", keysCache.MasterKey.PublicKey().String()).
Str("self_signing", keysCache.SelfSigningKey.PublicKey().String()).
Str("user_signing", keysCache.UserSigningKey.PublicKey().String()).
Msg("Generated cross-signing keys")
return &keysCache, nil
}
@@ -92,48 +93,45 @@ func (mach *OlmMachine) GenerateCrossSigningKeys() (*CrossSigningKeysCache, erro
// PublishCrossSigningKeys signs and uploads the public keys of the given cross-signing keys to the server.
func (mach *OlmMachine) PublishCrossSigningKeys(ctx context.Context, keys *CrossSigningKeysCache, uiaCallback mautrix.UIACallback) error {
userID := mach.Client.UserID
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey.String())
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,
masterKeyID: keys.MasterKey.PublicKey(),
},
}
masterSig, err := mach.account.Internal.SignJSON(masterKey)
if err != nil {
return fmt.Errorf("failed to sign master key: %w", err)
}
masterKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, mach.Client.DeviceID.String(), masterSig)
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,
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,
},
}
selfKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey().String(), 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,
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,
},
}
userKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey().String(), userSig)
err = mach.Client.UploadCrossSigningKeys(ctx, &mautrix.UploadCrossSigningKeysReq{
Master: masterKey,

View File

@@ -14,7 +14,7 @@ import (
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
@@ -34,31 +34,6 @@ var (
ErrMismatchingMasterKeyMAC = errors.New("mismatching cross-signing master key MAC")
)
func (mach *OlmMachine) fetchMasterKey(ctx context.Context, device *id.Device, content *event.VerificationMacEventContent, verState *verificationState, transactionID string) (id.Ed25519, error) {
crossSignKeys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, 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(ctx context.Context, userID id.UserID, masterKey id.Ed25519) error {
if userID == mach.Client.UserID {
@@ -85,7 +60,7 @@ func (mach *OlmMachine) SignUser(ctx context.Context, userID id.UserID, masterKe
Str("signature", signature).
Msg("Signed master key of user with our user-signing key")
if err := mach.CryptoStore.PutSignature(ctx, userID, masterKey, mach.Client.UserID, mach.CrossSigningKeys.UserSigningKey.PublicKey, signature); err != nil {
if err := mach.CryptoStore.PutSignature(ctx, userID, masterKey, mach.Client.UserID, mach.CrossSigningKeys.UserSigningKey.PublicKey(), signature); err != nil {
return fmt.Errorf("error storing signature in crypto store: %w", err)
}
@@ -102,7 +77,7 @@ func (mach *OlmMachine) SignOwnMasterKey(ctx context.Context) error {
userID := mach.Client.UserID
deviceID := mach.Client.DeviceID
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey
masterKey := mach.CrossSigningKeys.MasterKey.PublicKey()
masterKeyObj := mautrix.ReqKeysSignatures{
UserID: userID,
@@ -115,11 +90,7 @@ func (mach *OlmMachine) SignOwnMasterKey(ctx context.Context) error {
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,
},
}
masterKeyObj.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, deviceID.String(), signature)
mach.Log.Debug().
Str("device_id", deviceID.String()).
Str("signature", signature).
@@ -178,7 +149,7 @@ func (mach *OlmMachine) SignOwnDevice(ctx context.Context, device *id.Device) er
Str("signature", signature).
Msg("Signed own device key with self-signing key")
if err := mach.CryptoStore.PutSignature(ctx, device.UserID, device.SigningKey, mach.Client.UserID, mach.CrossSigningKeys.SelfSigningKey.PublicKey, signature); err != nil {
if err := mach.CryptoStore.PutSignature(ctx, 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)
}
@@ -209,16 +180,12 @@ func (mach *OlmMachine) getFullDeviceKeys(ctx context.Context, device *id.Device
}
// signAndUpload signs the given key signatures object and uploads it to the server.
func (mach *OlmMachine) signAndUpload(ctx context.Context, req mautrix.ReqKeysSignatures, userID id.UserID, signedThing string, key *olm.PkSigning) (string, error) {
func (mach *OlmMachine) signAndUpload(ctx context.Context, 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,
},
}
req.Signatures = signatures.NewSingleSignature(mach.Client.UserID, id.KeyAlgorithmEd25519, key.PublicKey().String(), signature)
resp, err := mach.Client.UploadSignatures(ctx, &mautrix.ReqUploadSignatures{
userID: map[string]mautrix.ReqKeysSignatures{

View File

@@ -14,6 +14,7 @@ import (
"maunium.net/go/mautrix/crypto/ssss"
"maunium.net/go/mautrix/crypto/utils"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// FetchCrossSigningKeysFromSSSS fetches all the cross-signing keys from SSSS, decrypts them using the given key and stores them in the olm machine.
@@ -57,33 +58,8 @@ func (mach *OlmMachine) retrieveDecryptXSigningKey(ctx context.Context, keyName
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(ctx context.Context, userPassword, passphrase string) (string, error) {
key, err := mach.SSSS.GenerateAndUploadKey(ctx, 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(ctx, 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(ctx, keysCache, func(uiResp *mautrix.RespUserInteractive) interface{} {
func (mach *OlmMachine) GenerateAndUploadCrossSigningKeysWithPassword(ctx context.Context, userPassword, passphrase string) (string, *CrossSigningKeysCache, error) {
return mach.GenerateAndUploadCrossSigningKeys(ctx, func(uiResp *mautrix.RespUserInteractive) interface{} {
return &mautrix.ReqUIAuthLogin{
BaseAuthData: mautrix.BaseAuthData{
Type: mautrix.AuthTypePassword,
@@ -92,29 +68,68 @@ func (mach *OlmMachine) GenerateAndUploadCrossSigningKeys(ctx context.Context, u
User: mach.Client.UserID.String(),
Password: userPassword,
}
})
}, passphrase)
}
// 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(ctx context.Context, uiaCallback mautrix.UIACallback, passphrase string) (string, *CrossSigningKeysCache, error) {
key, err := mach.SSSS.GenerateAndUploadKey(ctx, passphrase)
if err != nil {
return recoveryKey, fmt.Errorf("failed to publish cross-signing keys: %w", err)
return "", nil, 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 "", nil, err
}
// Store the private keys in SSSS
if err := mach.UploadCrossSigningKeysToSSSS(ctx, key, keysCache); err != nil {
return "", nil, fmt.Errorf("failed to upload cross-signing keys to SSSS: %w", err)
}
// Publish cross-signing keys
err = mach.PublishCrossSigningKeys(ctx, keysCache, uiaCallback)
if err != nil {
return "", nil, fmt.Errorf("failed to publish cross-signing keys: %w", err)
}
err = mach.SSSS.SetDefaultKeyID(ctx, key.ID)
if err != nil {
return recoveryKey, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err)
return "", nil, fmt.Errorf("failed to mark %s as the default key: %w", key.ID, err)
}
return recoveryKey, nil
return key.RecoveryKey(), keysCache, nil
}
// UploadCrossSigningKeysToSSSS stores the given cross-signing keys on the server encrypted with the given key.
func (mach *OlmMachine) UploadCrossSigningKeysToSSSS(ctx context.Context, key *ssss.Key, keys *CrossSigningKeysCache) error {
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningMaster, keys.MasterKey.Seed, key); err != nil {
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningMaster, keys.MasterKey.Seed(), key); err != nil {
return err
}
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningSelf, keys.SelfSigningKey.Seed, key); err != nil {
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningSelf, keys.SelfSigningKey.Seed(), key); err != nil {
return err
}
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed, key); err != nil {
if err := mach.SSSS.SetEncryptedAccountData(ctx, event.AccountDataCrossSigningUser, keys.UserSigningKey.Seed(), key); err != nil {
return err
}
// Also store these locally
if err := mach.CryptoStore.PutCrossSigningKey(ctx, mach.Client.UserID, id.XSUsageMaster, keys.MasterKey.PublicKey()); err != nil {
return err
}
if err := mach.CryptoStore.PutCrossSigningKey(ctx, mach.Client.UserID, id.XSUsageSelfSigning, keys.SelfSigningKey.PublicKey()); err != nil {
return err
}
if err := mach.CryptoStore.PutCrossSigningKey(ctx, mach.Client.UserID, id.XSUsageUserSigning, keys.UserSigningKey.PublicKey()); err != nil {
return err
}
return nil
}

View File

@@ -11,7 +11,7 @@ import (
"context"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
@@ -80,7 +80,7 @@ func (mach *OlmMachine) storeCrossSigningKeys(ctx context.Context, crossSigningK
}
log.Debug().Msg("Verifying cross-signing key signature")
if verified, err := olm.VerifySignatureJSON(userKeys, signUserID, signKeyName, signingKey); err != nil {
if verified, err := signatures.VerifySignatureJSON(userKeys, signUserID, signKeyName, signingKey); err != nil {
log.Warn().Err(err).Msg("Error verifying cross-signing key signature")
} else {
if verified {

View File

@@ -72,7 +72,11 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event
if sess.SigningKey == ownSigningKey && sess.SenderKey == ownIdentityKey && len(sess.ForwardingChains) == 0 {
trustLevel = id.TrustStateVerified
} else {
device, err = mach.GetOrFetchDeviceByKey(ctx, evt.Sender, sess.SenderKey)
if mach.DisableDecryptKeyFetching {
device, err = mach.CryptoStore.FindDeviceByKey(ctx, evt.Sender, sess.SenderKey)
} else {
device, err = mach.GetOrFetchDeviceByKey(ctx, evt.Sender, sess.SenderKey)
}
if err != nil {
// We don't want to throw these errors as the message can still be decrypted.
log.Debug().Err(err).Msg("Failed to get device to verify session")

View File

@@ -57,7 +57,7 @@ func (mach *OlmMachine) decryptOlmEvent(ctx context.Context, evt *event.Event) (
if !ok {
return nil, NotEncryptedForMe
}
decrypted, err := mach.decryptAndParseOlmCiphertext(ctx, evt.Sender, content.SenderKey, ownContent.Type, ownContent.Body)
decrypted, err := mach.decryptAndParseOlmCiphertext(ctx, evt, content.SenderKey, ownContent.Type, ownContent.Body)
if err != nil {
return nil, err
}
@@ -69,13 +69,13 @@ type OlmEventKeys struct {
Ed25519 id.Ed25519 `json:"ed25519"`
}
func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, sender id.UserID, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*DecryptedOlmEvent, error) {
func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, evt *event.Event, senderKey id.SenderKey, olmType id.OlmMsgType, ciphertext string) (*DecryptedOlmEvent, error) {
if olmType != id.OlmMsgTypePreKey && olmType != id.OlmMsgTypeMsg {
return nil, UnsupportedOlmMessageType
}
endTimeTrace := mach.timeTrace(ctx, "decrypting olm ciphertext", 5*time.Second)
plaintext, err := mach.tryDecryptOlmCiphertext(ctx, sender, senderKey, olmType, ciphertext)
plaintext, err := mach.tryDecryptOlmCiphertext(ctx, evt.Sender, senderKey, olmType, ciphertext)
endTimeTrace()
if err != nil {
return nil, err
@@ -88,7 +88,8 @@ func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, sender
if err != nil {
return nil, fmt.Errorf("failed to parse olm payload: %w", err)
}
if sender != olmEvt.Sender {
olmEvt.Type.Class = evt.Type.Class
if evt.Sender != olmEvt.Sender {
return nil, SenderMismatch
} else if mach.Client.UserID != olmEvt.Recipient {
return nil, RecipientMismatch

View File

@@ -14,7 +14,7 @@ import (
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
@@ -52,7 +52,7 @@ func (mach *OlmMachine) storeDeviceSelfSignatures(ctx context.Context, userID id
} else if _, ok := selfSigs[id.NewKeyID(id.KeyAlgorithmEd25519, pubKey.String())]; !ok {
continue
}
if verified, err := olm.VerifySignatureJSON(deviceKeys, signerUserID, pubKey.String(), pubKey); verified {
if verified, err := signatures.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())]
log.Trace().Err(err).
@@ -245,7 +245,7 @@ func (mach *OlmMachine) validateDevice(userID id.UserID, deviceID id.DeviceID, d
return existing, fmt.Errorf("%w (expected %s, got %s)", MismatchingSigningKey, existing.SigningKey, signingKey)
}
ok, err := olm.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey)
ok, err := signatures.VerifySignatureJSON(deviceKeys, userID, deviceID.String(), signingKey)
if err != nil {
return existing, fmt.Errorf("failed to verify signature: %w", err)
} else if !ok {

View File

@@ -118,7 +118,7 @@ func (mach *OlmMachine) EncryptMegolmEvent(ctx context.Context, roomID id.RoomID
log.Debug().Msg("Encrypted event successfully")
err = mach.CryptoStore.UpdateOutboundGroupSession(ctx, session)
if err != nil {
log.Warn().Err(err).Msg("Failed to update megolm session in crypto store after encrypting")
return nil, fmt.Errorf("failed to update outbound group session after encrypting: %w", err)
}
encrypted := &event.EncryptedEventContent{
Algorithm: id.AlgorithmMegolmV1,
@@ -330,6 +330,17 @@ func (mach *OlmMachine) encryptAndSendGroupSession(ctx context.Context, session
Str("target_user_id", userID.String()).
Str("target_device_id", deviceID.String()).
Msg("Encrypted group session for device")
if !mach.DisableSharedGroupSessionTracking {
err := mach.CryptoStore.MarkOutboundGroupSessionShared(ctx, userID, device.identity.IdentityKey, session.id)
if err != nil {
log.Warn().
Err(err).
Str("target_user_id", userID.String()).
Str("target_device_id", deviceID.String()).
Stringer("target_session_id", session.id).
Msg("Failed to mark outbound group session shared")
}
}
}
}

View File

@@ -12,7 +12,7 @@ import (
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -109,7 +109,7 @@ func (mach *OlmMachine) createOutboundSessions(ctx context.Context, input map[id
continue
}
identity := input[userID][deviceID]
if ok, err := olm.VerifySignatureJSON(oneTimeKey.RawData, userID, deviceID.String(), identity.SigningKey); err != nil {
if ok, err := signatures.VerifySignatureJSON(oneTimeKey.RawData, userID, deviceID.String(), identity.SigningKey); err != nil {
log.Error().Err(err).Msg("Failed to verify signature of one-time key")
} else if !ok {
log.Warn().Msg("One-time key has invalid signature from device")

View File

@@ -110,12 +110,13 @@ func (a Account) IdentityKeys() (id.Ed25519, id.Curve25519) {
return ed25519, curve25519
}
// Sign returns the signature of a message using the Ed25519 key for this Account.
// Sign returns the base64-encoded signature of a message using the Ed25519 key
// for this Account.
func (a Account) Sign(message []byte) ([]byte, error) {
if len(message) == 0 {
return nil, fmt.Errorf("sign: %w", goolm.ErrEmptyInput)
}
return goolm.Base64Encode(a.IdKeys.Ed25519.Sign(message)), nil
return []byte(base64.RawStdEncoding.EncodeToString(a.IdKeys.Ed25519.Sign(message))), nil
}
// OneTimeKeys returns the public parts of the unpublished one time keys of the Account.

View File

@@ -2,8 +2,10 @@ package cipher
import (
"bytes"
"crypto/aes"
"io"
"maunium.net/go/mautrix/crypto/aescbc"
"maunium.net/go/mautrix/crypto/goolm/crypto"
)
@@ -36,7 +38,7 @@ func deriveAESKeys(kdfInfo []byte, key []byte) (*derivedAESKeys, error) {
// AESSha512BlockSize resturns the blocksize of the cipher AESSHA256.
func AESSha512BlockSize() int {
return crypto.AESCBCBlocksize()
return aes.BlockSize
}
// AESSHA256 is a valid cipher using AES with CBC and HKDFSha256.
@@ -57,7 +59,7 @@ func (c AESSHA256) Encrypt(key, plaintext []byte) (ciphertext []byte, err error)
if err != nil {
return nil, err
}
ciphertext, err = crypto.AESCBCEncrypt(keys.key, keys.iv, plaintext)
ciphertext, err = aescbc.Encrypt(keys.key, keys.iv, plaintext)
if err != nil {
return nil, err
}
@@ -70,7 +72,7 @@ func (c AESSHA256) Decrypt(key, ciphertext []byte) (plaintext []byte, err error)
if err != nil {
return nil, err
}
plaintext, err = crypto.AESCBCDecrypt(keys.key, keys.iv, ciphertext)
plaintext, err = aescbc.Decrypt(keys.key, keys.iv, ciphertext)
if err != nil {
return nil, err
}

View File

@@ -1,4 +1,5 @@
// cipher provides the methods and structs to do encryptions for olm/megolm.
// Package cipher provides the methods and structs to do encryptions for
// olm/megolm.
package cipher
// Cipher defines a valid cipher.

View File

@@ -1,75 +0,0 @@
package crypto
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"fmt"
"maunium.net/go/mautrix/crypto/goolm"
)
// AESCBCBlocksize returns the blocksize of the encryption method
func AESCBCBlocksize() int {
return aes.BlockSize
}
// AESCBCEncrypt encrypts the plaintext with the key and iv. len(iv) must be equal to the blocksize!
func AESCBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, fmt.Errorf("AESCBCEncrypt: %w", goolm.ErrNoKeyProvided)
}
if len(iv) != AESCBCBlocksize() {
return nil, fmt.Errorf("iv: %w", goolm.ErrNotBlocksize)
}
var cipherText []byte
plaintext = pkcs5Padding(plaintext, AESCBCBlocksize())
if len(plaintext)%AESCBCBlocksize() != 0 {
return nil, fmt.Errorf("message: %w", goolm.ErrNotMultipleBlocksize)
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
cipherText = make([]byte, len(plaintext))
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(cipherText, plaintext)
return cipherText, nil
}
// AESCBCDecrypt decrypts the ciphertext with the key and iv. len(iv) must be equal to the blocksize!
func AESCBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {
if len(key) == 0 {
return nil, fmt.Errorf("AESCBCEncrypt: %w", goolm.ErrNoKeyProvided)
}
if len(iv) != AESCBCBlocksize() {
return nil, fmt.Errorf("iv: %w", goolm.ErrNotBlocksize)
}
var block cipher.Block
var err error
block, err = aes.NewCipher(key)
if err != nil {
return nil, err
}
if len(ciphertext) < AESCBCBlocksize() {
return nil, fmt.Errorf("ciphertext: %w", goolm.ErrNotMultipleBlocksize)
}
cbc := cipher.NewCBCDecrypter(block, iv)
cbc.CryptBlocks(ciphertext, ciphertext)
return pkcs5Unpadding(ciphertext), nil
}
// pkcs5Padding paddes the plaintext to be used in the AESCBC encryption.
func pkcs5Padding(plaintext []byte, blockSize int) []byte {
padding := (blockSize - len(plaintext)%blockSize)
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(plaintext, padtext...)
}
// pkcs5Unpadding undoes the padding to the plaintext after AESCBC decryption.
func pkcs5Unpadding(plaintext []byte) []byte {
length := len(plaintext)
unpadding := int(plaintext[length-1])
return plaintext[:(length - unpadding)]
}

View File

@@ -0,0 +1,2 @@
// Package crpyto provides the nessesary encryption methods for olm/megolm
package crypto

View File

@@ -1,2 +0,0 @@
// crpyto provides the nessesary encryption methods for olm/megolm
package crypto

View File

@@ -21,8 +21,6 @@ var (
ErrChainTooHigh = errors.New("chain index too high")
ErrBadInput = errors.New("bad input")
ErrBadVersion = errors.New("wrong version")
ErrNotBlocksize = errors.New("length != blocksize")
ErrNotMultipleBlocksize = errors.New("length not a multiple of the blocksize")
ErrWrongPickleVersion = errors.New("wrong pickle version")
ErrValueTooShort = errors.New("value too short")
ErrInputToSmall = errors.New("input too small (truncated?)")

View File

@@ -45,8 +45,8 @@ func NewDecryptionFromPrivate(privateKey crypto.Curve25519PrivateKey) (*Decrypti
return s, nil
}
// PubKey returns the public key base 64 encoded.
func (s Decryption) PubKey() id.Curve25519 {
// PublicKey returns the public key base 64 encoded.
func (s Decryption) PublicKey() id.Curve25519 {
return s.KeyPair.B64Encoded()
}

View File

@@ -2,7 +2,11 @@ package pk
import (
"crypto/rand"
"encoding/json"
"github.com/tidwall/sjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/goolm"
"maunium.net/go/mautrix/crypto/goolm/crypto"
"maunium.net/go/mautrix/id"
@@ -10,15 +14,15 @@ import (
// Signing is used for signing a pk
type Signing struct {
KeyPair crypto.Ed25519KeyPair `json:"key_pair"`
Seed []byte `json:"seed"`
keyPair crypto.Ed25519KeyPair
seed []byte
}
// NewSigningFromSeed constructs a new Signing based on a seed.
func NewSigningFromSeed(seed []byte) (*Signing, error) {
s := &Signing{}
s.Seed = seed
s.KeyPair = crypto.Ed25519GenerateFromSeed(seed)
s.seed = seed
s.keyPair = crypto.Ed25519GenerateFromSeed(seed)
return s, nil
}
@@ -32,13 +36,34 @@ func NewSigning() (*Signing, error) {
return NewSigningFromSeed(seed)
}
// Sign returns the signature of the message base64 encoded.
func (s Signing) Sign(message []byte) []byte {
signature := s.KeyPair.Sign(message)
return goolm.Base64Encode(signature)
// Seed returns the seed of the key pair.
func (s Signing) Seed() []byte {
return s.seed
}
// PublicKey returns the public key of the key pair base 64 encoded.
func (s Signing) PublicKey() id.Ed25519 {
return s.KeyPair.B64Encoded()
return s.keyPair.B64Encoded()
}
// Sign returns the signature of the message base64 encoded.
func (s Signing) Sign(message []byte) ([]byte, error) {
signature := s.keyPair.Sign(message)
return goolm.Base64Encode(signature), nil
}
// SignJSON creates a signature for the given object after encoding it to
// canonical JSON.
func (s Signing) SignJSON(obj any) (string, error) {
objJSON, err := json.Marshal(obj)
if err != nil {
return "", err
}
objJSON, _ = sjson.DeleteBytes(objJSON, "unsigned")
objJSON, _ = sjson.DeleteBytes(objJSON, "signatures")
signature, err := s.Sign(canonicaljson.CanonicalJSONAssumeValid(objJSON))
if err != nil {
return "", err
}
return string(signature), nil
}

View File

@@ -1,76 +0,0 @@
// sas provides the means to do SAS between keys
package sas
import (
"io"
"maunium.net/go/mautrix/crypto/goolm"
"maunium.net/go/mautrix/crypto/goolm/crypto"
)
// SAS contains the key pair and secret for SAS.
type SAS struct {
KeyPair crypto.Curve25519KeyPair
Secret []byte
}
// New creates a new SAS with a new key pair.
func New() (*SAS, error) {
kp, err := crypto.Curve25519GenerateKey(nil)
if err != nil {
return nil, err
}
s := &SAS{
KeyPair: kp,
}
return s, nil
}
// GetPubkey returns the public key of the key pair base64 encoded
func (s SAS) GetPubkey() []byte {
return goolm.Base64Encode(s.KeyPair.PublicKey)
}
// SetTheirKey sets the key of the other party and computes the shared secret.
func (s *SAS) SetTheirKey(key []byte) error {
keyDecoded, err := goolm.Base64Decode(key)
if err != nil {
return err
}
sharedSecret, err := s.KeyPair.SharedSecret(keyDecoded)
if err != nil {
return err
}
s.Secret = sharedSecret
return nil
}
// GenerateBytes creates length bytes from the shared secret and info.
func (s SAS) GenerateBytes(info []byte, length uint) ([]byte, error) {
byteReader := crypto.HKDFSHA256(s.Secret, nil, info)
output := make([]byte, length)
if _, err := io.ReadFull(byteReader, output); err != nil {
return nil, err
}
return output, nil
}
// calculateMAC returns a base64 encoded MAC of input.
func (s *SAS) calculateMAC(input, info []byte, length uint) ([]byte, error) {
key, err := s.GenerateBytes(info, length)
if err != nil {
return nil, err
}
mac := crypto.HMACSHA256(key, input)
return goolm.Base64Encode(mac), nil
}
// CalculateMACFixes returns a base64 encoded, 32 byte long MAC of input.
func (s SAS) CalculateMAC(input, info []byte) ([]byte, error) {
return s.calculateMAC(input, info, 32)
}
// CalculateMACLongKDF returns a base64 encoded, 256 byte long MAC of input.
func (s SAS) CalculateMACLongKDF(input, info []byte) ([]byte, error) {
return s.calculateMAC(input, info, 256)
}

View File

@@ -0,0 +1,3 @@
// Package session provides the different types of sessions for en/decrypting
// of messages
package session

View File

@@ -1,2 +0,0 @@
// session provides the different types of sessions for en/decrypting of messages
package session

View File

@@ -1,23 +0,0 @@
package utilities
import (
"encoding/base64"
"maunium.net/go/mautrix/crypto/goolm"
"maunium.net/go/mautrix/crypto/goolm/crypto"
"maunium.net/go/mautrix/id"
)
// VerifySignature verifies an ed25519 signature.
func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) {
keyDecoded, err := base64.RawStdEncoding.DecodeString(string(key))
if err != nil {
return false, err
}
signatureDecoded, err := goolm.Base64Decode(signature)
if err != nil {
return false, err
}
publicKey := crypto.Ed25519PublicKey(keyDecoded)
return publicKey.Verify(message, signatureDecoded), nil
}

184
vendor/maunium.net/go/mautrix/crypto/keybackup.go generated vendored Normal file
View File

@@ -0,0 +1,184 @@
package crypto
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/backup"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/signatures"
"maunium.net/go/mautrix/id"
)
func (mach *OlmMachine) DownloadAndStoreLatestKeyBackup(ctx context.Context, megolmBackupKey *backup.MegolmBackupKey) (id.KeyBackupVersion, error) {
log := mach.machOrContextLog(ctx).With().
Str("action", "download and store latest key backup").
Logger()
ctx = log.WithContext(ctx)
versionInfo, err := mach.GetAndVerifyLatestKeyBackupVersion(ctx)
if err != nil {
return "", err
} else if versionInfo == nil {
return "", nil
}
err = mach.GetAndStoreKeyBackup(ctx, versionInfo.Version, megolmBackupKey)
return versionInfo.Version, err
}
func (mach *OlmMachine) GetAndVerifyLatestKeyBackupVersion(ctx context.Context) (*mautrix.RespRoomKeysVersion[backup.MegolmAuthData], error) {
versionInfo, err := mach.Client.GetKeyBackupLatestVersion(ctx)
if err != nil {
return nil, err
}
if versionInfo.Algorithm != id.KeyBackupAlgorithmMegolmBackupV1 {
return nil, fmt.Errorf("unsupported key backup algorithm: %s", versionInfo.Algorithm)
}
log := mach.machOrContextLog(ctx).With().
Int("count", versionInfo.Count).
Str("etag", versionInfo.ETag).
Stringer("key_backup_version", versionInfo.Version).
Logger()
userSignatures, ok := versionInfo.AuthData.Signatures[mach.Client.UserID]
if !ok {
return nil, fmt.Errorf("no signature from user %s found in key backup", mach.Client.UserID)
}
crossSigningPubkeys := mach.GetOwnCrossSigningPublicKeys(ctx)
signatureVerified := false
for keyID := range userSignatures {
keyAlg, keyName := keyID.Parse()
if keyAlg != id.KeyAlgorithmEd25519 {
continue
}
log := log.With().Str("key_name", keyName).Logger()
var key id.Ed25519
if keyName == crossSigningPubkeys.MasterKey.String() {
key = crossSigningPubkeys.MasterKey
} else if device, err := mach.GetOrFetchDevice(ctx, mach.Client.UserID, id.DeviceID(keyName)); err != nil {
log.Warn().Err(err).Msg("Failed to fetch device")
continue
} else if !mach.IsDeviceTrusted(device) {
log.Warn().Err(err).Msg("Device is not trusted")
continue
} else {
key = device.SigningKey
}
ok, err = signatures.VerifySignatureJSON(versionInfo.AuthData, mach.Client.UserID, keyName, key)
if err != nil || !ok {
log.Warn().Err(err).Stringer("key_id", keyID).Msg("Signature verification failed")
continue
} else {
// One of the signatures is valid, break from the loop.
signatureVerified = true
break
}
}
if !signatureVerified {
return nil, fmt.Errorf("no valid signature from user %s found in key backup", mach.Client.UserID)
}
return versionInfo, nil
}
func (mach *OlmMachine) GetAndStoreKeyBackup(ctx context.Context, version id.KeyBackupVersion, megolmBackupKey *backup.MegolmBackupKey) error {
keys, err := mach.Client.GetKeyBackup(ctx, version)
if err != nil {
return err
}
log := zerolog.Ctx(ctx)
var count, failedCount int
for roomID, backup := range keys.Rooms {
for sessionID, keyBackupData := range backup.Sessions {
sessionData, err := keyBackupData.SessionData.Decrypt(megolmBackupKey)
if err != nil {
log.Warn().Err(err).Msg("Failed to decrypt session data")
failedCount++
continue
}
err = mach.ImportRoomKeyFromBackup(ctx, version, roomID, sessionID, sessionData)
if err != nil {
log.Warn().Err(err).Msg("Failed to import room key from backup")
failedCount++
continue
}
count++
}
}
log.Info().
Int("count", count).
Int("failed_count", failedCount).
Msg("successfully imported sessions from backup")
return nil
}
func (mach *OlmMachine) ImportRoomKeyFromBackup(ctx context.Context, version id.KeyBackupVersion, roomID id.RoomID, sessionID id.SessionID, keyBackupData *backup.MegolmSessionData) error {
log := zerolog.Ctx(ctx).With().
Str("room_id", roomID.String()).
Str("session_id", sessionID.String()).
Logger()
if keyBackupData.Algorithm != id.AlgorithmMegolmV1 {
return fmt.Errorf("ignoring room key in backup with weird algorithm %s", keyBackupData.Algorithm)
}
igsInternal, err := olm.InboundGroupSessionImport([]byte(keyBackupData.SessionKey))
if err != nil {
return fmt.Errorf("failed to import inbound group session: %w", err)
} else if igsInternal.ID() != sessionID {
log.Warn().
Stringer("actual_session_id", igsInternal.ID()).
Msg("Mismatched session ID while creating inbound group session from key backup")
return fmt.Errorf("mismatched session ID while creating inbound group session from key backup")
}
var maxAge time.Duration
var maxMessages int
if config, err := mach.StateStore.GetEncryptionEvent(ctx, roomID); err != nil {
log.Error().Err(err).Msg("Failed to get encryption event for room")
} else if config != nil {
maxAge = time.Duration(config.RotationPeriodMillis) * time.Millisecond
maxMessages = config.RotationPeriodMessages
}
if firstKnownIndex := igsInternal.FirstKnownIndex(); firstKnownIndex > 0 {
log.Warn().Uint32("first_known_index", firstKnownIndex).Msg("Importing partial session")
}
igs := &InboundGroupSession{
Internal: *igsInternal,
SigningKey: keyBackupData.SenderClaimedKeys.Ed25519,
SenderKey: keyBackupData.SenderKey,
RoomID: roomID,
ForwardingChains: append(keyBackupData.ForwardingKeyChain, keyBackupData.SenderKey.String()),
id: sessionID,
ReceivedAt: time.Now().UTC(),
MaxAge: maxAge.Milliseconds(),
MaxMessages: maxMessages,
KeyBackupVersion: version,
}
err = mach.CryptoStore.PutGroupSession(ctx, roomID, keyBackupData.SenderKey, sessionID, igs)
if err != nil {
return fmt.Errorf("failed to store new inbound group session: %w", err)
}
mach.markSessionReceived(ctx, sessionID)
return nil
}

View File

@@ -122,7 +122,7 @@ func (mach *OlmMachine) importExportedRoomKey(ctx context.Context, session Expor
if err != nil {
return false, fmt.Errorf("failed to store imported session: %w", err)
}
mach.markSessionReceived(igs.ID())
mach.markSessionReceived(ctx, igs.ID())
return true, nil
}

View File

@@ -168,6 +168,9 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt
if content.MaxMessages != 0 {
maxMessages = content.MaxMessages
}
if firstKnownIndex := igsInternal.FirstKnownIndex(); firstKnownIndex > 0 {
log.Warn().Uint32("first_known_index", firstKnownIndex).Msg("Importing partial session")
}
igs := &InboundGroupSession{
Internal: *igsInternal,
SigningKey: evt.Keys.Ed25519,
@@ -186,7 +189,7 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt
log.Error().Err(err).Msg("Failed to store new inbound group session")
return false
}
mach.markSessionReceived(content.SessionID)
mach.markSessionReceived(ctx, content.SessionID)
log.Debug().Msg("Received forwarded inbound group session")
return true
}
@@ -222,11 +225,34 @@ func (mach *OlmMachine) rejectKeyRequest(ctx context.Context, rejection KeyShare
}
}
func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Device, _ event.RequestedKeyInfo) *KeyShareRejection {
// sendToOneDevice sends a to-device event to a single device.
func (mach *OlmMachine) sendToOneDevice(ctx context.Context, userID id.UserID, deviceID id.DeviceID, eventType event.Type, content interface{}) error {
_, err := mach.Client.SendToDevice(ctx, eventType, &mautrix.ReqSendToDevice{
Messages: map[id.UserID]map[id.DeviceID]*event.Content{
userID: {
deviceID: {
Parsed: content,
},
},
},
})
return err
}
func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Device, evt event.RequestedKeyInfo) *KeyShareRejection {
log := mach.machOrContextLog(ctx)
if mach.Client.UserID != device.UserID {
log.Debug().Msg("Rejecting key request from a different user")
return &KeyShareRejectOtherUser
isShared, err := mach.CryptoStore.IsOutboundGroupSessionShared(ctx, device.UserID, device.IdentityKey, evt.SessionID)
if err != nil {
log.Err(err).Msg("Rejecting key request due to internal error when checking session sharing")
return &KeyShareRejectNoResponse
} else if !isShared {
log.Debug().Msg("Rejecting key request for unshared session")
return &KeyShareRejectOtherUser
}
log.Debug().Msg("Accepting key request for shared session")
return nil
} else if mach.Client.DeviceID == device.DeviceID {
log.Debug().Msg("Ignoring key request from ourselves")
return &KeyShareRejectNoResponse
@@ -248,7 +274,7 @@ func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Dev
}
}
func (mach *OlmMachine) handleRoomKeyRequest(ctx context.Context, sender id.UserID, content *event.RoomKeyRequestEventContent) {
func (mach *OlmMachine) HandleRoomKeyRequest(ctx context.Context, sender id.UserID, content *event.RoomKeyRequestEventContent) {
log := zerolog.Ctx(ctx).With().
Str("request_id", content.RequestID).
Str("device_id", content.RequestingDeviceID.String()).
@@ -327,7 +353,7 @@ func (mach *OlmMachine) handleRoomKeyRequest(ctx context.Context, sender id.User
}
}
func (mach *OlmMachine) handleBeeperRoomKeyAck(ctx context.Context, sender id.UserID, content *event.BeeperRoomKeyAckEventContent) {
func (mach *OlmMachine) HandleBeeperRoomKeyAck(ctx context.Context, sender id.UserID, content *event.BeeperRoomKeyAckEventContent) {
log := mach.machOrContextLog(ctx).With().
Str("room_id", content.RoomID.String()).
Str("session_id", content.SessionID.String()).

View File

@@ -33,18 +33,17 @@ type OlmMachine struct {
PlaintextMentions bool
// Never ask the server for keys automatically as a side effect.
DisableKeyFetching bool
// Never ask the server for keys automatically as a side effect during Megolm decryption.
DisableDecryptKeyFetching bool
// Don't mark outbound Olm sessions as shared for devices they were initially sent to.
DisableSharedGroupSessionTracking bool
SendKeysMinTrust id.TrustState
ShareKeysMinTrust id.TrustState
AllowKeyShare func(context.Context, *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
@@ -53,6 +52,9 @@ type OlmMachine struct {
keyWaiters map[id.SessionID]chan struct{}
keyWaitersLock sync.Mutex
// Optional callback which is called when we save a session to store
SessionReceived func(context.Context, id.SessionID)
devicesToUnwedge map[id.IdentityKey]bool
devicesToUnwedgeLock sync.Mutex
recentlyUnwedged map[id.IdentityKey]time.Time
@@ -78,6 +80,9 @@ type OlmMachine struct {
DeleteKeysOnDeviceDelete bool
DisableDeviceChangeKeyRotation bool
secretLock sync.Mutex
secretListeners map[string]chan<- string
}
// StateStore is used by OlmMachine to get room state information that's needed for encryption.
@@ -106,12 +111,6 @@ func NewOlmMachine(client *mautrix.Client, log *zerolog.Logger, cryptoStore Stor
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{},
@@ -119,6 +118,7 @@ func NewOlmMachine(client *mautrix.Client, log *zerolog.Logger, cryptoStore Stor
devicesToUnwedge: make(map[id.IdentityKey]bool),
recentlyUnwedged: make(map[id.IdentityKey]time.Time),
secretListeners: make(map[string]chan<- string),
}
mach.AllowKeyShare = mach.defaultAllowKeyShare
return mach
@@ -145,11 +145,21 @@ func (mach *OlmMachine) Load(ctx context.Context) (err error) {
return nil
}
func (mach *OlmMachine) saveAccount(ctx context.Context) {
func (mach *OlmMachine) saveAccount(ctx context.Context) error {
err := mach.CryptoStore.PutAccount(ctx, mach.account)
if err != nil {
mach.Log.Error().Err(err).Msg("Failed to save account")
}
return err
}
func (mach *OlmMachine) KeyBackupVersion() id.KeyBackupVersion {
return mach.account.KeyBackupVersion
}
func (mach *OlmMachine) SetKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) error {
mach.account.KeyBackupVersion = version
return mach.saveAccount(ctx)
}
// FlushStore calls the Flush method of the CryptoStore.
@@ -227,11 +237,7 @@ func (mach *OlmMachine) HandleDeviceLists(ctx context.Context, dl *mautrix.Devic
Str("trace_id", traceID).
Interface("changes", dl.Changed).
Msg("Device list changes in /sync")
if mach.DisableKeyFetching {
mach.CryptoStore.MarkTrackedUsersOutdated(ctx, dl.Changed)
} else {
mach.FetchKeys(ctx, dl.Changed, false)
}
mach.FetchKeys(ctx, dl.Changed, false)
mach.Log.Debug().Str("trace_id", traceID).Msg("Finished handling device list changes")
}
}
@@ -328,6 +334,47 @@ func (mach *OlmMachine) HandleMemberEvent(ctx context.Context, evt *event.Event)
}
}
func (mach *OlmMachine) HandleEncryptedEvent(ctx context.Context, evt *event.Event) {
if _, ok := evt.Content.Parsed.(*event.EncryptedEventContent); !ok {
mach.machOrContextLog(ctx).Warn().Msg("Passed invalid event to encrypted handler")
return
}
decryptedEvt, err := mach.decryptOlmEvent(ctx, evt)
if err != nil {
mach.machOrContextLog(ctx).Error().Err(err).Msg("Failed to decrypt to-device event")
return
}
log := mach.machOrContextLog(ctx).With().
Str("decrypted_type", decryptedEvt.Type.Type).
Str("sender_device", decryptedEvt.SenderDevice.String()).
Str("sender_signing_key", decryptedEvt.Keys.Ed25519.String()).
Logger()
log.Trace().Msg("Successfully decrypted to-device event")
switch decryptedContent := decryptedEvt.Content.Parsed.(type) {
case *event.RoomKeyEventContent:
mach.receiveRoomKey(ctx, decryptedEvt, decryptedContent)
log.Trace().Msg("Handled room key event")
case *event.ForwardedRoomKeyEventContent:
if mach.importForwardedRoomKey(ctx, 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{}))
}
}
log.Trace().Msg("Handled forwarded room key event")
case *event.DummyEventContent:
log.Debug().Msg("Received encrypted dummy event")
case *event.SecretSendEventContent:
mach.receiveSecret(ctx, decryptedEvt, decryptedContent)
log.Trace().Msg("Handled secret send event")
default:
log.Debug().Msg("Unhandled encrypted to-device event")
}
}
// 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(ctx context.Context, evt *event.Event) {
@@ -352,60 +399,19 @@ func (mach *OlmMachine) HandleToDeviceEvent(ctx context.Context, evt *event.Even
}
switch content := evt.Content.Parsed.(type) {
case *event.EncryptedEventContent:
log = log.With().
Str("sender_key", content.SenderKey.String()).
Logger()
log.Debug().Msg("Handling encrypted to-device event")
ctx = log.WithContext(ctx)
decryptedEvt, err := mach.decryptOlmEvent(ctx, evt)
if err != nil {
log.Error().Err(err).Msg("Failed to decrypt to-device event")
return
}
log = log.With().
Str("decrypted_type", decryptedEvt.Type.Type).
Str("sender_device", decryptedEvt.SenderDevice.String()).
Str("sender_signing_key", decryptedEvt.Keys.Ed25519.String()).
Logger()
log.Trace().Msg("Successfully decrypted to-device event")
switch decryptedContent := decryptedEvt.Content.Parsed.(type) {
case *event.RoomKeyEventContent:
mach.receiveRoomKey(ctx, decryptedEvt, decryptedContent)
log.Trace().Msg("Handled room key event")
case *event.ForwardedRoomKeyEventContent:
if mach.importForwardedRoomKey(ctx, 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{}))
}
}
log.Trace().Msg("Handled forwarded room key event")
case *event.DummyEventContent:
log.Debug().Msg("Received encrypted dummy event")
default:
log.Debug().Msg("Unhandled encrypted to-device event")
}
mach.HandleEncryptedEvent(ctx, evt)
return
case *event.RoomKeyRequestEventContent:
go mach.handleRoomKeyRequest(ctx, evt.Sender, content)
go mach.HandleRoomKeyRequest(ctx, evt.Sender, content)
case *event.BeeperRoomKeyAckEventContent:
mach.handleBeeperRoomKeyAck(ctx, evt.Sender, content)
// verification cases
case *event.VerificationStartEventContent:
mach.handleVerificationStart(ctx, evt.Sender, content, content.TransactionID, 10*time.Minute, "")
case *event.VerificationAcceptEventContent:
mach.handleVerificationAccept(ctx, evt.Sender, content, content.TransactionID)
case *event.VerificationKeyEventContent:
mach.handleVerificationKey(ctx, evt.Sender, content, content.TransactionID)
case *event.VerificationMacEventContent:
mach.handleVerificationMAC(ctx, evt.Sender, content, content.TransactionID)
case *event.VerificationCancelEventContent:
mach.handleVerificationCancel(evt.Sender, content, content.TransactionID)
case *event.VerificationRequestEventContent:
mach.handleVerificationRequest(ctx, evt.Sender, content, content.TransactionID, "")
mach.HandleBeeperRoomKeyAck(ctx, evt.Sender, content)
case *event.RoomKeyWithheldEventContent:
mach.handleRoomKeyWithheld(ctx, content)
mach.HandleRoomKeyWithheld(ctx, content)
case *event.SecretRequestEventContent:
if content.Action == event.SecretRequestRequest {
mach.HandleSecretRequest(ctx, evt.Sender, content)
log.Trace().Msg("Handled secret request event")
}
default:
deviceID, _ := evt.Content.Raw["device_id"].(string)
log.Debug().Str("maybe_device_id", deviceID).Msg("Unhandled to-device event")
@@ -420,7 +426,7 @@ func (mach *OlmMachine) GetOrFetchDevice(ctx context.Context, userID id.UserID,
device, err := mach.CryptoStore.GetDevice(ctx, userID, deviceID)
if err != nil {
return nil, fmt.Errorf("failed to get sender device from store: %w", err)
} else if device != nil || mach.DisableKeyFetching {
} else if device != nil {
return device, nil
}
if usersToDevices, err := mach.FetchKeys(ctx, []id.UserID{userID}, true); err != nil {
@@ -439,7 +445,7 @@ func (mach *OlmMachine) GetOrFetchDevice(ctx context.Context, userID id.UserID,
// the given identity key.
func (mach *OlmMachine) GetOrFetchDeviceByKey(ctx context.Context, userID id.UserID, identityKey id.IdentityKey) (*id.Device, error) {
deviceIdentity, err := mach.CryptoStore.FindDeviceByKey(ctx, userID, identityKey)
if err != nil || deviceIdentity != nil || mach.DisableKeyFetching {
if err != nil || deviceIdentity != nil {
return deviceIdentity, err
}
mach.machOrContextLog(ctx).Debug().
@@ -517,7 +523,7 @@ func (mach *OlmMachine) createGroupSession(ctx context.Context, senderKey id.Sen
log.Error().Err(err).Str("session_id", sessionID.String()).Msg("Failed to store new inbound group session")
return
}
mach.markSessionReceived(sessionID)
mach.markSessionReceived(ctx, sessionID)
log.Debug().
Str("session_id", sessionID.String()).
Str("sender_key", senderKey.String()).
@@ -527,7 +533,11 @@ func (mach *OlmMachine) createGroupSession(ctx context.Context, senderKey id.Sen
Msg("Received inbound group session")
}
func (mach *OlmMachine) markSessionReceived(id id.SessionID) {
func (mach *OlmMachine) markSessionReceived(ctx context.Context, id id.SessionID) {
if mach.SessionReceived != nil {
mach.SessionReceived(ctx, id)
}
mach.keyWaitersLock.Lock()
ch, ok := mach.keyWaiters[id]
if ok {
@@ -619,7 +629,7 @@ func (mach *OlmMachine) receiveRoomKey(ctx context.Context, evt *DecryptedOlmEve
mach.createGroupSession(ctx, evt.SenderKey, evt.Keys.Ed25519, content.RoomID, content.SessionID, content.SessionKey, maxAge, maxMessages, content.IsScheduled)
}
func (mach *OlmMachine) handleRoomKeyWithheld(ctx context.Context, content *event.RoomKeyWithheldEventContent) {
func (mach *OlmMachine) HandleRoomKeyWithheld(ctx context.Context, content *event.RoomKeyWithheldEventContent) {
if content.Algorithm != id.AlgorithmMegolmV1 {
zerolog.Ctx(ctx).Debug().Interface("content", content).Msg("Non-megolm room key withheld event")
return
@@ -682,8 +692,7 @@ func (mach *OlmMachine) ShareKeys(ctx context.Context, currentOTKCount int) erro
}
mach.lastOTKUpload = time.Now()
mach.account.Shared = true
mach.saveAccount(ctx)
return nil
return mach.saveAccount(ctx)
}
func (mach *OlmMachine) ExpiredKeyDeleteLoop(ctx context.Context) {

View File

@@ -1,71 +1,29 @@
// Copyright (c) 2024 Sumner Evans
//
// 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/.
// When the goolm build flag is enabled, this file will make [PKSigning]
// constructors use the goolm constuctors.
//go:build goolm
package olm
import (
"encoding/json"
import "maunium.net/go/mautrix/crypto/goolm/pk"
"github.com/tidwall/sjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/goolm/pk"
"maunium.net/go/mautrix/id"
)
// PkSigning stores a key pair for signing messages.
type PkSigning struct {
pk.Signing
PublicKey id.Ed25519
Seed []byte
// NewPKSigningFromSeed creates a new PKSigning object using the given seed.
func NewPKSigningFromSeed(seed []byte) (PKSigning, error) {
return pk.NewSigningFromSeed(seed)
}
// Clear clears the underlying memory of a PkSigning object.
func (p *PkSigning) Clear() {
p.Signing = pk.Signing{}
// NewPKSigning creates a new [PKSigning] object, containing a key pair for
// signing messages.
func NewPKSigning() (PKSigning, error) {
return pk.NewSigning()
}
// NewPkSigningFromSeed creates a new PkSigning object using the given seed.
func NewPkSigningFromSeed(seed []byte) (*PkSigning, error) {
p := &PkSigning{}
signing, err := pk.NewSigningFromSeed(seed)
if err != nil {
return nil, err
}
p.Signing = *signing
p.Seed = seed
p.PublicKey = p.Signing.PublicKey()
return p, nil
}
// NewPkSigning creates a new PkSigning object, containing a key pair for signing messages.
func NewPkSigning() (*PkSigning, error) {
p := &PkSigning{}
signing, err := pk.NewSigning()
if err != nil {
return nil, err
}
p.Signing = *signing
p.Seed = signing.Seed
p.PublicKey = p.Signing.PublicKey()
return p, err
}
// Sign creates a signature for the given message using this key.
func (p *PkSigning) Sign(message []byte) ([]byte, error) {
return p.Signing.Sign(message), 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
func NewPKDecryption(privateKey []byte) (PKDecryption, error) {
return pk.NewDecryption()
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 olm
import (
"maunium.net/go/mautrix/crypto/goolm/pk"
"maunium.net/go/mautrix/id"
)
// PKSigning is an interface for signing messages.
type PKSigning interface {
// Seed returns the seed of the key.
Seed() []byte
// PublicKey returns the public key.
PublicKey() id.Ed25519
// Sign creates a signature for the given message using this key.
Sign(message []byte) ([]byte, error)
// SignJSON creates a signature for the given object after encoding it to
// canonical JSON.
SignJSON(obj any) (string, error)
}
var _ PKSigning = (*pk.Signing)(nil)
// PKDecryption is an interface for decrypting messages.
type PKDecryption interface {
// PublicKey returns the public key.
PublicKey() id.Curve25519
// Decrypt verifies and decrypts the given message.
Decrypt(ciphertext, mac []byte, key id.Curve25519) ([]byte, error)
}
var _ PKDecryption = (*pk.Decryption)(nil)

View File

@@ -1,3 +1,9 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 !goolm
package olm
@@ -18,14 +24,17 @@ import (
"maunium.net/go/mautrix/id"
)
// PkSigning stores a key pair for signing messages.
type PkSigning struct {
// LibOlmPKSigning stores a key pair for signing messages.
type LibOlmPKSigning struct {
int *C.OlmPkSigning
mem []byte
PublicKey id.Ed25519
Seed []byte
publicKey id.Ed25519
seed []byte
}
// Ensure that LibOlmPKSigning implements PKSigning.
var _ PKSigning = (*LibOlmPKSigning)(nil)
func pkSigningSize() uint {
return uint(C.olm_pk_signing_size())
}
@@ -42,48 +51,57 @@ func pkSigningSignatureLength() uint {
return uint(C.olm_pk_signature_length())
}
func NewBlankPkSigning() *PkSigning {
func newBlankPKSigning() *LibOlmPKSigning {
memory := make([]byte, pkSigningSize())
return &PkSigning{
return &LibOlmPKSigning{
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()
// 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
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) {
// NewPKSigning creates a new LibOlmPKSigning 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)
pk, err := NewPKSigningFromSeed(seed)
return pk, err
}
func (p *LibOlmPKSigning) PublicKey() id.Ed25519 {
return p.publicKey
}
func (p *LibOlmPKSigning) Seed() []byte {
return p.seed
}
// clear clears the underlying memory of a LibOlmPKSigning object.
func (p *LibOlmPKSigning) clear() {
C.olm_clear_pk_signing((*C.OlmPkSigning)(p.int))
}
// Sign creates a signature for the given message using this key.
func (p *PkSigning) Sign(message []byte) ([]byte, error) {
func (p *LibOlmPKSigning) 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() {
@@ -93,7 +111,7 @@ func (p *PkSigning) Sign(message []byte) ([]byte, error) {
}
// SignJSON creates a signature for the given object after encoding it to canonical JSON.
func (p *PkSigning) SignJSON(obj interface{}) (string, error) {
func (p *LibOlmPKSigning) SignJSON(obj interface{}) (string, error) {
objJSON, err := json.Marshal(obj)
if err != nil {
return "", err
@@ -107,12 +125,13 @@ func (p *PkSigning) SignJSON(obj interface{}) (string, error) {
return string(signature), nil
}
// lastError returns the last error that happened in relation to this PkSigning object.
func (p *PkSigning) lastError() error {
// lastError returns the last error that happened in relation to this
// LibOlmPKSigning object.
func (p *LibOlmPKSigning) lastError() error {
return convertError(C.GoString(C.olm_pk_signing_last_error((*C.OlmPkSigning)(p.int))))
}
type PkDecryption struct {
type LibOlmPKDecryption struct {
int *C.OlmPkDecryption
mem []byte
PublicKey []byte
@@ -126,13 +145,13 @@ func pkDecryptionPublicKeySize() uint {
return uint(C.olm_pk_key_length())
}
func NewPkDecryption(privateKey []byte) (*PkDecryption, error) {
func NewPkDecryption(privateKey []byte) (*LibOlmPKDecryption, error) {
memory := make([]byte, pkDecryptionSize())
p := &PkDecryption{
p := &LibOlmPKDecryption{
int: C.olm_pk_decryption(unsafe.Pointer(&memory[0])),
mem: memory,
}
p.Clear()
p.clear()
pubKey := make([]byte, pkDecryptionPublicKeySize())
if C.olm_pk_key_from_private((*C.OlmPkDecryption)(p.int),
@@ -145,7 +164,7 @@ func NewPkDecryption(privateKey []byte) (*PkDecryption, error) {
return p, nil
}
func (p *PkDecryption) Decrypt(ephemeralKey []byte, mac []byte, ciphertext []byte) ([]byte, error) {
func (p *LibOlmPKDecryption) Decrypt(ephemeralKey []byte, mac []byte, ciphertext []byte) ([]byte, error) {
maxPlaintextLength := uint(C.olm_pk_max_plaintext_length((*C.OlmPkDecryption)(p.int), C.size_t(len(ciphertext))))
plaintext := make([]byte, maxPlaintextLength)
@@ -162,11 +181,12 @@ func (p *PkDecryption) Decrypt(ephemeralKey []byte, mac []byte, ciphertext []byt
}
// Clear clears the underlying memory of a PkDecryption object.
func (p *PkDecryption) Clear() {
func (p *LibOlmPKDecryption) clear() {
C.olm_clear_pk_decryption((*C.OlmPkDecryption)(p.int))
}
// lastError returns the last error that happened in relation to this PkDecryption object.
func (p *PkDecryption) lastError() error {
// lastError returns the last error that happened in relation to this
// LibOlmPKDecryption object.
func (p *LibOlmPKDecryption) lastError() error {
return convertError(C.GoString(C.olm_pk_decryption_last_error((*C.OlmPkDecryption)(p.int))))
}

View File

@@ -1,146 +0,0 @@
//go:build !goolm
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"
"go.mau.fi/util/exgjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/id"
)
// 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) {
var err error
objJSON, ok := obj.(json.RawMessage)
if !ok {
objJSON, err = json.Marshal(obj)
if err != nil {
return false, err
}
}
sig := gjson.GetBytes(objJSON, exgjson.Path("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

@@ -1,92 +0,0 @@
//go:build goolm
package olm
import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"go.mau.fi/util/exgjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/goolm/utilities"
"maunium.net/go/mautrix/id"
)
// Utility stores the necessary state to perform hash and signature
// verification operations.
type Utility struct{}
// Clear clears the memory used to back this utility.
func (u *Utility) Clear() error {
return nil
}
// NewUtility creates a new utility.
func NewUtility() *Utility {
return &Utility{}
}
// 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)
}
hash := sha256.Sum256([]byte(input))
return base64.RawStdEncoding.EncodeToString(hash[:])
}
// 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
}
return utilities.VerifySignature([]byte(message), key, []byte(signature))
}
// 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) {
var err error
objJSON, ok := obj.(json.RawMessage)
if !ok {
objJSON, err = json.Marshal(obj)
if err != nil {
return false, err
}
}
sig := gjson.GetBytes(objJSON, exgjson.Path("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

@@ -1,142 +0,0 @@
//go:build !nosas && !goolm
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
}

View File

@@ -1,23 +0,0 @@
//go:build !nosas && goolm
package olm
import (
"maunium.net/go/mautrix/crypto/goolm/sas"
)
// SAS stores an Olm Short Authentication String (SAS) object.
type SAS struct {
sas.SAS
}
// NewSAS creates a new SAS object.
func NewSAS() *SAS {
newSAS, err := sas.New()
if err != nil {
panic(err)
}
return &SAS{
SAS: *newSAS,
}
}

30
vendor/maunium.net/go/mautrix/crypto/pkcs7/pkcs7.go generated vendored Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 pkcs7
import "bytes"
// Pad implements PKCS#7 padding as defined in [RFC2315]. It pads the plaintext
// to the given blockSize in the range [1, 255]. This is normally used in
// AES-CBC encryption.
//
// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt
func Pad(plaintext []byte, blockSize int) []byte {
padding := blockSize - len(plaintext)%blockSize
return append(plaintext, bytes.Repeat([]byte{byte(padding)}, padding)...)
}
// Unpad implements PKCS#7 unpadding as defined in [RFC2315]. It unpads the
// plaintext by reading the padding amount from the last byte of the plaintext.
// This is normally used in AES-CBC decryption.
//
// [RFC2315]: https://www.ietf.org/rfc/rfc2315.txt
func Unpad(plaintext []byte) []byte {
length := len(plaintext)
unpadding := int(plaintext[length-1])
return plaintext[:length-unpadding]
}

View File

@@ -105,10 +105,11 @@ type InboundGroupSession struct {
ForwardingChains []string
RatchetSafety RatchetSafety
ReceivedAt time.Time
MaxAge int64
MaxMessages int
IsScheduled bool
ReceivedAt time.Time
MaxAge int64
MaxMessages int
IsScheduled bool
KeyBackupVersion id.KeyBackupVersion
id id.SessionID
}

191
vendor/maunium.net/go/mautrix/crypto/sharing.go generated vendored Normal file
View File

@@ -0,0 +1,191 @@
// Copyright (c) 2024 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 (
"context"
"time"
"go.mau.fi/util/random"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
// Callback function to process a received secret.
//
// Returning true or an error will immediately return from the wait loop, returning false will continue waiting for new responses.
type SecretReceiverFunc func(string) (bool, error)
func (mach *OlmMachine) GetOrRequestSecret(ctx context.Context, name id.Secret, receiver SecretReceiverFunc, timeout time.Duration) (err error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// always offer our stored secret first, if any
secret, err := mach.CryptoStore.GetSecret(ctx, name)
if err != nil {
return err
} else if secret != "" {
if ok, err := receiver(secret); ok || err != nil {
return err
}
}
requestID, secretChan := random.String(64), make(chan string, 5)
mach.secretLock.Lock()
mach.secretListeners[requestID] = secretChan
mach.secretLock.Unlock()
defer func() {
mach.secretLock.Lock()
delete(mach.secretListeners, requestID)
mach.secretLock.Unlock()
}()
// request secret from any device
err = mach.sendToOneDevice(ctx, mach.Client.UserID, id.DeviceID("*"), event.ToDeviceSecretRequest, &event.SecretRequestEventContent{
Action: event.SecretRequestRequest,
RequestID: requestID,
Name: name,
RequestingDeviceID: mach.Client.DeviceID,
})
if err != nil {
return
}
// best effort cancel request from all devices when returning
defer func() {
go mach.sendToOneDevice(context.Background(), mach.Client.UserID, id.DeviceID("*"), event.ToDeviceSecretRequest, &event.SecretRequestEventContent{
Action: event.SecretRequestCancellation,
RequestID: requestID,
RequestingDeviceID: mach.Client.DeviceID,
})
}()
for {
select {
case <-ctx.Done():
return ctx.Err()
case secret = <-secretChan:
if ok, err := receiver(secret); err != nil {
return err
} else if ok {
return mach.CryptoStore.PutSecret(ctx, name, secret)
}
}
}
}
func (mach *OlmMachine) HandleSecretRequest(ctx context.Context, userID id.UserID, content *event.SecretRequestEventContent) {
log := mach.machOrContextLog(ctx).With().
Stringer("user_id", userID).
Stringer("requesting_device_id", content.RequestingDeviceID).
Stringer("action", content.Action).
Str("request_id", content.RequestID).
Stringer("secret", content.Name).
Logger()
log.Trace().Msg("Handling secret request")
if content.Action == event.SecretRequestCancellation {
log.Trace().Msg("Secret request cancellation is unimplemented, ignoring")
return
} else if content.Action != event.SecretRequestRequest {
log.Warn().Msg("Ignoring unknown secret request action")
return
}
// immediately ignore requests from other users
if userID != mach.Client.UserID || content.RequestingDeviceID == "" {
log.Debug().Msg("Secret request was not from our own device, ignoring")
return
}
if content.RequestingDeviceID == mach.Client.DeviceID {
log.Debug().Msg("Secret request was from this device, ignoring")
return
}
keys, err := mach.CryptoStore.GetCrossSigningKeys(ctx, mach.Client.UserID)
if err != nil {
log.Err(err).Msg("Failed to get cross signing keys from crypto store")
return
}
crossSigningKey, ok := keys[id.XSUsageSelfSigning]
if !ok {
log.Warn().Msg("Couldn't find self signing key to verify requesting device")
return
}
device, err := mach.GetOrFetchDevice(ctx, mach.Client.UserID, content.RequestingDeviceID)
if err != nil {
log.Err(err).Msg("Failed to get or fetch requesting device")
return
}
verified, err := mach.CryptoStore.IsKeySignedBy(ctx, mach.Client.UserID, device.SigningKey, mach.Client.UserID, crossSigningKey.Key)
if err != nil {
log.Err(err).Msg("Failed to check if requesting device is verified")
return
}
if !verified {
log.Warn().Msg("Requesting device is not verified, ignoring request")
return
}
secret, err := mach.CryptoStore.GetSecret(ctx, content.Name)
if err != nil {
log.Err(err).Msg("Failed to get secret from store")
return
} else if secret != "" {
log.Debug().Msg("Responding to secret request")
mach.SendEncryptedToDevice(ctx, device, event.ToDeviceSecretSend, event.Content{
Parsed: event.SecretSendEventContent{
RequestID: content.RequestID,
Secret: secret,
},
})
} else {
log.Debug().Msg("No stored secret found, secret request ignored")
}
}
func (mach *OlmMachine) receiveSecret(ctx context.Context, evt *DecryptedOlmEvent, content *event.SecretSendEventContent) {
log := mach.machOrContextLog(ctx).With().
Stringer("sender", evt.Sender).
Stringer("sender_device", evt.SenderDevice).
Str("request_id", content.RequestID).
Logger()
log.Trace().Msg("Handling secret send request")
// immediately ignore secrets from other users
if evt.Sender != mach.Client.UserID {
log.Warn().Msg("Secret send was not from our own device")
return
} else if content.Secret == "" {
log.Warn().Msg("We were sent an empty secret")
return
}
mach.secretLock.Lock()
secretChan := mach.secretListeners[content.RequestID]
mach.secretLock.Unlock()
if secretChan == nil {
log.Warn().Msg("We were sent a secret we didn't request")
return
}
// secret channel is buffered and we don't want to block
// at worst we drop _some_ of the responses
select {
case secretChan <- content.Secret:
default:
}
}

View File

@@ -0,0 +1,94 @@
// Copyright (c) 2024 Sumner Evans
//
// 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 signatures
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"go.mau.fi/util/exgjson"
"maunium.net/go/mautrix/crypto/canonicaljson"
"maunium.net/go/mautrix/crypto/goolm/crypto"
"maunium.net/go/mautrix/id"
)
var (
ErrEmptyInput = errors.New("empty input")
ErrSignatureNotFound = errors.New("input JSON doesn't contain signature from specified device")
)
// Signatures represents a set of signatures for some data from multiple users
// and keys.
type Signatures map[id.UserID]map[id.KeyID]string
// NewSingleSignature creates a new [Signatures] object with a single
// signature.
func NewSingleSignature(userID id.UserID, algorithm id.KeyAlgorithm, keyID string, signature string) Signatures {
return Signatures{
userID: {
id.NewKeyID(algorithm, keyID): signature,
},
}
}
// VerifySignature verifies an Ed25519 signature.
func VerifySignature(message []byte, key id.Ed25519, signature []byte) (ok bool, err error) {
if len(message) == 0 || len(key) == 0 || len(signature) == 0 {
return false, ErrEmptyInput
}
keyDecoded, err := base64.RawStdEncoding.DecodeString(key.String())
if err != nil {
return false, err
}
publicKey := crypto.Ed25519PublicKey(keyDecoded)
return publicKey.Verify(message, signature), nil
}
// VerifySignatureJSON verifies the signature in the given JSON object "obj"
// as described in [Appendix 3] of the Matrix Spec.
//
// This function is a wrapper over [Utility.VerifySignatureJSON] that creates
// and destroys the [Utility] object transparently.
//
// If the "obj" is not already a [json.RawMessage], it will re-encoded as JSON
// for the verification, so "json" tags will be honored.
//
// [Appendix 3]: https://spec.matrix.org/v1.9/appendices/#signing-json
func VerifySignatureJSON(obj any, userID id.UserID, keyName string, key id.Ed25519) (bool, error) {
var err error
objJSON, ok := obj.(json.RawMessage)
if !ok {
objJSON, err = json.Marshal(obj)
if err != nil {
return false, err
}
}
sig := gjson.GetBytes(objJSON, exgjson.Path("signatures", string(userID), fmt.Sprintf("ed25519:%s", keyName)))
if !sig.Exists() || sig.Type != gjson.String {
return false, ErrSignatureNotFound
}
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 := canonicaljson.CanonicalJSONAssumeValid(objJSON)
sigBytes, err := base64.RawStdEncoding.DecodeString(sig.Str)
if err != nil {
return false, err
}
return VerifySignature(objJSONString, key, sigBytes)
}

View File

@@ -21,6 +21,7 @@ import (
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto/goolm/cipher"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/crypto/sql_store_upgrade"
"maunium.net/go/mautrix/event"
@@ -124,20 +125,21 @@ func (store *SQLCryptoStore) PutAccount(ctx context.Context, account *OlmAccount
store.Account = account
bytes := account.Internal.Pickle(store.PickleKey)
_, err := store.DB.Exec(ctx, `
INSERT INTO crypto_account (device_id, shared, sync_token, account, account_id) VALUES ($1, $2, $3, $4, $5)
INSERT INTO crypto_account (device_id, shared, sync_token, account, account_id, key_backup_version) VALUES ($1, $2, $3, $4, $5, $6)
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)
account=excluded.account, account_id=excluded.account_id,
key_backup_version=excluded.key_backup_version
`, store.DeviceID, account.Shared, store.SyncToken, bytes, store.AccountID, account.KeyBackupVersion)
return err
}
// GetAccount retrieves an OlmAccount from the database.
func (store *SQLCryptoStore) GetAccount(ctx context.Context) (*OlmAccount, error) {
if store.Account == nil {
row := store.DB.QueryRow(ctx, "SELECT shared, sync_token, account FROM crypto_account WHERE account_id=$1", store.AccountID)
row := store.DB.QueryRow(ctx, "SELECT shared, sync_token, account, key_backup_version 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)
err := row.Scan(&acc.Shared, &store.SyncToken, &accountBytes, &acc.KeyBackupVersion)
if err == sql.ErrNoRows {
return nil, nil
} else if err != nil {
@@ -284,17 +286,18 @@ func (store *SQLCryptoStore) PutGroupSession(ctx context.Context, roomID id.Room
_, err = store.DB.Exec(ctx, `
INSERT INTO crypto_megolm_inbound_session (
session_id, sender_key, signing_key, room_id, session, forwarding_chains,
ratchet_safety, received_at, max_age, max_messages, is_scheduled, account_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, account_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
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,
ratchet_safety=excluded.ratchet_safety, received_at=excluded.received_at,
max_age=excluded.max_age, max_messages=excluded.max_messages, is_scheduled=excluded.is_scheduled
max_age=excluded.max_age, max_messages=excluded.max_messages, is_scheduled=excluded.is_scheduled,
key_backup_version=excluded.key_backup_version
`,
sessionID, senderKey, session.SigningKey, roomID, sessionBytes, forwardingChains,
ratchetSafety, datePtr(session.ReceivedAt), intishPtr(session.MaxAge), intishPtr(session.MaxMessages),
session.IsScheduled, store.AccountID,
session.IsScheduled, session.KeyBackupVersion, store.AccountID,
)
return err
}
@@ -306,12 +309,13 @@ func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.Room
var receivedAt sql.NullTime
var maxAge, maxMessages sql.NullInt64
var isScheduled bool
var version id.KeyBackupVersion
err := store.DB.QueryRow(ctx, `
SELECT sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled
SELECT sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session
WHERE room_id=$1 AND (sender_key=$2 OR $2 = '') AND session_id=$3 AND account_id=$4`,
roomID, senderKey, sessionID, store.AccountID,
).Scan(&senderKeyDB, &signingKey, &sessionBytes, &forwardingChains, &withheldCode, &withheldReason, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled)
).Scan(&senderKeyDB, &signingKey, &sessionBytes, &forwardingChains, &withheldCode, &withheldReason, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
} else if err != nil {
@@ -341,6 +345,7 @@ func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.Room
MaxAge: maxAge.Int64,
MaxMessages: int(maxMessages.Int64),
IsScheduled: isScheduled,
KeyBackupVersion: version,
}, nil
}
@@ -468,7 +473,8 @@ func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*In
var receivedAt sql.NullTime
var maxAge, maxMessages sql.NullInt64
var isScheduled bool
err := rows.Scan(&roomID, &senderKey, &signingKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled)
var version id.KeyBackupVersion
err := rows.Scan(&roomID, &senderKey, &signingKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version)
if err != nil {
return nil, err
}
@@ -484,31 +490,35 @@ func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*In
MaxAge: maxAge.Int64,
MaxMessages: int(maxMessages.Int64),
IsScheduled: isScheduled,
KeyBackupVersion: version,
}, nil
}
func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID id.RoomID) ([]*InboundGroupSession, error) {
func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID id.RoomID) dbutil.RowIter[*InboundGroupSession] {
rows, err := store.DB.Query(ctx, `
SELECT room_id, sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled
SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session WHERE room_id=$1 AND account_id=$2 AND session IS NOT NULL`,
roomID, store.AccountID,
)
if err != nil {
return nil, err
}
return dbutil.NewRowIter(rows, store.scanInboundGroupSession).AsList()
return dbutil.NewRowIterWithError(rows, store.scanInboundGroupSession, err)
}
func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) ([]*InboundGroupSession, error) {
func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) dbutil.RowIter[*InboundGroupSession] {
rows, err := store.DB.Query(ctx, `
SELECT room_id, sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled
FROM crypto_megolm_inbound_session WHERE account_id=$2 AND session IS NOT NULL`,
SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session WHERE account_id=$1 AND session IS NOT NULL`,
store.AccountID,
)
if err != nil {
return nil, err
}
return dbutil.NewRowIter(rows, store.scanInboundGroupSession).AsList()
return dbutil.NewRowIterWithError(rows, store.scanInboundGroupSession, err)
}
func (store *SQLCryptoStore) GetGroupSessionsWithoutKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession] {
rows, err := store.DB.Query(ctx, `
SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version
FROM crypto_megolm_inbound_session WHERE account_id=$1 AND session IS NOT NULL AND key_backup_version != $2`,
store.AccountID, version,
)
return dbutil.NewRowIterWithError(rows, store.scanInboundGroupSession, err)
}
// AddOutboundGroupSession stores an outbound Megolm session, along with the information about the room and involved devices.
@@ -568,6 +578,20 @@ func (store *SQLCryptoStore) RemoveOutboundGroupSession(ctx context.Context, roo
return err
}
func (store *SQLCryptoStore) MarkOutboundGroupSessionShared(ctx context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) error {
_, err := store.DB.Exec(ctx, "INSERT INTO crypto_megolm_outbound_session_shared (user_id, identity_key, session_id) VALUES ($1, $2, $3)", userID, identityKey, sessionID)
return err
}
func (store *SQLCryptoStore) IsOutboundGroupSessionShared(ctx context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) (shared bool, err error) {
err = store.DB.QueryRow(ctx, `SELECT TRUE FROM crypto_megolm_outbound_session_shared WHERE user_id=$1 AND identity_key=$2 AND session_id=$3`,
userID, identityKey, sessionID).Scan(&shared)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
return
}
// 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(ctx context.Context, senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
@@ -845,3 +869,32 @@ func (store *SQLCryptoStore) DropSignaturesByKey(ctx context.Context, userID id.
}
return count, nil
}
func (store *SQLCryptoStore) PutSecret(ctx context.Context, name id.Secret, value string) error {
bytes, err := cipher.Pickle(store.PickleKey, []byte(value))
if err != nil {
return err
}
_, err = store.DB.Exec(ctx, `
INSERT INTO crypto_secrets (name, secret) VALUES ($1, $2)
ON CONFLICT (name) DO UPDATE SET secret=excluded.secret
`, name, bytes)
return err
}
func (store *SQLCryptoStore) GetSecret(ctx context.Context, name id.Secret) (value string, err error) {
var bytes []byte
err = store.DB.QueryRow(ctx, `SELECT secret FROM crypto_secrets WHERE name=$1`, name).Scan(&bytes)
if errors.Is(err, sql.ErrNoRows) {
return "", nil
} else if err != nil {
return "", err
}
bytes, err = cipher.Unpickle(store.PickleKey, bytes)
return string(bytes), err
}
func (store *SQLCryptoStore) DeleteSecret(ctx context.Context, name id.Secret) (err error) {
_, err = store.DB.Exec(ctx, "DELETE FROM crypto_secrets WHERE name=$1", name)
return
}

View File

@@ -1,10 +1,11 @@
-- v0 -> v11: Latest revision
-- v0 -> v14 (compatible with v9+): 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
account_id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
shared BOOLEAN NOT NULL,
sync_token TEXT NOT NULL,
account bytea NOT NULL,
key_backup_version TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS crypto_message_index (
@@ -44,20 +45,21 @@ CREATE TABLE IF NOT EXISTS crypto_olm_session (
);
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,
ratchet_safety jsonb,
received_at timestamp,
max_age BIGINT,
max_messages INTEGER,
is_scheduled BOOLEAN NOT NULL DEFAULT false,
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,
ratchet_safety jsonb,
received_at timestamp,
max_age BIGINT,
max_messages INTEGER,
is_scheduled BOOLEAN NOT NULL DEFAULT false,
key_backup_version TEXT NOT NULL DEFAULT '',
PRIMARY KEY (account_id, session_id)
);
@@ -75,6 +77,14 @@ CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session (
PRIMARY KEY (account_id, room_id)
);
CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session_shared (
user_id TEXT NOT NULL,
identity_key CHAR(43) NOT NULL,
session_id CHAR(43) NOT NULL,
PRIMARY KEY (user_id, identity_key, session_id)
);
CREATE TABLE IF NOT EXISTS crypto_cross_signing_keys (
user_id TEXT,
usage TEXT,
@@ -93,3 +103,8 @@ CREATE TABLE IF NOT EXISTS crypto_cross_signing_signatures (
signature CHAR(88) NOT NULL,
PRIMARY KEY (signed_user_id, signed_key, signer_user_id, signer_key)
);
CREATE TABLE IF NOT EXISTS crypto_secrets (
name TEXT PRIMARY KEY NOT NULL,
secret bytea NOT NULL
);

View File

@@ -0,0 +1,5 @@
-- v12 (compatible with v9+): Add crypto_secrets table
CREATE TABLE IF NOT EXISTS crypto_secrets (
name TEXT PRIMARY KEY NOT NULL,
secret bytea NOT NULL
);

View File

@@ -0,0 +1,9 @@
-- v13 (compatible with v9+): Add crypto_megolm_outbound_session_shared table
CREATE TABLE IF NOT EXISTS crypto_megolm_outbound_session_shared (
user_id TEXT NOT NULL,
identity_key CHAR(43) NOT NULL,
session_id CHAR(43) NOT NULL,
PRIMARY KEY (user_id, identity_key, session_id)
);

View File

@@ -0,0 +1,4 @@
-- v14 (compatible with v9+): Add key_backup_version column to account and igs
ALTER TABLE crypto_account ADD COLUMN key_backup_version TEXT NOT NULL DEFAULT '';
ALTER TABLE crypto_megolm_inbound_session ADD COLUMN key_backup_version TEXT NOT NULL DEFAULT '';

View File

@@ -8,6 +8,7 @@ package ssss
import (
"context"
"errors"
"fmt"
"maunium.net/go/mautrix"
@@ -34,7 +35,7 @@ func (mach *Machine) GetDefaultKeyID(ctx context.Context) (string, error) {
var data DefaultSecretStorageKeyContent
err := mach.Client.GetAccountData(ctx, event.AccountDataSecretStorageDefaultKey.Type, &data)
if err != nil {
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_NOT_FOUND" {
if httpErr, ok := err.(mautrix.HTTPError); ok && errors.Is(httpErr.RespError, mautrix.MNotFound) {
return "", ErrNoDefaultKeyAccountDataEvent
}
return "", fmt.Errorf("failed to get default key account data from server: %w", err)

View File

@@ -67,12 +67,12 @@ func NewKey(passphrase string) (*Key, error) {
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.IV = base64.RawStdEncoding.EncodeToString(ivBytes[:])
keyData.MAC = keyData.calculateHash(ssssKey)
return &Key{
Key: ssssKey,
ID: base64.StdEncoding.EncodeToString(keyIDBytes),
ID: base64.RawStdEncoding.EncodeToString(keyIDBytes),
Metadata: &keyData,
}, nil
}
@@ -87,13 +87,18 @@ 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)
// For some reason, keys in secret storage are base64 encoded before encrypting.
// Even more confusingly, it's a part of each key type's spec rather than the secrets spec.
// Key backup (`m.megolm_backup.v1`): https://spec.matrix.org/v1.9/client-server-api/#recovery-key
// Cross-signing (master, etc): https://spec.matrix.org/v1.9/client-server-api/#cross-signing (the very last paragraph)
// It's also not clear whether unpadded base64 is the right option, but assuming it is because everything else is unpadded.
payload := make([]byte, base64.RawStdEncoding.EncodedLen(len(data)))
base64.RawStdEncoding.Encode(payload, data)
utils.XorA256CTR(payload, aesKey, iv)
return EncryptedKeyData{
Ciphertext: base64.StdEncoding.EncodeToString(payload),
IV: base64.StdEncoding.EncodeToString(iv[:]),
Ciphertext: base64.RawStdEncoding.EncodeToString(payload),
IV: base64.RawStdEncoding.EncodeToString(iv[:]),
MAC: utils.HMACSHA256B64(payload, hmacKey),
}
}
@@ -101,10 +106,10 @@ func (key *Key) Encrypt(eventType string, data []byte) EncryptedKeyData {
// 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)
decodedIV, _ := base64.RawStdEncoding.DecodeString(strings.TrimRight(data.IV, "="))
copy(ivBytes[:], decodedIV)
payload, err := base64.StdEncoding.DecodeString(data.Ciphertext)
payload, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(data.Ciphertext, "="))
if err != nil {
return nil, err
}
@@ -114,11 +119,11 @@ func (key *Key) Decrypt(eventType string, data EncryptedKeyData) ([]byte, error)
// 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, "=", "") {
if strings.TrimRight(data.MAC, "=") != calcMac {
return nil, ErrKeyDataMACMismatch
}
utils.XorA256CTR(payload, aesKey, ivBytes)
decryptedDecoded, err := base64.StdEncoding.DecodeString(string(payload))
decryptedDecoded, err := base64.RawStdEncoding.DecodeString(strings.TrimRight(string(payload), "="))
return decryptedDecoded, err
}

View File

@@ -19,9 +19,14 @@ import (
type KeyMetadata struct {
id string
Algorithm Algorithm `json:"algorithm"`
IV string `json:"iv"`
MAC string `json:"mac"`
Name string `json:"name"`
Algorithm Algorithm `json:"algorithm"`
// Note: as per https://spec.matrix.org/v1.9/client-server-api/#msecret_storagev1aes-hmac-sha2,
// these fields are "maybe padded" base64, so both unpadded and padded values must be supported.
IV string `json:"iv"`
MAC string `json:"mac"`
Passphrase *PassphraseMetadata `json:"passphrase,omitempty"`
}
@@ -59,7 +64,7 @@ func (kd *KeyMetadata) VerifyRecoveryKey(recoverKey string) (*Key, error) {
// 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), "=", "")
return strings.TrimRight(kd.MAC, "=") == kd.calculateHash(key)
}
// calculateHash calculates the hash used for checking if the key is entered correctly as described
@@ -68,7 +73,7 @@ func (kd *KeyMetadata) calculateHash(key []byte) string {
aesKey, hmacKey := utils.DeriveKeysSHA256(key, "")
var ivBytes [utils.AESCTRIVLength]byte
_, _ = base64.StdEncoding.Decode(ivBytes[:], []byte(kd.IV))
_, _ = base64.RawStdEncoding.Decode(ivBytes[:], []byte(strings.TrimRight(kd.IV, "=")))
cipher := utils.XorA256CTR(make([]byte, utils.AESCTRKeyLength), aesKey, ivBytes)

View File

@@ -47,6 +47,8 @@ const (
)
type EncryptedKeyData struct {
// Note: as per https://spec.matrix.org/v1.9/client-server-api/#msecret_storagev1aes-hmac-sha2-1,
// these fields are "maybe padded" base64, so both unpadded and padded values must be supported.
Ciphertext string `json:"ciphertext"`
IV string `json:"iv"`
MAC string `json:"mac"`
@@ -72,4 +74,5 @@ func init() {
event.TypeMap[event.AccountDataCrossSigningUser] = encryptedContent
event.TypeMap[event.AccountDataSecretStorageDefaultKey] = reflect.TypeOf(&DefaultSecretStorageKeyContent{})
event.TypeMap[event.AccountDataSecretStorageKey] = reflect.TypeOf(&KeyMetadata{})
event.TypeMap[event.AccountDataMegolmBackupKey] = reflect.TypeOf(&EncryptedAccountDataEventContent{})
}

View File

@@ -12,6 +12,8 @@ import (
"sort"
"sync"
"go.mau.fi/util/dbutil"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -68,10 +70,12 @@ type Store interface {
// 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(context.Context, id.RoomID) ([]*InboundGroupSession, error)
GetGroupSessionsForRoom(context.Context, id.RoomID) dbutil.RowIter[*InboundGroupSession]
// 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(context.Context) ([]*InboundGroupSession, error)
GetAllGroupSessions(context.Context) dbutil.RowIter[*InboundGroupSession]
// GetGroupSessionsWithoutKeyBackupVersion gets all the inbound Megolm sessions in the store that do not match given key backup version.
GetGroupSessionsWithoutKeyBackupVersion(context.Context, id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession]
// AddOutboundGroupSession inserts the given outbound Megolm session into the store.
//
@@ -84,6 +88,10 @@ type Store interface {
GetOutboundGroupSession(context.Context, id.RoomID) (*OutboundGroupSession, error)
// RemoveOutboundGroupSession removes the stored outbound Megolm session for the given room ID.
RemoveOutboundGroupSession(context.Context, id.RoomID) error
// MarkOutboutGroupSessionShared flags that the currently known device has been shared the keys for the specified session.
MarkOutboundGroupSessionShared(context.Context, id.UserID, id.IdentityKey, id.SessionID) error
// IsOutboutGroupSessionShared checks if the specified session has been shared with the device.
IsOutboundGroupSessionShared(context.Context, id.UserID, id.IdentityKey, id.SessionID) (bool, error)
// ValidateMessageIndex validates that the given message details aren't from a replay attack.
//
@@ -123,6 +131,13 @@ type Store interface {
IsKeySignedBy(ctx context.Context, 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(context.Context, id.UserID, id.Ed25519) (int64, error)
// PutSecret stores a named secret, replacing it if it exists already.
PutSecret(context.Context, id.Secret, string) error
// GetSecret returns a named secret.
GetSecret(context.Context, id.Secret) (string, error)
// DeleteSecret removes a named secret.
DeleteSecret(context.Context, id.Secret) error
}
type messageIndexKey struct {
@@ -148,11 +163,13 @@ type MemoryStore struct {
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
SharedGroupSessions map[id.UserID]map[id.IdentityKey]map[id.SessionID]struct{}
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
OutdatedUsers map[id.UserID]struct{}
Secrets map[id.Secret]string
}
var _ Store = (*MemoryStore)(nil)
@@ -168,11 +185,13 @@ func NewMemoryStore(saveCallback func() error) *MemoryStore {
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),
SharedGroupSessions: make(map[id.UserID]map[id.IdentityKey]map[id.SessionID]struct{}),
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),
OutdatedUsers: make(map[id.UserID]struct{}),
Secrets: make(map[id.Secret]string),
}
}
@@ -361,12 +380,12 @@ func (gs *MemoryStore) GetWithheldGroupSession(_ context.Context, roomID id.Room
return session, nil
}
func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.RoomID) ([]*InboundGroupSession, error) {
func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.RoomID) dbutil.RowIter[*InboundGroupSession] {
gs.lock.Lock()
defer gs.lock.Unlock()
room, ok := gs.GroupSessions[roomID]
if !ok {
return []*InboundGroupSession{}, nil
return nil
}
var result []*InboundGroupSession
for _, sessions := range room {
@@ -374,10 +393,10 @@ func (gs *MemoryStore) GetGroupSessionsForRoom(_ context.Context, roomID id.Room
result = append(result, session)
}
}
return result, nil
return dbutil.NewSliceIter[*InboundGroupSession](result)
}
func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) ([]*InboundGroupSession, error) {
func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) dbutil.RowIter[*InboundGroupSession] {
gs.lock.Lock()
var result []*InboundGroupSession
for _, room := range gs.GroupSessions {
@@ -388,7 +407,23 @@ func (gs *MemoryStore) GetAllGroupSessions(_ context.Context) ([]*InboundGroupSe
}
}
gs.lock.Unlock()
return result, nil
return dbutil.NewSliceIter[*InboundGroupSession](result)
}
func (gs *MemoryStore) GetGroupSessionsWithoutKeyBackupVersion(_ context.Context, version id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession] {
gs.lock.Lock()
var result []*InboundGroupSession
for _, room := range gs.GroupSessions {
for _, sessions := range room {
for _, session := range sessions {
if session.KeyBackupVersion != version {
result = append(result, session)
}
}
}
}
gs.lock.Unlock()
return dbutil.NewSliceIter[*InboundGroupSession](result)
}
func (gs *MemoryStore) AddOutboundGroupSession(_ context.Context, session *OutboundGroupSession) error {
@@ -426,6 +461,41 @@ func (gs *MemoryStore) RemoveOutboundGroupSession(_ context.Context, roomID id.R
return nil
}
func (gs *MemoryStore) MarkOutboundGroupSessionShared(_ context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) error {
gs.lock.Lock()
if _, ok := gs.SharedGroupSessions[userID]; !ok {
gs.SharedGroupSessions[userID] = make(map[id.IdentityKey]map[id.SessionID]struct{})
}
identities := gs.SharedGroupSessions[userID]
if _, ok := identities[identityKey]; !ok {
identities[identityKey] = make(map[id.SessionID]struct{})
}
identities[identityKey][sessionID] = struct{}{}
gs.lock.Unlock()
return nil
}
func (gs *MemoryStore) IsOutboundGroupSessionShared(_ context.Context, userID id.UserID, identityKey id.IdentityKey, sessionID id.SessionID) (isShared bool, err error) {
gs.lock.Lock()
defer gs.lock.Unlock()
if _, ok := gs.SharedGroupSessions[userID]; !ok {
return
}
identities := gs.SharedGroupSessions[userID]
if _, ok := identities[identityKey]; !ok {
return
}
_, isShared = identities[identityKey][sessionID]
return
}
func (gs *MemoryStore) ValidateMessageIndex(_ context.Context, senderKey id.SenderKey, sessionID id.SessionID, eventID id.EventID, index uint, timestamp int64) (bool, error) {
gs.lock.Lock()
defer gs.lock.Unlock()
@@ -645,3 +715,24 @@ func (gs *MemoryStore) DropSignaturesByKey(_ context.Context, userID id.UserID,
gs.lock.RUnlock()
return count, nil
}
func (gs *MemoryStore) PutSecret(_ context.Context, name id.Secret, value string) error {
gs.lock.Lock()
gs.Secrets[name] = value
gs.lock.Unlock()
return nil
}
func (gs *MemoryStore) GetSecret(_ context.Context, name id.Secret) (value string, _ error) {
gs.lock.RLock()
value = gs.Secrets[name]
gs.lock.RUnlock()
return
}
func (gs *MemoryStore) DeleteSecret(_ context.Context, name id.Secret) error {
gs.lock.Lock()
delete(gs.Secrets, name)
gs.lock.Unlock()
return nil
}

View File

@@ -124,9 +124,9 @@ func EncodeBase58RecoveryKey(key []byte) string {
return spacedKey
}
// HMACSHA256B64 calculates the base64 of the SHA256 hmac of the input with the given key.
// HMACSHA256B64 calculates the unpadded 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))
return base64.RawStdEncoding.EncodeToString(h.Sum(nil))
}

View File

@@ -1,801 +0,0 @@
// 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 (
"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(ctx context.Context, userID id.UserID, deviceID id.DeviceID, eventType event.Type, content interface{}) error {
_, err := mach.Client.SendToDevice(ctx, 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().Msgf("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().Msgf("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(ctx context.Context, transactionID string, userID id.UserID) (*verificationState, error) {
verStateInterface, ok := mach.keyVerificationTransactionState.Load(userID.String() + ":" + transactionID)
if !ok {
_ = mach.SendSASVerificationCancel(ctx, 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(ctx, userID, id.DeviceID("*"), transactionID, reason, event.VerificationCancelUserMismatch)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, 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(ctx context.Context, userID id.UserID, content *event.VerificationStartEventContent, transactionID string, timeout time.Duration, inRoomID id.RoomID) {
mach.Log.Debug().Msgf("Received verification start from %v", content.FromDevice)
otherDevice, err := mach.GetOrFetchDevice(ctx, userID, content.FromDevice)
if err != nil {
mach.Log.Error().Msgf("Could not find device %v of user %v", content.FromDevice, userID)
return
}
warnAndCancel := func(logReason, cancelReason string) {
mach.Log.Warn().Msgf("Canceling verification transaction %v as it %s", transactionID, logReason)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, cancelReason, event.VerificationCancelUnknownMethod)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, 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(ctx, userID, content, otherDevice, transactionID, timeout, inRoomID)
}
}
func (mach *OlmMachine) actuallyStartVerification(ctx context.Context, userID id.UserID, content *event.VerificationStartEventContent, otherDevice *id.Device, transactionID string, timeout time.Duration, inRoomID id.RoomID) {
if inRoomID != "" && transactionID != "" {
verState, err := mach.getTransactionState(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("Failed to get transaction state for in-room verification %s start: %v", transactionID, err)
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Internal state error in gomuks :(", "net.maunium.internal_error")
return
}
mach.timeoutAfter(ctx, verState, transactionID, timeout)
sasMethods := commonSASMethods(verState.hooks, content.ShortAuthenticationString)
err = mach.SendInRoomSASVerificationAccept(ctx, inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods)
if err != nil {
mach.Log.Error().Msgf("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().Msgf("No common SAS methods: %v", content.ShortAuthenticationString)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "No common SAS methods", event.VerificationCancelUnknownMethod)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, 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().Msgf("Transaction %v already exists, canceling", transactionID)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Transaction already exists", event.VerificationCancelUnexpectedMessage)
}
return
}
mach.timeoutAfter(ctx, verState, transactionID, timeout)
var err error
if inRoomID == "" {
err = mach.SendSASVerificationAccept(ctx, userID, content, verState.sas.GetPubkey(), sasMethods)
} else {
err = mach.SendInRoomSASVerificationAccept(ctx, inRoomID, userID, content, transactionID, verState.sas.GetPubkey(), sasMethods)
}
if err != nil {
mach.Log.Error().Msgf("Error accepting SAS verification: %v", err)
}
} else if resp == RejectRequest {
mach.Log.Debug().Msgf("Not accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
var err error
if inRoomID == "" {
err = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
} else {
err = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
}
if err != nil {
mach.Log.Error().Msgf("Error canceling SAS verification: %v", err)
}
} else {
mach.Log.Debug().Msgf("Ignoring SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
}
}
func (mach *OlmMachine) timeoutAfter(ctx context.Context, verState *verificationState, transactionID string, timeout time.Duration) {
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, 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(ctx, verState, transactionID, "Timed out", event.VerificationCancelByTimeout)
mach.Log.Warn().Msgf("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().Msgf("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(ctx context.Context, userID id.UserID, content *event.VerificationAcceptEventContent, transactionID string) {
mach.Log.Debug().Msgf("Received verification accept for transaction %v", transactionID)
verState, err := mach.getTransactionState(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("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().Msgf("Unexpected verification accept message for transaction %v", transactionID)
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, 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().Msgf("Canceling verification transaction %v due to unknown parameter", transactionID)
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, 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(ctx, userID, verState.otherDevice.DeviceID, transactionID, string(key))
} else {
err = mach.SendInRoomSASVerificationKey(ctx, verState.inRoomID, userID, transactionID, string(key))
}
if err != nil {
mach.Log.Error().Msgf("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(ctx context.Context, userID id.UserID, content *event.VerificationKeyEventContent, transactionID string) {
mach.Log.Debug().Msgf("Got verification key for transaction %v: %v", transactionID, content.Key)
verState, err := mach.getTransactionState(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("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().Msgf("Unexpected verification key message for transaction %v", transactionID)
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Unexpected key message", event.VerificationCancelUnexpectedMessage)
return
}
if err := verState.sas.SetTheirKey([]byte(content.Key)); err != nil {
mach.Log.Error().Msgf("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().Msgf("Received commitment: %v Expected: %v", verState.commitment, expectedCommitment)
if expectedCommitment != verState.commitment {
mach.Log.Warn().Msgf("Canceling verification transaction %v due to commitment mismatch", transactionID)
mach.keyVerificationTransactionState.Delete(userID.String() + ":" + transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, 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(ctx, userID, device.DeviceID, transactionID, string(key))
} else {
err = mach.SendInRoomSASVerificationKey(ctx, verState.inRoomID, userID, transactionID, string(key))
}
if err != nil {
mach.Log.Error().Msgf("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().Msgf("Error generating SAS (method %v): %v", sasMethod.Type(), err)
return
}
mach.Log.Debug().Msgf("Generated SAS (%v): %v", sasMethod.Type(), sas)
go func() {
result := verState.hooks.VerifySASMatch(device, sas)
mach.sasCompared(ctx, 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(ctx context.Context, 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(ctx, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas)
} else {
err = mach.SendInRoomSASVerificationMAC(ctx, verState.inRoomID, verState.otherDevice.UserID, verState.otherDevice.DeviceID, transactionID, verState.sas)
}
if err != nil {
mach.Log.Error().Msgf("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(ctx context.Context, userID id.UserID, content *event.VerificationMacEventContent, transactionID string) {
mach.Log.Debug().Msgf("Got MAC for verification %v: %v, MAC for keys: %v", transactionID, content.Mac, content.Keys)
verState, err := mach.getTransactionState(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("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().Msgf("Unexpected MAC message for transaction %v", transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, 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().Msgf("SAS do not match! Canceling transaction %v", transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, 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().Msgf("Error generating MAC to match with received MAC: %v", err)
return
}
mach.Log.Debug().Msgf("Expected %s keys MAC, got %s", expectedKeysMAC, content.Keys)
if content.Keys != expectedKeysMAC {
mach.Log.Warn().Msgf("Canceling verification transaction %v due to mismatched keys MAC", transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Mismatched keys MACs", event.VerificationCancelKeyMismatch)
return
}
mach.Log.Debug().Msgf("Expected %s PK MAC, got %s", expectedPKMAC, content.Mac[keyID])
if content.Mac[keyID] != expectedPKMAC {
mach.Log.Warn().Msgf("Canceling verification transaction %v due to mismatched PK MAC", transactionID)
_ = mach.callbackAndCancelSASVerification(ctx, verState, transactionID, "Mismatched PK MACs", event.VerificationCancelKeyMismatch)
return
}
// we can finally trust this device
device.Trust = id.TrustStateVerified
err = mach.CryptoStore.PutDevice(ctx, device.UserID, device)
if err != nil {
mach.Log.Warn().Msgf("Failed to put device after verifying: %v", err)
}
if mach.CrossSigningKeys != nil {
if device.UserID == mach.Client.UserID {
err := mach.SignOwnDevice(ctx, device)
if err != nil {
mach.Log.Error().Msgf("Failed to cross-sign own device %s: %v", device.DeviceID, err)
} else {
mach.Log.Debug().Msgf("Cross-signed own device %v after SAS verification", device.DeviceID)
}
} else {
masterKey, err := mach.fetchMasterKey(ctx, device, content, verState, transactionID)
if err != nil {
mach.Log.Warn().Msgf("Failed to fetch %s's master key: %v", device.UserID, err)
} else {
if err := mach.SignUser(ctx, device.UserID, masterKey); err != nil {
mach.Log.Error().Msgf("Failed to cross-sign master key of %s: %v", device.UserID, err)
} else {
mach.Log.Debug().Msgf("Cross-signed master key of %v after SAS verification", device.UserID)
}
}
}
} else {
// TODO ask user to unlock cross-signing keys?
mach.Log.Debug().Msgf("Cross-signing keys not cached, not signing %s/%s", device.UserID, device.DeviceID)
}
mach.Log.Debug().Msgf("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().Msgf("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(ctx context.Context, userID id.UserID, content *event.VerificationRequestEventContent, transactionID string, inRoomID id.RoomID) {
mach.Log.Debug().Msgf("Received verification request from %v", content.FromDevice)
otherDevice, err := mach.GetOrFetchDevice(ctx, userID, content.FromDevice)
if err != nil {
mach.Log.Error().Msgf("Could not find device %v of user %v", content.FromDevice, userID)
return
}
if !content.SupportsVerificationMethod(event.VerificationMethodSAS) {
mach.Log.Warn().Msgf("Canceling verification transaction %v as SAS is not supported", transactionID)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Only SAS method is supported", event.VerificationCancelUnknownMethod)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, 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().Msgf("Accepting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
if inRoomID == "" {
_, err = mach.NewSASVerificationWith(ctx, otherDevice, hooks, transactionID, mach.DefaultSASTimeout)
} else {
if err := mach.SendInRoomSASVerificationReady(ctx, inRoomID, transactionID); err != nil {
mach.Log.Error().Msgf("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(ctx, inRoomID, otherDevice, hooks, transactionID, mach.DefaultSASTimeout)
}
}
if err != nil {
mach.Log.Error().Msgf("Error accepting SAS verification request: %v", err)
}
} else if resp == RejectRequest {
mach.Log.Debug().Msgf("Rejecting SAS verification %v from %v of user %v", transactionID, otherDevice.DeviceID, otherDevice.UserID)
if inRoomID == "" {
_ = mach.SendSASVerificationCancel(ctx, otherDevice.UserID, otherDevice.DeviceID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
} else {
_ = mach.SendInRoomSASVerificationCancel(ctx, inRoomID, otherDevice.UserID, transactionID, "Not accepted by user", event.VerificationCancelByUser)
}
} else {
mach.Log.Debug().Msgf("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(ctx context.Context, device *id.Device, hooks VerificationHooks) (string, error) {
return mach.NewSASVerificationWith(ctx, 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(ctx context.Context, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) {
if transactionID == "" {
transactionID = strconv.Itoa(rand.Int())
}
mach.Log.Debug().Msgf("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(ctx, 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(ctx, 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(ctx context.Context, 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().Msgf("User canceled verification transaction %v with reason: %v", transactionID, reason)
mach.keyVerificationTransactionState.Delete(mapKey)
return mach.callbackAndCancelSASVerification(ctx, 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(ctx context.Context, 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(ctx, userID, deviceID, event.ToDeviceVerificationCancel, content)
}
// SendSASVerificationStart is used to manually send the SAS verification start message to another device.
func (mach *OlmMachine) SendSASVerificationStart(ctx context.Context, 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(ctx, 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(ctx context.Context, 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(ctx, 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(ctx, fromUser, startEvent.FromDevice, event.ToDeviceVerificationAccept, content)
}
func (mach *OlmMachine) callbackAndCancelSASVerification(ctx context.Context, verState *verificationState, transactionID, reason string, code event.VerificationCancelCode) error {
go verState.hooks.OnCancel(true, reason, code)
return mach.SendSASVerificationCancel(ctx, 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(ctx context.Context, userID id.UserID, deviceID id.DeviceID, transactionID string, key string) error {
content := &event.VerificationKeyEventContent{
TransactionID: transactionID,
Key: key,
}
return mach.sendToOneDevice(ctx, userID, deviceID, event.ToDeviceVerificationKey, content)
}
// SendSASVerificationMAC is use the MAC of a device's key to the partner device.
func (mach *OlmMachine) SendSASVerificationMAC(ctx context.Context, 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().Msgf("Error generating master key MAC: %v", err)
} else {
mach.Log.Debug().Msgf("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().Msgf("MAC of key %s is: %s", signingKey, pubKeyMac)
mach.Log.Debug().Msgf("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(ctx, 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

@@ -1,334 +0,0 @@
// 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 (
"context"
"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
}
ctx := context.TODO()
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(ctx, evt.Sender, newContent, evt.ID.String(), evt.RoomID)
}
case *event.VerificationStartEventContent:
mach.handleVerificationStart(ctx, evt.Sender, content, content.RelatesTo.EventID.String(), 10*time.Minute, evt.RoomID)
case *event.VerificationReadyEventContent:
mach.handleInRoomVerificationReady(ctx, evt.Sender, evt.RoomID, content, content.RelatesTo.EventID.String())
case *event.VerificationAcceptEventContent:
mach.handleVerificationAccept(ctx, evt.Sender, content, content.RelatesTo.EventID.String())
case *event.VerificationKeyEventContent:
mach.handleVerificationKey(ctx, evt.Sender, content, content.RelatesTo.EventID.String())
case *event.VerificationMacEventContent:
mach.handleVerificationMAC(ctx, 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(ctx context.Context, 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(ctx, roomID, event.InRoomVerificationCancel, content)
if err != nil {
return err
}
_, err = mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, 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(ctx, roomID, event.EventMessage, content)
if err != nil {
return "", err
}
resp, err := mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, 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(ctx, roomID, event.InRoomVerificationReady, content)
if err != nil {
return err
}
_, err = mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, 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(ctx, roomID, event.InRoomVerificationStart, content)
if err != nil {
return nil, err
}
_, err = mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, 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(ctx, 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(ctx, roomID, event.InRoomVerificationAccept, content)
if err != nil {
return err
}
_, err = mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, 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(ctx, roomID, event.InRoomVerificationKey, content)
if err != nil {
return err
}
_, err = mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, 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().Msgf("Error generating master key MAC: %v", err)
} else {
mach.Log.Debug().Msgf("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().Msgf("MAC of key %s is: %s", signingKey, pubKeyMac)
mach.Log.Debug().Msgf("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(ctx, roomID, event.InRoomVerificationMAC, content)
if err != nil {
return err
}
_, err = mach.Client.SendMessageEvent(ctx, 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(ctx context.Context, inRoomID id.RoomID, userID id.UserID, hooks VerificationHooks, timeout time.Duration) (string, error) {
return mach.newInRoomSASVerificationWithInner(ctx, inRoomID, &id.Device{UserID: userID}, hooks, "", timeout)
}
func (mach *OlmMachine) newInRoomSASVerificationWithInner(ctx context.Context, inRoomID id.RoomID, device *id.Device, hooks VerificationHooks, transactionID string, timeout time.Duration) (string, error) {
mach.Log.Debug().Msgf("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(ctx, 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(ctx, 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(ctx, verState, transactionID, timeout)
return transactionID, nil
}
func (mach *OlmMachine) handleInRoomVerificationReady(ctx context.Context, userID id.UserID, roomID id.RoomID, content *event.VerificationReadyEventContent, transactionID string) {
device, err := mach.GetOrFetchDevice(ctx, userID, content.FromDevice)
if err != nil {
mach.Log.Error().Msgf("Error fetching device %v of user %v: %v", content.FromDevice, userID, err)
return
}
verState, err := mach.getTransactionState(ctx, transactionID, userID)
if err != nil {
mach.Log.Error().Msgf("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(ctx, roomID, device, verState.hooks, transactionID, 10*time.Minute)
verState.lock.Unlock()
}
}

View File

@@ -1,201 +0,0 @@
// 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/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
}