Secret sharing implementation

pull/165/head
Toni Spets 2024-01-19 20:32:28 +02:00 committed by Toni Spets
parent 97d19484a3
commit 94664f1c8a
10 changed files with 300 additions and 4 deletions

View File

@ -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")

173
crypto/sharing.go Normal file
View File

@ -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,
})
}()
}

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"
@ -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
}

View File

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

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

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

View File

@ -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{}),

View File

@ -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{}

View File

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

View File

@ -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"
)