Skip to content

Commit

Permalink
signalmeow: add support for ssre2
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Nov 18, 2024
1 parent cf216cb commit 29d55eb
Show file tree
Hide file tree
Showing 17 changed files with 145 additions and 19 deletions.
55 changes: 55 additions & 0 deletions pkg/libsignalgo/accountentropy.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/ContactDiscovery.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/DeviceName.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/Groups.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/Provisioning.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 19 additions & 3 deletions pkg/signalmeow/protobuf/SignalService.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified pkg/signalmeow/protobuf/SignalService.pb.raw
Binary file not shown.
2 changes: 2 additions & 0 deletions pkg/signalmeow/protobuf/SignalService.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/StickerResources.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions pkg/signalmeow/protobuf/StorageService.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified pkg/signalmeow/protobuf/StorageService.pb.raw
Binary file not shown.
2 changes: 2 additions & 0 deletions pkg/signalmeow/protobuf/StorageService.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/signalmeow/protobuf/WebSocketResources.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pkg/signalmeow/provisioning.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down
17 changes: 17 additions & 0 deletions pkg/signalmeow/receiving.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package signalmeow

import (
"bytes"
"context"
"encoding/base64"
"fmt"
Expand Down Expand Up @@ -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")
Expand Down
30 changes: 23 additions & 7 deletions pkg/signalmeow/storageservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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).
Expand Down

0 comments on commit 29d55eb

Please sign in to comment.