signalmeow: add support for ssre2

Corresponds to a338bc5a67
This commit is contained in:
Tulir Asokan 2024-11-18 15:18:44 +02:00
parent cf216cba7a
commit 29d55ebbde
17 changed files with 145 additions and 19 deletions

View file

@ -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 <https://www.gnu.org/licenses/>.
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
}

View file

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

View file

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

View file

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

View file

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

View file

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

Binary file not shown.

View file

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

View file

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

View file

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

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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