diff --git a/pkg/libsignalgo/accountentropy.go b/pkg/libsignalgo/accountentropy.go new file mode 100644 index 00000000..3160d646 --- /dev/null +++ b/pkg/libsignalgo/accountentropy.go @@ -0,0 +1,55 @@ +// mautrix-signal - A Matrix-signal puppeting bridge. +// Copyright (C) 2024 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package libsignalgo + +/* +#cgo LDFLAGS: -lsignal_ffi -ldl -lm +#include "./libsignal-ffi.h" +*/ +import "C" +import ( + "runtime" + "unsafe" +) + +type AccountEntropyPool string + +func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) { + var out [C.SignalSVR_KEY_LEN]byte + signalFfiError := C.signal_account_entropy_pool_derive_svr_key( + (*[C.SignalSVR_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)), + C.CString(string(aep)), + ) + runtime.KeepAlive(aep) + if signalFfiError != nil { + return nil, wrapError(signalFfiError) + } + return out[:], nil +} + +func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) { + var out [C.SignalBACKUP_KEY_LEN]byte + signalFfiError := C.signal_account_entropy_pool_derive_backup_key( + (*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)), + C.CString(string(aep)), + ) + runtime.KeepAlive(aep) + if signalFfiError != nil { + return nil, wrapError(signalFfiError) + } + return out[:], nil +} diff --git a/pkg/signalmeow/protobuf/ContactDiscovery.pb.go b/pkg/signalmeow/protobuf/ContactDiscovery.pb.go index 6fd86ab6..3c3d5b19 100644 --- a/pkg/signalmeow/protobuf/ContactDiscovery.pb.go +++ b/pkg/signalmeow/protobuf/ContactDiscovery.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: ContactDiscovery.proto diff --git a/pkg/signalmeow/protobuf/DeviceName.pb.go b/pkg/signalmeow/protobuf/DeviceName.pb.go index 8207918f..97219262 100644 --- a/pkg/signalmeow/protobuf/DeviceName.pb.go +++ b/pkg/signalmeow/protobuf/DeviceName.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: DeviceName.proto diff --git a/pkg/signalmeow/protobuf/Groups.pb.go b/pkg/signalmeow/protobuf/Groups.pb.go index b10d66a8..b6d22807 100644 --- a/pkg/signalmeow/protobuf/Groups.pb.go +++ b/pkg/signalmeow/protobuf/Groups.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: Groups.proto diff --git a/pkg/signalmeow/protobuf/Provisioning.pb.go b/pkg/signalmeow/protobuf/Provisioning.pb.go index e7b75858..24e3a647 100644 --- a/pkg/signalmeow/protobuf/Provisioning.pb.go +++ b/pkg/signalmeow/protobuf/Provisioning.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: Provisioning.proto diff --git a/pkg/signalmeow/protobuf/SignalService.pb.go b/pkg/signalmeow/protobuf/SignalService.pb.go index 9ce328c7..f9abde18 100644 --- a/pkg/signalmeow/protobuf/SignalService.pb.go +++ b/pkg/signalmeow/protobuf/SignalService.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: SignalService.proto @@ -6243,8 +6243,10 @@ type SyncMessage_Keys struct { unknownFields protoimpl.UnknownFields // @deprecated - StorageService []byte `protobuf:"bytes,1,opt,name=storageService" json:"storageService,omitempty"` - Master []byte `protobuf:"bytes,2,opt,name=master" json:"master,omitempty"` + StorageService []byte `protobuf:"bytes,1,opt,name=storageService" json:"storageService,omitempty"` + Master []byte `protobuf:"bytes,2,opt,name=master" json:"master,omitempty"` + AccountEntropyPool *string `protobuf:"bytes,3,opt,name=accountEntropyPool" json:"accountEntropyPool,omitempty"` // Copied manually from Signal Desktop + MediaRootBackupKey []byte `protobuf:"bytes,4,opt,name=mediaRootBackupKey" json:"mediaRootBackupKey,omitempty"` // Copied manually from Signal Desktop } func (x *SyncMessage_Keys) Reset() { @@ -6291,6 +6293,20 @@ func (x *SyncMessage_Keys) GetMaster() []byte { return nil } +func (x *SyncMessage_Keys) GetAccountEntropyPool() string { + if x != nil && x.AccountEntropyPool != nil { + return *x.AccountEntropyPool + } + return "" +} + +func (x *SyncMessage_Keys) GetMediaRootBackupKey() []byte { + if x != nil { + return x.MediaRootBackupKey + } + return nil +} + type SyncMessage_MessageRequestResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache diff --git a/pkg/signalmeow/protobuf/SignalService.pb.raw b/pkg/signalmeow/protobuf/SignalService.pb.raw index 45c938aa..0212283a 100644 Binary files a/pkg/signalmeow/protobuf/SignalService.pb.raw and b/pkg/signalmeow/protobuf/SignalService.pb.raw differ diff --git a/pkg/signalmeow/protobuf/SignalService.proto b/pkg/signalmeow/protobuf/SignalService.proto index aa0a7524..8a4bca89 100644 --- a/pkg/signalmeow/protobuf/SignalService.proto +++ b/pkg/signalmeow/protobuf/SignalService.proto @@ -547,6 +547,8 @@ message SyncMessage { // @deprecated optional bytes storageService = 1; optional bytes master = 2; + optional string accountEntropyPool = 3; // Copied manually from Signal Desktop + optional bytes mediaRootBackupKey = 4; // Copied manually from Signal Desktop } message MessageRequestResponse { diff --git a/pkg/signalmeow/protobuf/StickerResources.pb.go b/pkg/signalmeow/protobuf/StickerResources.pb.go index 0da3266e..d49db130 100644 --- a/pkg/signalmeow/protobuf/StickerResources.pb.go +++ b/pkg/signalmeow/protobuf/StickerResources.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: StickerResources.proto diff --git a/pkg/signalmeow/protobuf/StorageService.pb.go b/pkg/signalmeow/protobuf/StorageService.pb.go index 269354ae..88b890a8 100644 --- a/pkg/signalmeow/protobuf/StorageService.pb.go +++ b/pkg/signalmeow/protobuf/StorageService.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: StorageService.proto @@ -623,7 +623,8 @@ type ManifestRecord struct { Version uint64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` SourceDevice uint32 `protobuf:"varint,3,opt,name=sourceDevice,proto3" json:"sourceDevice,omitempty"` - Identifiers []*ManifestRecord_Identifier `protobuf:"bytes,2,rep,name=identifiers,proto3" json:"identifiers,omitempty"` // Next ID: 4 + Identifiers []*ManifestRecord_Identifier `protobuf:"bytes,2,rep,name=identifiers,proto3" json:"identifiers,omitempty"` // Next ID: 4 + RecordIkm []byte `protobuf:"bytes,4,opt,name=recordIkm,proto3,oneof" json:"recordIkm,omitempty"` // Copied manually from Signal Desktop } func (x *ManifestRecord) Reset() { @@ -677,6 +678,13 @@ func (x *ManifestRecord) GetIdentifiers() []*ManifestRecord_Identifier { return nil } +func (x *ManifestRecord) GetRecordIkm() []byte { + if x != nil { + return x.RecordIkm + } + return nil +} + type StorageRecord struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2130,6 +2138,7 @@ func file_StorageService_proto_init() { if File_StorageService_proto != nil { return } + file_StorageService_proto_msgTypes[5].OneofWrappers = []any{} file_StorageService_proto_msgTypes[6].OneofWrappers = []any{ (*StorageRecord_Contact)(nil), (*StorageRecord_GroupV1)(nil), diff --git a/pkg/signalmeow/protobuf/StorageService.pb.raw b/pkg/signalmeow/protobuf/StorageService.pb.raw index c4befd57..cee69e5c 100644 Binary files a/pkg/signalmeow/protobuf/StorageService.pb.raw and b/pkg/signalmeow/protobuf/StorageService.pb.raw differ diff --git a/pkg/signalmeow/protobuf/StorageService.proto b/pkg/signalmeow/protobuf/StorageService.proto index 7b2fa3aa..c8f45824 100644 --- a/pkg/signalmeow/protobuf/StorageService.proto +++ b/pkg/signalmeow/protobuf/StorageService.proto @@ -61,6 +61,8 @@ message ManifestRecord { uint32 sourceDevice = 3; repeated Identifier identifiers = 2; // Next ID: 4 + + optional bytes recordIkm = 4; // Copied manually from Signal Desktop } message StorageRecord { diff --git a/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go b/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go index b42ca6f7..e4eda97c 100644 --- a/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go +++ b/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: UnidentifiedDelivery.proto diff --git a/pkg/signalmeow/protobuf/WebSocketResources.pb.go b/pkg/signalmeow/protobuf/WebSocketResources.pb.go index b3514e64..157c8022 100644 --- a/pkg/signalmeow/protobuf/WebSocketResources.pb.go +++ b/pkg/signalmeow/protobuf/WebSocketResources.pb.go @@ -5,7 +5,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 +// protoc-gen-go v1.35.2 // protoc v3.21.12 // source: WebSocketResources.proto diff --git a/pkg/signalmeow/provisioning.go b/pkg/signalmeow/provisioning.go index 75bb6df6..c9979592 100644 --- a/pkg/signalmeow/provisioning.go +++ b/pkg/signalmeow/provisioning.go @@ -166,6 +166,14 @@ func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, dev Password: password, MasterKey: provisioningMessage.GetMasterKey(), } + if provisioningMessage.GetMasterKey() == nil && provisioningMessage.GetAccountEntropyPool() != "" { + data.MasterKey, err = libsignalgo.AccountEntropyPool(provisioningMessage.GetAccountEntropyPool()).DeriveSVRKey() + if err != nil { + log.Err(err).Msg("Failed to derive master key from account entropy pool") + } else { + log.Debug().Msg("Derived master key from account entropy pool") + } + } // Store the provisioning data err = deviceStore.PutDevice(ctx, data) @@ -334,6 +342,7 @@ func continueProvisioning(ctx context.Context, ws *websocket.Conn, provisioningC var signalCapabilities = map[string]any{ "deleteSync": true, "versionedExpirationTimer": true, + "ssre2": true, } var signalCapabilitiesBody = exerrors.Must(json.Marshal(signalCapabilities)) diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index 1bebf771..aa074316 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -17,6 +17,7 @@ package signalmeow import ( + "bytes" "context" "encoding/base64" "fmt" @@ -718,7 +719,23 @@ func (cli *Client) handleDecryptedResult( // TODO: handle more sync messages if content.SyncMessage != nil { if content.SyncMessage.Keys != nil { + aep := libsignalgo.AccountEntropyPool(content.SyncMessage.Keys.GetAccountEntropyPool()) cli.Store.MasterKey = content.SyncMessage.Keys.GetMaster() + if aep != "" { + aepMasterKey, err := aep.DeriveSVRKey() + if err != nil { + log.Err(err).Msg("Failed to derive master key from account entropy pool") + } else if cli.Store.MasterKey == nil { + cli.Store.MasterKey = aepMasterKey + log.Debug().Msg("Derived master key from account entropy pool (no master key in sync message)") + } else if !bytes.Equal(aepMasterKey, cli.Store.MasterKey) { + log.Warn().Msg("Derived master key doesn't match one in sync message") + } else { + log.Debug().Msg("Derived master key matches one in sync message") + } + } else { + log.Debug().Msg("No account entropy pool in sync message") + } err = cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData) if err != nil { log.Err(err).Msg("Failed to save device after receiving master key") diff --git a/pkg/signalmeow/storageservice.go b/pkg/signalmeow/storageservice.go index f2385f30..1902c4b5 100644 --- a/pkg/signalmeow/storageservice.go +++ b/pkg/signalmeow/storageservice.go @@ -29,6 +29,7 @@ import ( "github.com/google/uuid" "github.com/rs/zerolog" "go.mau.fi/util/exerrors" + "golang.org/x/crypto/hkdf" "golang.org/x/exp/maps" "golang.org/x/exp/slices" "google.golang.org/protobuf/proto" @@ -146,7 +147,7 @@ func (cli *Client) FetchStorage(ctx context.Context, masterKey []byte, currentVe } delete(newKeys, key) } - newRecords, missingKeys, err := cli.fetchStorageRecords(ctx, storageKey, newKeys) + newRecords, missingKeys, err := cli.fetchStorageRecords(ctx, storageKey, manifest.GetRecordIkm(), newKeys) if err != nil { return nil, err } @@ -178,10 +179,20 @@ func deriveStorageManifestKey(storageKey []byte, version uint64) []byte { return h.Sum(nil) } -func deriveStorageItemKey(storageKey []byte, itemID string) []byte { - h := hmac.New(sha256.New, storageKey) - exerrors.Must(fmt.Fprintf(h, "Item_%s", itemID)) - return h.Sum(nil) +const storageServiceItemKeyInfoPrefix = "20240801_SIGNAL_STORAGE_SERVICE_ITEM_" +const storageServiceItemKeyLen = 32 + +func deriveStorageItemKey(storageKey, recordIKM []byte, itemID string) []byte { + if recordIKM == nil { + h := hmac.New(sha256.New, storageKey) + exerrors.Must(fmt.Fprintf(h, "Item_%s", itemID)) + return h.Sum(nil) + } else { + h := hkdf.New(sha256.New, recordIKM, []byte{}, append([]byte(storageServiceItemKeyInfoPrefix), itemID...)) + out := make([]byte, storageServiceItemKeyLen) + exerrors.Must(io.ReadFull(h, out)) + return out + } } // MaxReadStorageRecords is the maximum number of storage records to fetch at once @@ -231,7 +242,12 @@ func (cli *Client) fetchStorageManifest(ctx context.Context, storageKey []byte, } } -func (cli *Client) fetchStorageRecords(ctx context.Context, storageKey []byte, inputRecords map[string]signalpb.ManifestRecord_Identifier_Type) ([]*DecryptedStorageRecord, []string, error) { +func (cli *Client) fetchStorageRecords( + ctx context.Context, + storageKey []byte, + recordIKM []byte, + inputRecords map[string]signalpb.ManifestRecord_Identifier_Type, +) ([]*DecryptedStorageRecord, []string, error) { recordKeys := make([][]byte, 0, len(inputRecords)) for key := range inputRecords { decoded, err := base64.StdEncoding.DecodeString(key) @@ -262,7 +278,7 @@ func (cli *Client) fetchStorageRecords(ctx context.Context, storageKey []byte, i log.Warn().Int("item_index", i).Str("item_key", base64Key).Msg("Received unexpected storage item") continue } - itemKey := deriveStorageItemKey(storageKey, base64Key) + itemKey := deriveStorageItemKey(storageKey, recordIKM, base64Key) decryptedItemBytes, err := decryptBytes(itemKey, encryptedItem.GetValue()) if err != nil { log.Warn().Err(err).