mirror of https://github.com/mautrix/go.git
Secret sharing implementation
parent
97d19484a3
commit
94664f1c8a
|
@ -78,6 +78,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.
|
||||
|
@ -119,6 +122,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
|
||||
|
@ -357,6 +361,9 @@ func (mach *OlmMachine) HandleEncryptedEvent(ctx context.Context, evt *event.Eve
|
|||
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")
|
||||
}
|
||||
|
@ -407,6 +414,11 @@ func (mach *OlmMachine) HandleToDeviceEvent(ctx context.Context, evt *event.Even
|
|||
mach.handleVerificationRequest(ctx, evt.Sender, content, content.TransactionID, "")
|
||||
case *event.RoomKeyWithheldEventContent:
|
||||
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")
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
func (mach *OlmMachine) GetOrRequestSecret(ctx context.Context, name id.Secret, timeout time.Duration) (secret string, err error) {
|
||||
secret, err = mach.CryptoStore.GetSecret(ctx, name)
|
||||
if err != nil || secret != "" {
|
||||
return
|
||||
}
|
||||
|
||||
requestID, secretChan := random.String(64), make(chan string, 1)
|
||||
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
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
case <-time.After(timeout):
|
||||
case secret = <-secretChan:
|
||||
}
|
||||
|
||||
if secret != "" {
|
||||
err = mach.CryptoStore.PutSecret(ctx, name, secret)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
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.sendToOneDevice(ctx, mach.Client.UserID, content.RequestingDeviceID, event.ToDeviceSecretRequest, &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
|
||||
}
|
||||
|
||||
select {
|
||||
case secretChan <- content.Secret:
|
||||
default:
|
||||
}
|
||||
|
||||
// best effort cancel this for all other targets
|
||||
go func() {
|
||||
mach.sendToOneDevice(ctx, mach.Client.UserID, id.DeviceID("*"), event.ToDeviceSecretRequest, &event.SecretRequestEventContent{
|
||||
Action: event.SecretRequestCancellation,
|
||||
RequestID: content.RequestID,
|
||||
RequestingDeviceID: mach.Client.DeviceID,
|
||||
})
|
||||
}()
|
||||
}
|
|
@ -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"
|
||||
|
@ -845,3 +846,30 @@ 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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
-- v0 -> v11: Latest revision
|
||||
-- v0 -> v12 (compatible with v9+): Latest revision
|
||||
CREATE TABLE IF NOT EXISTS crypto_account (
|
||||
account_id TEXT PRIMARY KEY,
|
||||
device_id TEXT NOT NULL,
|
||||
|
@ -93,3 +93,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
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
|
@ -123,6 +123,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 {
|
||||
|
@ -153,6 +160,7 @@ type MemoryStore struct {
|
|||
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)
|
||||
|
@ -173,6 +181,7 @@ func NewMemoryStore(saveCallback func() error) *MemoryStore {
|
|||
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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -645,3 +654,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
|
||||
}
|
||||
|
|
|
@ -69,6 +69,8 @@ var TypeMap = map[Type]reflect.Type{
|
|||
ToDeviceRoomKeyRequest: reflect.TypeOf(RoomKeyRequestEventContent{}),
|
||||
ToDeviceEncrypted: reflect.TypeOf(EncryptedEventContent{}),
|
||||
ToDeviceRoomKeyWithheld: reflect.TypeOf(RoomKeyWithheldEventContent{}),
|
||||
ToDeviceSecretRequest: reflect.TypeOf(SecretRequestEventContent{}),
|
||||
ToDeviceSecretSend: reflect.TypeOf(SecretSendEventContent{}),
|
||||
ToDeviceDummy: reflect.TypeOf(DummyEventContent{}),
|
||||
|
||||
ToDeviceVerificationStart: reflect.TypeOf(VerificationStartEventContent{}),
|
||||
|
|
|
@ -176,4 +176,27 @@ func (withheld *RoomKeyWithheldEventContent) Is(other error) bool {
|
|||
return withheld.Code == "" || otherWithheld.Code == "" || withheld.Code == otherWithheld.Code
|
||||
}
|
||||
|
||||
type SecretRequestAction string
|
||||
|
||||
func (a SecretRequestAction) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
const (
|
||||
SecretRequestRequest = "request"
|
||||
SecretRequestCancellation = "request_cancellation"
|
||||
)
|
||||
|
||||
type SecretRequestEventContent struct {
|
||||
Name id.Secret `json:"name,omitempty"`
|
||||
Action SecretRequestAction `json:"action"`
|
||||
RequestingDeviceID id.DeviceID `json:"requesting_device_id"`
|
||||
RequestID string `json:"request_id"`
|
||||
}
|
||||
|
||||
type SecretSendEventContent struct {
|
||||
RequestID string `json:"request_id"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type DummyEventContent struct{}
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type RoomType string
|
||||
|
@ -235,9 +237,9 @@ var (
|
|||
|
||||
AccountDataSecretStorageDefaultKey = Type{"m.secret_storage.default_key", AccountDataEventType}
|
||||
AccountDataSecretStorageKey = Type{"m.secret_storage.key", AccountDataEventType}
|
||||
AccountDataCrossSigningMaster = Type{"m.cross_signing.master", AccountDataEventType}
|
||||
AccountDataCrossSigningUser = Type{"m.cross_signing.user_signing", AccountDataEventType}
|
||||
AccountDataCrossSigningSelf = Type{"m.cross_signing.self_signing", AccountDataEventType}
|
||||
AccountDataCrossSigningMaster = Type{string(id.SecretXSMaster), AccountDataEventType}
|
||||
AccountDataCrossSigningUser = Type{string(id.SecretXSUserSigning), AccountDataEventType}
|
||||
AccountDataCrossSigningSelf = Type{string(id.SecretXSSelfSigning), AccountDataEventType}
|
||||
AccountDataMegolmBackupKey = Type{"m.megolm_backup.v1", AccountDataEventType}
|
||||
)
|
||||
|
||||
|
@ -248,6 +250,8 @@ var (
|
|||
ToDeviceForwardedRoomKey = Type{"m.forwarded_room_key", ToDeviceEventType}
|
||||
ToDeviceEncrypted = Type{"m.room.encrypted", ToDeviceEventType}
|
||||
ToDeviceRoomKeyWithheld = Type{"m.room_key.withheld", ToDeviceEventType}
|
||||
ToDeviceSecretRequest = Type{"m.secret.request", ToDeviceEventType}
|
||||
ToDeviceSecretSend = Type{"m.secret.send", ToDeviceEventType}
|
||||
ToDeviceDummy = Type{"m.dummy", ToDeviceEventType}
|
||||
ToDeviceVerificationRequest = Type{"m.key.verification.request", ToDeviceEventType}
|
||||
ToDeviceVerificationStart = Type{"m.key.verification.start", ToDeviceEventType}
|
||||
|
|
14
id/crypto.go
14
id/crypto.go
|
@ -153,3 +153,17 @@ type CrossSigningKey struct {
|
|||
Key Ed25519
|
||||
First Ed25519
|
||||
}
|
||||
|
||||
// Secret storage keys
|
||||
type Secret string
|
||||
|
||||
func (s Secret) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
const (
|
||||
SecretXSMaster Secret = "m.cross_signing.master"
|
||||
SecretXSSelfSigning Secret = "m.cross_signing.self_signing"
|
||||
SecretXSUserSigning Secret = "m.cross_signing.user_signing"
|
||||
SecretMegolmBackupV1 Secret = "m.megolm_backup.v1"
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue