mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-15 06:45:51 +01:00
To properly receive and store a requested secret, we usually need to validate it against something like a public key to ensure we got the correct one. This changes the API so that we instead use a callback to receive any incoming secret matching our request but we'll fail when we hit the specified timeout if we never receive anything that is accepted.
191 lines
5.5 KiB
Go
191 lines
5.5 KiB
Go
// 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:
|
|
}
|
|
}
|