diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
index c10630f..cba1054 100644
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ b/.github/ISSUE_TEMPLATE/bug.md
@@ -11,7 +11,8 @@ type: Bug
### Checklist
-
+
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
+* [ ] The bug is still present on the main branch. The `!signal version` command output is: ``
diff --git a/go.mod b/go.mod
index 1106eb6..931af45 100644
--- a/go.mod
+++ b/go.mod
@@ -11,16 +11,17 @@ require (
github.com/emersion/go-vcard v0.0.0-20241024213814-c9703dde27ff
github.com/google/uuid v1.6.0
github.com/mattn/go-pointer v0.0.1
- github.com/rs/zerolog v1.35.0
+ github.com/rs/zerolog v1.35.1
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0
- go.mau.fi/util v0.9.8
+ go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
golang.org/x/crypto v0.50.0
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/net v0.53.0
+ golang.org/x/sync v0.20.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- maunium.net/go/mautrix v0.27.0
+ maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
)
require (
@@ -31,7 +32,7 @@ require (
github.com/lib/pq v1.12.3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mattn/go-sqlite3 v1.14.42 // indirect
+ github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
@@ -43,7 +44,6 @@ require (
github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.35.0 // indirect
- golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
diff --git a/go.sum b/go.sum
index 0f27daa..2f02866 100644
--- a/go.sum
+++ b/go.sum
@@ -30,8 +30,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0=
github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc=
-github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
-github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
+github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
+github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -42,8 +42,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
-github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
+github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
+github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -61,8 +61,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
-go.mau.fi/util v0.9.8 h1:+/jf8eM2dAT2wx9UidmaneH28r/CSCKCniCyby1qWz8=
-go.mau.fi/util v0.9.8/go.mod h1:up/5mbzH2M1pSBNXqRxODn8dg/hEKbLJu92W4/SNAX0=
+go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg=
+go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
@@ -91,5 +91,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
-maunium.net/go/mautrix v0.27.0 h1:yfEYwoIluVWkofUgbZl9gP4i5nQTF+QNsxtb+r5bKlM=
-maunium.net/go/mautrix v0.27.0/go.mod h1:7QpEQiTy6p4LHkXXaZI+N46tGYy8HMhD0JjzZAFoFWs=
+maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
+maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go
index e791324..5eab6a8 100644
--- a/pkg/connector/capabilities.go
+++ b/pkg/connector/capabilities.go
@@ -38,7 +38,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
- base := "fi.mau.signal.capabilities.2025_12_09"
+ base := "fi.mau.signal.capabilities.2026_05_12"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@@ -111,7 +111,8 @@ var signalCaps = &event.RoomFeatures{
},
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
- "image/webp": event.CapLevelFullySupported,
+ // Signal clients will only render static webp, so apng is preferred
+ "image/webp": event.CapLevelPartialSupport,
"image/png": event.CapLevelFullySupported,
"image/apng": event.CapLevelFullySupported,
"image/gif": supportedIfFFmpeg(),
@@ -211,6 +212,7 @@ var signalGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
+ ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@@ -235,5 +237,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
}
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
- return 1, 7
+ return 1, 8
}
diff --git a/pkg/connector/client.go b/pkg/connector/client.go
index 17ee216..4fcf188 100644
--- a/pkg/connector/client.go
+++ b/pkg/connector/client.go
@@ -27,6 +27,7 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
+ "maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-signal/pkg/signalid"
"go.mau.fi/mautrix-signal/pkg/signalmeow"
@@ -46,6 +47,7 @@ type SignalClient struct {
var (
_ bridgev2.NetworkAPI = (*SignalClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*SignalClient)(nil)
+ _ bridgev2.StickerImportingNetworkAPI = (*SignalClient)(nil)
)
var pushCfg = &bridgev2.PushConfig{
@@ -76,6 +78,14 @@ func (s *SignalClient) RegisterPushNotifications(ctx context.Context, pushType b
}
}
+func (s *SignalClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
+ return s.Main.MsgConv.DownloadImagePack(ctx, url)
+}
+
+func (s *SignalClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
+ return []*event.ImagePackMetadata{}, nil
+}
+
func (s *SignalClient) LogoutRemote(ctx context.Context) {
if s.Client == nil {
return
diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go
index 0877d66..05e2a07 100644
--- a/pkg/connector/directmedia.go
+++ b/pkg/connector/directmedia.go
@@ -4,7 +4,6 @@ import (
"context"
"encoding/base64"
"fmt"
- "io"
"os"
"maunium.net/go/mautrix/bridgev2"
@@ -30,6 +29,7 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("failed to parse direct media id: %w", err)
}
+ var rawDataResp []byte
switch info := info.(type) {
case *signalid.DirectMediaAttachment:
log.Info().
@@ -76,18 +76,11 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("failed to to get group master key: %w", err)
}
- return &mediaproxy.GetMediaResponseCallback{
- Callback: func(w io.Writer) (int64, error) {
- data, err := client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
- if err != nil {
- log.Err(err).Msg("Direct download failed")
- return 0, err
- }
-
- _, err = w.Write(data)
- return int64(len(data)), err
- },
- }, nil
+ rawDataResp, err = client.Client.DownloadGroupAvatar(ctx, info.GroupAvatarPath, groupMasterKey)
+ if err != nil {
+ log.Err(err).Msg("Direct download failed")
+ return nil, err
+ }
case *signalid.DirectMediaProfileAvatar:
log.Info().
Stringer("user_id", info.UserID).
@@ -111,19 +104,27 @@ func (s *SignalConnector) Download(ctx context.Context, mediaID networkid.MediaI
return nil, fmt.Errorf("profile key not found")
}
- return &mediaproxy.GetMediaResponseCallback{
- Callback: func(w io.Writer) (int64, error) {
- data, err := client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
- if err != nil {
- log.Err(err).Msg("Direct download failed")
- return 0, err
- }
+ rawDataResp, err = client.Client.DownloadUserAvatar(ctx, info.ProfileAvatarPath, *profileKey)
+ if err != nil {
+ log.Err(err).Msg("Direct download failed")
+ return nil, err
+ }
+ case *signalid.DirectMediaSticker:
+ log.Info().
+ Hex("pack_id", info.PackID).
+ Uint32("sticker_id", info.StickerID).
+ Msg("Direct downloading sticker")
- _, err = w.Write(data)
- return int64(len(data)), err
- },
- }, nil
+ rawDataResp, err = signalmeow.DownloadStickerPackItem(ctx, info.PackID, info.PackKey, info.StickerID)
+ if err != nil {
+ log.Err(err).Msg("Direct download failed")
+ return nil, err
+ }
default:
return nil, fmt.Errorf("no downloader for direct media type: %T", info)
}
+ if rawDataResp == nil {
+ return nil, fmt.Errorf("unexpected fallthrough with no data")
+ }
+ return mediaproxy.GetMediaResponseRawData(rawDataResp), nil
}
diff --git a/pkg/libsignalgo/identitykeystore.go b/pkg/libsignalgo/identitykeystore.go
index 43941da..ba26f06 100644
--- a/pkg/libsignalgo/identitykeystore.go
+++ b/pkg/libsignalgo/identitykeystore.go
@@ -159,11 +159,11 @@ func signal_destroy_identity_key_store_callback(storeCtx unsafe.Pointer) {
func (ctx *CallbackContext) wrapIdentityKeyStore(store IdentityKeyStore) C.SignalConstPointerFfiIdentityKeyStoreStruct {
return C.SignalConstPointerFfiIdentityKeyStoreStruct{&C.SignalIdentityKeyStore{
ctx: wrapStore(ctx, store),
- get_local_identity_key_pair: C.SignalFfiBridgeIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback),
- get_local_registration_id: C.SignalFfiBridgeIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
- get_identity_key: C.SignalFfiBridgeIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback),
- save_identity_key: C.SignalFfiBridgeIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback),
- is_trusted_identity: C.SignalFfiBridgeIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback),
- destroy: C.SignalFfiBridgeIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
+ get_local_identity_key_pair: C.SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair(C.signal_get_identity_key_pair_callback),
+ get_local_registration_id: C.SignalFfiIdentityKeyStoreGetLocalRegistrationId(C.signal_get_local_registration_id_callback),
+ get_identity_key: C.SignalFfiIdentityKeyStoreGetIdentityKey(C.signal_get_identity_key_callback),
+ save_identity_key: C.SignalFfiIdentityKeyStoreSaveIdentityKey(C.signal_save_identity_key_callback),
+ is_trusted_identity: C.SignalFfiIdentityKeyStoreIsTrustedIdentity(C.signal_is_trusted_identity_callback),
+ destroy: C.SignalFfiIdentityKeyStoreDestroy(C.signal_destroy_identity_key_store_callback),
}}
}
diff --git a/pkg/libsignalgo/kyberprekeystore.go b/pkg/libsignalgo/kyberprekeystore.go
index ebb5a9f..9deea17 100644
--- a/pkg/libsignalgo/kyberprekeystore.go
+++ b/pkg/libsignalgo/kyberprekeystore.go
@@ -77,9 +77,9 @@ func signal_destroy_kyber_pre_key_store_callback(storeCtx unsafe.Pointer) {
func (ctx *CallbackContext) wrapKyberPreKeyStore(store KyberPreKeyStore) C.SignalConstPointerFfiKyberPreKeyStoreStruct {
return C.SignalConstPointerFfiKyberPreKeyStoreStruct{&C.SignalKyberPreKeyStore{
ctx: wrapStore(ctx, store),
- load_kyber_pre_key: C.SignalFfiBridgeKyberPreKeyStoreLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
- store_kyber_pre_key: C.SignalFfiBridgeKyberPreKeyStoreStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
- mark_kyber_pre_key_used: C.SignalFfiBridgeKyberPreKeyStoreMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
- destroy: C.SignalFfiBridgeKyberPreKeyStoreDestroy(C.signal_destroy_kyber_pre_key_store_callback),
+ load_kyber_pre_key: C.SignalFfiKyberPreKeyStoreLoadKyberPreKey(C.signal_load_kyber_pre_key_callback),
+ store_kyber_pre_key: C.SignalFfiKyberPreKeyStoreStoreKyberPreKey(C.signal_store_kyber_pre_key_callback),
+ mark_kyber_pre_key_used: C.SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed(C.signal_mark_kyber_pre_key_used_callback),
+ destroy: C.SignalFfiKyberPreKeyStoreDestroy(C.signal_destroy_kyber_pre_key_store_callback),
}}
}
diff --git a/pkg/libsignalgo/libsignal b/pkg/libsignalgo/libsignal
index b58bd7d..bbc1688 160000
--- a/pkg/libsignalgo/libsignal
+++ b/pkg/libsignalgo/libsignal
@@ -1 +1 @@
-Subproject commit b58bd7d5dfa0a391486df4210fd83bab96b9b479
+Subproject commit bbc16886cae2feab1cd1fe271ccc651e8860ce96
diff --git a/pkg/libsignalgo/libsignal-ffi.h b/pkg/libsignalgo/libsignal-ffi.h
index fe3bf52..b75462a 100644
--- a/pkg/libsignalgo/libsignal-ffi.h
+++ b/pkg/libsignalgo/libsignal-ffi.h
@@ -629,13 +629,6 @@ typedef struct {
const SignalHttpRequest *raw;
} SignalConstPointerHttpRequest;
-/**
- * A wrapper type for raw UUIDs, because C treats arrays specially in argument position.
- */
-typedef struct {
- uint8_t bytes[16];
-} SignalUuid;
-
/**
* The fixed-width binary representation of a ServiceId.
*
@@ -643,6 +636,27 @@ typedef struct {
*/
typedef uint8_t SignalServiceIdFixedWidthBinaryBytes[17];
+typedef struct {
+ const uint32_t *base;
+ size_t length;
+} SignalBorrowedSliceOfu32;
+
+typedef struct {
+ const SignalCiphertextMessage *raw;
+} SignalConstPointerCiphertextMessage;
+
+typedef struct {
+ const SignalConstPointerCiphertextMessage *base;
+ size_t length;
+} SignalBorrowedSliceOfConstPointerCiphertextMessage;
+
+/**
+ * A wrapper type for raw UUIDs, because C treats arrays specially in argument position.
+ */
+typedef struct {
+ uint8_t bytes[16];
+} SignalUuid;
+
typedef struct {
SignalPrivateKey *raw;
} SignalMutPointerPrivateKey;
@@ -754,10 +768,6 @@ typedef struct {
const SignalPlaintextContent *raw;
} SignalConstPointerPlaintextContent;
-typedef struct {
- const SignalCiphertextMessage *raw;
-} SignalConstPointerCiphertextMessage;
-
typedef struct {
SignalConnectionInfo *raw;
} SignalMutPointerConnectionInfo;
@@ -782,20 +792,18 @@ typedef struct {
SignalSessionRecord *raw;
} SignalMutPointerSessionRecord;
-typedef int (*SignalFfiBridgeSessionStoreLoadSession)(void *ctx, SignalMutPointerSessionRecord *out, SignalMutPointerProtocolAddress address);
+typedef int (*SignalFfiSessionStoreLoadSession)(void *ctx, SignalMutPointerSessionRecord *out, SignalMutPointerProtocolAddress address);
-typedef int (*SignalFfiBridgeSessionStoreStoreSession)(void *ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record);
+typedef int (*SignalFfiSessionStoreStoreSession)(void *ctx, SignalMutPointerProtocolAddress address, SignalMutPointerSessionRecord record);
-typedef void (*SignalFfiBridgeSessionStoreDestroy)(void *ctx);
+typedef void (*SignalFfiSessionStoreDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgeSessionStoreLoadSession load_session;
- SignalFfiBridgeSessionStoreStoreSession store_session;
- SignalFfiBridgeSessionStoreDestroy destroy;
-} SignalFfiBridgeSessionStoreStruct;
-
-typedef SignalFfiBridgeSessionStoreStruct SignalSessionStore;
+ SignalFfiSessionStoreLoadSession load_session;
+ SignalFfiSessionStoreStoreSession store_session;
+ SignalFfiSessionStoreDestroy destroy;
+} SignalSessionStore;
typedef struct {
const SignalSessionStore *raw;
@@ -810,29 +818,27 @@ typedef struct {
SignalMutPointerPublicKey second;
} SignalPairOfMutPointerPrivateKeyMutPointerPublicKey;
-typedef int (*SignalFfiBridgeIdentityKeyStoreGetLocalIdentityKeyPair)(void *ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *out);
+typedef int (*SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair)(void *ctx, SignalPairOfMutPointerPrivateKeyMutPointerPublicKey *out);
-typedef int (*SignalFfiBridgeIdentityKeyStoreGetLocalRegistrationId)(void *ctx, uint32_t *out);
+typedef int (*SignalFfiIdentityKeyStoreGetLocalRegistrationId)(void *ctx, uint32_t *out);
-typedef int (*SignalFfiBridgeIdentityKeyStoreGetIdentityKey)(void *ctx, SignalMutPointerPublicKey *out, SignalMutPointerProtocolAddress address);
+typedef int (*SignalFfiIdentityKeyStoreGetIdentityKey)(void *ctx, SignalMutPointerPublicKey *out, SignalMutPointerProtocolAddress address);
-typedef int (*SignalFfiBridgeIdentityKeyStoreSaveIdentityKey)(void *ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
+typedef int (*SignalFfiIdentityKeyStoreSaveIdentityKey)(void *ctx, uint8_t *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key);
-typedef int (*SignalFfiBridgeIdentityKeyStoreIsTrustedIdentity)(void *ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
+typedef int (*SignalFfiIdentityKeyStoreIsTrustedIdentity)(void *ctx, bool *out, SignalMutPointerProtocolAddress address, SignalMutPointerPublicKey public_key, uint32_t direction);
-typedef void (*SignalFfiBridgeIdentityKeyStoreDestroy)(void *ctx);
+typedef void (*SignalFfiIdentityKeyStoreDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgeIdentityKeyStoreGetLocalIdentityKeyPair get_local_identity_key_pair;
- SignalFfiBridgeIdentityKeyStoreGetLocalRegistrationId get_local_registration_id;
- SignalFfiBridgeIdentityKeyStoreGetIdentityKey get_identity_key;
- SignalFfiBridgeIdentityKeyStoreSaveIdentityKey save_identity_key;
- SignalFfiBridgeIdentityKeyStoreIsTrustedIdentity is_trusted_identity;
- SignalFfiBridgeIdentityKeyStoreDestroy destroy;
-} SignalFfiBridgeIdentityKeyStoreStruct;
-
-typedef SignalFfiBridgeIdentityKeyStoreStruct SignalIdentityKeyStore;
+ SignalFfiIdentityKeyStoreGetLocalIdentityKeyPair get_local_identity_key_pair;
+ SignalFfiIdentityKeyStoreGetLocalRegistrationId get_local_registration_id;
+ SignalFfiIdentityKeyStoreGetIdentityKey get_identity_key;
+ SignalFfiIdentityKeyStoreSaveIdentityKey save_identity_key;
+ SignalFfiIdentityKeyStoreIsTrustedIdentity is_trusted_identity;
+ SignalFfiIdentityKeyStoreDestroy destroy;
+} SignalIdentityKeyStore;
typedef struct {
const SignalIdentityKeyStore *raw;
@@ -846,23 +852,21 @@ typedef struct {
SignalPreKeyRecord *raw;
} SignalMutPointerPreKeyRecord;
-typedef int (*SignalFfiBridgePreKeyStoreLoadPreKey)(void *ctx, SignalMutPointerPreKeyRecord *out, uint32_t id);
+typedef int (*SignalFfiPreKeyStoreLoadPreKey)(void *ctx, SignalMutPointerPreKeyRecord *out, uint32_t id);
-typedef int (*SignalFfiBridgePreKeyStoreStorePreKey)(void *ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
+typedef int (*SignalFfiPreKeyStoreStorePreKey)(void *ctx, uint32_t id, SignalMutPointerPreKeyRecord record);
-typedef int (*SignalFfiBridgePreKeyStoreRemovePreKey)(void *ctx, uint32_t id);
+typedef int (*SignalFfiPreKeyStoreRemovePreKey)(void *ctx, uint32_t id);
-typedef void (*SignalFfiBridgePreKeyStoreDestroy)(void *ctx);
+typedef void (*SignalFfiPreKeyStoreDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgePreKeyStoreLoadPreKey load_pre_key;
- SignalFfiBridgePreKeyStoreStorePreKey store_pre_key;
- SignalFfiBridgePreKeyStoreRemovePreKey remove_pre_key;
- SignalFfiBridgePreKeyStoreDestroy destroy;
-} SignalFfiBridgePreKeyStoreStruct;
-
-typedef SignalFfiBridgePreKeyStoreStruct SignalPreKeyStore;
+ SignalFfiPreKeyStoreLoadPreKey load_pre_key;
+ SignalFfiPreKeyStoreStorePreKey store_pre_key;
+ SignalFfiPreKeyStoreRemovePreKey remove_pre_key;
+ SignalFfiPreKeyStoreDestroy destroy;
+} SignalPreKeyStore;
typedef struct {
const SignalPreKeyStore *raw;
@@ -872,20 +876,18 @@ typedef struct {
SignalSignedPreKeyRecord *raw;
} SignalMutPointerSignedPreKeyRecord;
-typedef int (*SignalFfiBridgeSignedPreKeyStoreLoadSignedPreKey)(void *ctx, SignalMutPointerSignedPreKeyRecord *out, uint32_t id);
+typedef int (*SignalFfiSignedPreKeyStoreLoadSignedPreKey)(void *ctx, SignalMutPointerSignedPreKeyRecord *out, uint32_t id);
-typedef int (*SignalFfiBridgeSignedPreKeyStoreStoreSignedPreKey)(void *ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
+typedef int (*SignalFfiSignedPreKeyStoreStoreSignedPreKey)(void *ctx, uint32_t id, SignalMutPointerSignedPreKeyRecord record);
-typedef void (*SignalFfiBridgeSignedPreKeyStoreDestroy)(void *ctx);
+typedef void (*SignalFfiSignedPreKeyStoreDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgeSignedPreKeyStoreLoadSignedPreKey load_signed_pre_key;
- SignalFfiBridgeSignedPreKeyStoreStoreSignedPreKey store_signed_pre_key;
- SignalFfiBridgeSignedPreKeyStoreDestroy destroy;
-} SignalFfiBridgeSignedPreKeyStoreStruct;
-
-typedef SignalFfiBridgeSignedPreKeyStoreStruct SignalSignedPreKeyStore;
+ SignalFfiSignedPreKeyStoreLoadSignedPreKey load_signed_pre_key;
+ SignalFfiSignedPreKeyStoreStoreSignedPreKey store_signed_pre_key;
+ SignalFfiSignedPreKeyStoreDestroy destroy;
+} SignalSignedPreKeyStore;
typedef struct {
const SignalSignedPreKeyStore *raw;
@@ -895,23 +897,21 @@ typedef struct {
SignalKyberPreKeyRecord *raw;
} SignalMutPointerKyberPreKeyRecord;
-typedef int (*SignalFfiBridgeKyberPreKeyStoreLoadKyberPreKey)(void *ctx, SignalMutPointerKyberPreKeyRecord *out, uint32_t id);
+typedef int (*SignalFfiKyberPreKeyStoreLoadKyberPreKey)(void *ctx, SignalMutPointerKyberPreKeyRecord *out, uint32_t id);
-typedef int (*SignalFfiBridgeKyberPreKeyStoreStoreKyberPreKey)(void *ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
+typedef int (*SignalFfiKyberPreKeyStoreStoreKyberPreKey)(void *ctx, uint32_t id, SignalMutPointerKyberPreKeyRecord record);
-typedef int (*SignalFfiBridgeKyberPreKeyStoreMarkKyberPreKeyUsed)(void *ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
+typedef int (*SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed)(void *ctx, uint32_t id, uint32_t ec_prekey_id, SignalMutPointerPublicKey base_key);
-typedef void (*SignalFfiBridgeKyberPreKeyStoreDestroy)(void *ctx);
+typedef void (*SignalFfiKyberPreKeyStoreDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgeKyberPreKeyStoreLoadKyberPreKey load_kyber_pre_key;
- SignalFfiBridgeKyberPreKeyStoreStoreKyberPreKey store_kyber_pre_key;
- SignalFfiBridgeKyberPreKeyStoreMarkKyberPreKeyUsed mark_kyber_pre_key_used;
- SignalFfiBridgeKyberPreKeyStoreDestroy destroy;
-} SignalFfiBridgeKyberPreKeyStoreStruct;
-
-typedef SignalFfiBridgeKyberPreKeyStoreStruct SignalKyberPreKeyStore;
+ SignalFfiKyberPreKeyStoreLoadKyberPreKey load_kyber_pre_key;
+ SignalFfiKyberPreKeyStoreStoreKyberPreKey store_kyber_pre_key;
+ SignalFfiKyberPreKeyStoreMarkKyberPreKeyUsed mark_kyber_pre_key_used;
+ SignalFfiKyberPreKeyStoreDestroy destroy;
+} SignalKyberPreKeyStore;
typedef struct {
const SignalKyberPreKeyStore *raw;
@@ -1047,20 +1047,18 @@ typedef struct {
SignalSenderKeyRecord *raw;
} SignalMutPointerSenderKeyRecord;
-typedef int (*SignalFfiBridgeSenderKeyStoreLoadSenderKey)(void *ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id);
+typedef int (*SignalFfiSenderKeyStoreLoadSenderKey)(void *ctx, SignalMutPointerSenderKeyRecord *out, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id);
-typedef int (*SignalFfiBridgeSenderKeyStoreStoreSenderKey)(void *ctx, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id, SignalMutPointerSenderKeyRecord record);
+typedef int (*SignalFfiSenderKeyStoreStoreSenderKey)(void *ctx, SignalMutPointerProtocolAddress sender, SignalUuid distribution_id, SignalMutPointerSenderKeyRecord record);
-typedef void (*SignalFfiBridgeSenderKeyStoreDestroy)(void *ctx);
+typedef void (*SignalFfiSenderKeyStoreDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgeSenderKeyStoreLoadSenderKey load_sender_key;
- SignalFfiBridgeSenderKeyStoreStoreSenderKey store_sender_key;
- SignalFfiBridgeSenderKeyStoreDestroy destroy;
-} SignalFfiBridgeSenderKeyStoreStruct;
-
-typedef SignalFfiBridgeSenderKeyStoreStruct SignalSenderKeyStore;
+ SignalFfiSenderKeyStoreLoadSenderKey load_sender_key;
+ SignalFfiSenderKeyStoreStoreSenderKey store_sender_key;
+ SignalFfiSenderKeyStoreDestroy destroy;
+} SignalSenderKeyStore;
typedef struct {
const SignalSenderKeyStore *raw;
@@ -1117,6 +1115,11 @@ typedef struct {
SignalFfiLoggerDestroy destroy;
} SignalFfiLoggerStruct;
+typedef struct {
+ SignalOwnedBuffer first;
+ SignalOwnedBuffer second;
+} SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
+
/**
* A C callback used to report the results of Rust futures.
*
@@ -1127,10 +1130,10 @@ typedef struct {
* completed once.
*/
typedef struct {
- void (*complete)(SignalFfiError *error, const SignalOwnedBuffer *result, const void *context);
+ void (*complete)(SignalFfiError *error, const SignalPairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *result, const void *context);
const void *context;
SignalCancellationId cancellation_id;
-} SignalCPromiseOwnedBufferOfc_uchar;
+} SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar;
typedef struct {
const SignalUnauthenticatedChatConnection *raw;
@@ -1200,20 +1203,18 @@ typedef struct {
const SignalMessageBackupValidationOutcome *raw;
} SignalConstPointerMessageBackupValidationOutcome;
-typedef int (*SignalFfiBridgeInputStreamRead)(void *ctx, size_t *out, SignalBorrowedMutableBuffer buf);
+typedef int (*SignalFfiInputStreamRead)(void *ctx, size_t *out, SignalBorrowedMutableBuffer buf);
-typedef int (*SignalFfiBridgeInputStreamSkip)(void *ctx, uint64_t amount);
+typedef int (*SignalFfiInputStreamSkip)(void *ctx, uint64_t amount);
-typedef void (*SignalFfiBridgeInputStreamDestroy)(void *ctx);
+typedef void (*SignalFfiInputStreamDestroy)(void *ctx);
typedef struct {
void *ctx;
- SignalFfiBridgeInputStreamRead read;
- SignalFfiBridgeInputStreamSkip skip;
- SignalFfiBridgeInputStreamDestroy destroy;
-} SignalFfiBridgeInputStreamStruct;
-
-typedef SignalFfiBridgeInputStreamStruct SignalInputStream;
+ SignalFfiInputStreamRead read;
+ SignalFfiInputStreamSkip skip;
+ SignalFfiInputStreamDestroy destroy;
+} SignalInputStream;
typedef struct {
const SignalInputStream *raw;
@@ -1647,9 +1648,7 @@ typedef struct {
SignalValidatingMac *raw;
} SignalMutPointerValidatingMac;
-typedef SignalFfiBridgeInputStreamStruct SignalFfiBridgeSyncInputStreamStruct;
-
-typedef SignalFfiBridgeSyncInputStreamStruct SignalSyncInputStream;
+typedef SignalInputStream SignalSyncInputStream;
typedef struct {
const SignalSyncInputStream *raw;
@@ -1737,6 +1736,10 @@ SignalFfiError *signal_authenticated_chat_connection_preconnect(SignalCPromisebo
SignalFfiError *signal_authenticated_chat_connection_send(SignalCPromiseFfiChatResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, SignalConstPointerHttpRequest http_request, uint32_t timeout_millis);
+SignalFfiError *signal_authenticated_chat_connection_send_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *destination, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfConstPointerCiphertextMessage contents, bool online_only, bool is_urgent);
+
+SignalFfiError *signal_authenticated_chat_connection_send_sync_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerAuthenticatedChatConnection chat, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfConstPointerCiphertextMessage contents, bool is_urgent);
+
SignalFfiError *signal_backup_auth_credential_check_valid_contents(SignalBorrowedBuffer params_bytes);
SignalFfiError *signal_backup_auth_credential_get_backup_id(uint8_t (*out)[16], SignalBorrowedBuffer credential_bytes);
@@ -1897,7 +1900,7 @@ SignalFfiError *signal_create_call_link_credential_request_issue_deterministic(S
SignalFfiError *signal_create_call_link_credential_response_check_valid_contents(SignalBorrowedBuffer response_bytes);
-SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
+SignalFfiError *signal_decrypt_message(SignalOwnedBuffer *out, SignalConstPointerSignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store);
SignalFfiError *signal_decrypt_pre_key_message(SignalOwnedBuffer *out, SignalConstPointerPreKeySignalMessage message, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, SignalConstPointerFfiPreKeyStoreStruct prekey_store, SignalConstPointerFfiSignedPreKeyStoreStruct signed_prekey_store, SignalConstPointerFfiKyberPreKeyStoreStruct kyber_prekey_store);
@@ -2118,9 +2121,7 @@ bool signal_init_logger(SignalLogLevel max_level, SignalFfiLoggerStruct logger);
SignalFfiError *signal_key_transparency_aci_search_key(SignalOwnedBuffer *out, const SignalServiceIdFixedWidthBinaryBytes *aci);
-SignalFfiError *signal_key_transparency_check(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalBorrowedBuffer last_distinguished_tree_head, bool is_self_check, bool is_e164_discoverable);
-
-SignalFfiError *signal_key_transparency_distinguished(SignalCPromiseOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head);
+SignalFfiError *signal_key_transparency_check(SignalCPromisePairOfOwnedBufferOfc_ucharOwnedBufferOfc_uchar *promise, SignalConstPointerTokioAsyncContext async_runtime, uint8_t environment, SignalConstPointerUnauthenticatedChatConnection chat_connection, const SignalServiceIdFixedWidthBinaryBytes *aci, SignalConstPointerPublicKey aci_identity_key, const char *e164, SignalOptionalBorrowedSliceOfc_uchar unidentified_access_key, SignalOptionalBorrowedSliceOfc_uchar username_hash, SignalOptionalBorrowedSliceOfc_uchar account_data, SignalOptionalBorrowedSliceOfc_uchar last_distinguished_tree_head, bool is_self_check, bool is_e164_discoverable);
SignalFfiError *signal_key_transparency_e164_search_key(SignalOwnedBuffer *out, const char *e164);
@@ -2354,7 +2355,7 @@ SignalFfiError *signal_privatekey_serialize(SignalOwnedBuffer *out, SignalConstP
SignalFfiError *signal_privatekey_sign(SignalOwnedBuffer *out, SignalConstPointerPrivateKey key, SignalBorrowedBuffer message);
-SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
+SignalFfiError *signal_process_prekey_bundle(SignalConstPointerPreKeyBundle bundle, SignalConstPointerProtocolAddress protocol_address, SignalConstPointerProtocolAddress local_address, SignalConstPointerFfiSessionStoreStruct session_store, SignalConstPointerFfiIdentityKeyStoreStruct identity_key_store, uint64_t now);
SignalFfiError *signal_process_sender_key_distribution_message(SignalConstPointerProtocolAddress sender, SignalConstPointerSenderKeyDistributionMessage sender_key_distribution_message, SignalConstPointerFfiSenderKeyStoreStruct store);
@@ -2782,6 +2783,8 @@ SignalFfiError *signal_unauthenticated_chat_connection_look_up_username_link(Sig
SignalFfiError *signal_unauthenticated_chat_connection_send(SignalCPromiseFfiChatResponse *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalConstPointerHttpRequest http_request, uint32_t timeout_millis);
+SignalFfiError *signal_unauthenticated_chat_connection_send_message(SignalCPromisebool *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, const SignalServiceIdFixedWidthBinaryBytes *destination, uint64_t timestamp, SignalBorrowedSliceOfu32 device_ids, SignalBorrowedSliceOfu32 registration_ids, SignalBorrowedSliceOfBuffers contents, uint8_t auth_kind, SignalOptionalBorrowedSliceOfc_uchar auth_buffer, bool online_only, bool is_urgent);
+
SignalFfiError *signal_unauthenticated_chat_connection_send_multi_recipient_message(SignalCPromiseOwnedBufferOfServiceIdFixedWidthBinaryBytes *promise, SignalConstPointerTokioAsyncContext async_runtime, SignalConstPointerUnauthenticatedChatConnection chat, SignalBorrowedBuffer payload, uint64_t timestamp, SignalBorrowedBuffer auth, bool online_only, bool is_urgent);
SignalFfiError *signal_unidentified_sender_message_content_deserialize(SignalMutPointerUnidentifiedSenderMessageContent *out, SignalBorrowedBuffer data);
diff --git a/pkg/libsignalgo/message.go b/pkg/libsignalgo/message.go
index 1b581c0..6cba873 100644
--- a/pkg/libsignalgo/message.go
+++ b/pkg/libsignalgo/message.go
@@ -49,7 +49,7 @@ func Encrypt(ctx context.Context, plaintext []byte, forAddress, localAddress *Ad
return wrapCiphertextMessage(ciphertextMessage.raw), nil
}
-func Decrypt(ctx context.Context, message *Message, fromAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
+func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) ([]byte, error) {
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
var decrypted C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
@@ -57,6 +57,7 @@ func Decrypt(ctx context.Context, message *Message, fromAddress *Address, sessio
&decrypted,
message.constPtr(),
fromAddress.constPtr(),
+ localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore),
)
diff --git a/pkg/libsignalgo/prekeybundle.go b/pkg/libsignalgo/prekeybundle.go
index 4cd5547..8a6fcaa 100644
--- a/pkg/libsignalgo/prekeybundle.go
+++ b/pkg/libsignalgo/prekeybundle.go
@@ -27,13 +27,14 @@ import (
"time"
)
-func ProcessPreKeyBundle(ctx context.Context, bundle *PreKeyBundle, forAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) error {
+func ProcessPreKeyBundle(ctx context.Context, bundle *PreKeyBundle, forAddress, localAddress *Address, sessionStore SessionStore, identityStore IdentityKeyStore) error {
callbackCtx := NewCallbackContext(ctx)
defer callbackCtx.Unref()
var now C.uint64_t = C.uint64_t(time.Now().Unix())
signalFfiError := C.signal_process_prekey_bundle(
bundle.constPtr(),
forAddress.constPtr(),
+ localAddress.constPtr(),
callbackCtx.wrapSessionStore(sessionStore),
callbackCtx.wrapIdentityKeyStore(identityStore),
now,
diff --git a/pkg/libsignalgo/prekeystore.go b/pkg/libsignalgo/prekeystore.go
index ed8ea21..8c3c36f 100644
--- a/pkg/libsignalgo/prekeystore.go
+++ b/pkg/libsignalgo/prekeystore.go
@@ -76,9 +76,9 @@ func signal_destroy_pre_key_store_callback(storeCtx unsafe.Pointer) {
func (ctx *CallbackContext) wrapPreKeyStore(store PreKeyStore) C.SignalConstPointerFfiPreKeyStoreStruct {
return C.SignalConstPointerFfiPreKeyStoreStruct{&C.SignalPreKeyStore{
ctx: wrapStore(ctx, store),
- load_pre_key: C.SignalFfiBridgePreKeyStoreLoadPreKey(C.signal_load_pre_key_callback),
- store_pre_key: C.SignalFfiBridgePreKeyStoreStorePreKey(C.signal_store_pre_key_callback),
- remove_pre_key: C.SignalFfiBridgePreKeyStoreRemovePreKey(C.signal_remove_pre_key_callback),
- destroy: C.SignalFfiBridgePreKeyStoreDestroy(C.signal_destroy_pre_key_store_callback),
+ load_pre_key: C.SignalFfiPreKeyStoreLoadPreKey(C.signal_load_pre_key_callback),
+ store_pre_key: C.SignalFfiPreKeyStoreStorePreKey(C.signal_store_pre_key_callback),
+ remove_pre_key: C.SignalFfiPreKeyStoreRemovePreKey(C.signal_remove_pre_key_callback),
+ destroy: C.SignalFfiPreKeyStoreDestroy(C.signal_destroy_pre_key_store_callback),
}}
}
diff --git a/pkg/libsignalgo/senderkeystore.go b/pkg/libsignalgo/senderkeystore.go
index a07a287..1649216 100644
--- a/pkg/libsignalgo/senderkeystore.go
+++ b/pkg/libsignalgo/senderkeystore.go
@@ -70,8 +70,8 @@ func signal_destroy_sender_key_store_callback(storeCtx unsafe.Pointer) {
func (ctx *CallbackContext) wrapSenderKeyStore(store SenderKeyStore) C.SignalConstPointerFfiSenderKeyStoreStruct {
return C.SignalConstPointerFfiSenderKeyStoreStruct{&C.SignalSenderKeyStore{
ctx: wrapStore(ctx, store),
- load_sender_key: C.SignalFfiBridgeSenderKeyStoreLoadSenderKey(C.signal_load_sender_key_callback),
- store_sender_key: C.SignalFfiBridgeSenderKeyStoreStoreSenderKey(C.signal_store_sender_key_callback),
- destroy: C.SignalFfiBridgeSenderKeyStoreDestroy(C.signal_destroy_sender_key_store_callback),
+ load_sender_key: C.SignalFfiSenderKeyStoreLoadSenderKey(C.signal_load_sender_key_callback),
+ store_sender_key: C.SignalFfiSenderKeyStoreStoreSenderKey(C.signal_store_sender_key_callback),
+ destroy: C.SignalFfiSenderKeyStoreDestroy(C.signal_destroy_sender_key_store_callback),
}}
}
diff --git a/pkg/libsignalgo/session_test.go b/pkg/libsignalgo/session_test.go
index 4bde894..dd05718 100644
--- a/pkg/libsignalgo/session_test.go
+++ b/pkg/libsignalgo/session_test.go
@@ -30,7 +30,7 @@ import (
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
)
-func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtocolStore, bobAddress *libsignalgo.Address) {
+func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtocolStore, bobAddress, aliceAddress *libsignalgo.Address) {
ctx := context.TODO()
bobPreKey, err := libsignalgo.GeneratePrivateKey()
@@ -86,7 +86,7 @@ func initializeSessions(t *testing.T, aliceStore, bobStore *InMemorySignalProtoc
assert.NoError(t, err)
// Alice processes the bundle
- err = libsignalgo.ProcessPreKeyBundle(ctx, bobBundle, bobAddress, aliceStore, aliceStore)
+ err = libsignalgo.ProcessPreKeyBundle(ctx, bobBundle, bobAddress, aliceAddress, aliceStore, aliceStore)
assert.NoError(t, err)
record, err := aliceStore.LoadSession(ctx, bobAddress)
@@ -132,7 +132,7 @@ func TestSessionCipher(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore()
- initializeSessions(t, aliceStore, bobStore, bobAddress)
+ initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9}
@@ -163,7 +163,7 @@ func TestSessionCipher(t *testing.T) {
assert.NoError(t, err)
aliceCiphertext2, err := libsignalgo.DeserializeMessage(bobCiphertext2Serialized)
assert.NoError(t, err)
- alicePlaintext2, err := libsignalgo.Decrypt(ctx, aliceCiphertext2, bobAddress, aliceStore, aliceStore)
+ alicePlaintext2, err := libsignalgo.Decrypt(ctx, aliceCiphertext2, bobAddress, aliceAddress, aliceStore, aliceStore)
assert.NoError(t, err)
assert.Equal(t, bobPlaintext2, alicePlaintext2)
}
@@ -183,7 +183,7 @@ func TestSessionCipherWithBadStore(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore()
bobStore := &BadInMemorySignalProtocolStore{NewInMemorySignalProtocolStore()}
- initializeSessions(t, aliceStore, bobStore.InMemorySignalProtocolStore, bobAddress)
+ initializeSessions(t, aliceStore, bobStore.InMemorySignalProtocolStore, bobAddress, aliceAddress)
alicePlaintext := []byte{8, 6, 7, 5, 3, 0, 9}
@@ -216,7 +216,7 @@ func TestSealedSenderEncrypt_Repeated(t *testing.T) {
aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore()
- initializeSessions(t, aliceStore, bobStore, bobAddress)
+ initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
trustRoot, err := libsignalgo.GenerateIdentityKeyPair()
assert.NoError(t, err)
@@ -252,15 +252,18 @@ func TestArchiveSession(t *testing.T) {
ctx := context.TODO()
setupLogging()
+ aliceACI := uuid.New()
bobACI := uuid.New()
+ aliceAddress, err := libsignalgo.NewACIServiceID(aliceACI).Address(1)
+ assert.NoError(t, err)
bobAddress, err := libsignalgo.NewACIServiceID(bobACI).Address(1)
assert.NoError(t, err)
aliceStore := NewInMemorySignalProtocolStore()
bobStore := NewInMemorySignalProtocolStore()
- initializeSessions(t, aliceStore, bobStore, bobAddress)
+ initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
session, err := aliceStore.LoadSession(ctx, bobAddress)
assert.NoError(t, err)
@@ -315,7 +318,7 @@ func TestSealedSenderGroupCipher(t *testing.T) {
bobStore := NewInMemorySignalProtocolStore()
- initializeSessions(t, aliceStore, bobStore, bobAddress)
+ initializeSessions(t, aliceStore, bobStore, bobAddress, aliceAddress)
trustRoot, err := libsignalgo.GenerateIdentityKeyPair()
assert.NoError(t, err)
diff --git a/pkg/libsignalgo/sessionstore.go b/pkg/libsignalgo/sessionstore.go
index 2515232..99000e5 100644
--- a/pkg/libsignalgo/sessionstore.go
+++ b/pkg/libsignalgo/sessionstore.go
@@ -67,8 +67,8 @@ func signal_destroy_session_store_callback(storeCtx unsafe.Pointer) {
func (ctx *CallbackContext) wrapSessionStore(store SessionStore) C.SignalConstPointerFfiSessionStoreStruct {
return C.SignalConstPointerFfiSessionStoreStruct{&C.SignalSessionStore{
ctx: wrapStore(ctx, store),
- load_session: C.SignalFfiBridgeSessionStoreLoadSession(C.signal_load_session_callback),
- store_session: C.SignalFfiBridgeSessionStoreStoreSession(C.signal_store_session_callback),
- destroy: C.SignalFfiBridgeSessionStoreDestroy(C.signal_destroy_session_store_callback),
+ load_session: C.SignalFfiSessionStoreLoadSession(C.signal_load_session_callback),
+ store_session: C.SignalFfiSessionStoreStoreSession(C.signal_store_session_callback),
+ destroy: C.SignalFfiSessionStoreDestroy(C.signal_destroy_session_store_callback),
}}
}
diff --git a/pkg/libsignalgo/signedprekeystore.go b/pkg/libsignalgo/signedprekeystore.go
index cfb3015..b1306e2 100644
--- a/pkg/libsignalgo/signedprekeystore.go
+++ b/pkg/libsignalgo/signedprekeystore.go
@@ -67,8 +67,8 @@ func signal_destroy_signed_pre_key_store_callback(storeCtx unsafe.Pointer) {
func (ctx *CallbackContext) wrapSignedPreKeyStore(store SignedPreKeyStore) C.SignalConstPointerFfiSignedPreKeyStoreStruct {
return C.SignalConstPointerFfiSignedPreKeyStoreStruct{&C.SignalSignedPreKeyStore{
ctx: wrapStore(ctx, store),
- load_signed_pre_key: C.SignalFfiBridgeSignedPreKeyStoreLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
- store_signed_pre_key: C.SignalFfiBridgeSignedPreKeyStoreStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
- destroy: C.SignalFfiBridgeSignedPreKeyStoreDestroy(C.signal_destroy_signed_pre_key_store_callback),
+ load_signed_pre_key: C.SignalFfiSignedPreKeyStoreLoadSignedPreKey(C.signal_load_signed_pre_key_callback),
+ store_signed_pre_key: C.SignalFfiSignedPreKeyStoreStoreSignedPreKey(C.signal_store_signed_pre_key_callback),
+ destroy: C.SignalFfiSignedPreKeyStoreDestroy(C.signal_destroy_signed_pre_key_store_callback),
}}
}
diff --git a/pkg/libsignalgo/version.go b/pkg/libsignalgo/version.go
index bd14084..1f7e94d 100644
--- a/pkg/libsignalgo/version.go
+++ b/pkg/libsignalgo/version.go
@@ -2,4 +2,4 @@
package libsignalgo
-const Version = "v0.92.1"
+const Version = "v0.93.2"
diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go
index 89b0181..a334afd 100644
--- a/pkg/msgconv/from-matrix.go
+++ b/pkg/msgconv/from-matrix.go
@@ -110,21 +110,24 @@ func (mc *MessageConverter) ToSignal(
return nil, fmt.Errorf("failed to convert sticker: %w", err)
}
att.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS))
- var emoji *string
- // TODO check for single grapheme cluster?
- if len([]rune(content.Body)) == 1 {
- emoji = proto.String(variationselector.Remove(content.Body))
- }
- dm.Sticker = &signalpb.DataMessage_Sticker{
- // Signal iOS validates that pack id/key are of the correct length.
- // Android is fine with any non-nil values (like a zero-length byte string).
- PackId: make([]byte, 16),
- PackKey: make([]byte, 32),
- StickerId: proto.Uint32(0),
- Data: att,
- Emoji: emoji,
+ dm.Sticker = ParseStickerMeta(content.Info.BridgedSticker)
+ if dm.Sticker == nil {
+ var emoji *string
+ // TODO check for single grapheme cluster?
+ if len([]rune(content.Body)) == 1 {
+ emoji = proto.String(variationselector.Remove(content.Body))
+ }
+ dm.Sticker = &signalpb.DataMessage_Sticker{
+ // Signal iOS validates that pack id/key are of the correct length.
+ // Android is fine with any non-nil values (like a zero-length byte string).
+ PackId: make([]byte, 16),
+ PackKey: make([]byte, 32),
+ StickerId: proto.Uint32(0),
+ Emoji: emoji,
+ }
}
+ dm.Sticker.Data = att
case event.MsgLocation:
lat, lon, err := parseGeoURI(content.GeoURI)
if err != nil {
diff --git a/pkg/msgconv/from-signal.go b/pkg/msgconv/from-signal.go
index 96b4f10..defbe44 100644
--- a/pkg/msgconv/from-signal.go
+++ b/pkg/msgconv/from-signal.go
@@ -468,20 +468,16 @@ func (mc *MessageConverter) convertStickerToMatrix(ctx context.Context, sticker
converted.Content.Info.Height = 200
}
converted.Content.Body = sticker.GetEmoji()
+ if len(sticker.GetPackId()) == PackIDLength && len(sticker.GetPackKey()) == PackKeyLength && !bytes.Equal(sticker.GetPackId(), zeroPackID) {
+ converted.Content.Info.BridgedSticker = &event.BridgedSticker{
+ Network: StickerSourceID,
+ ID: strconv.FormatUint(uint64(sticker.GetStickerId()), 10),
+ Emoji: sticker.GetEmoji(),
+ PackURL: fmt.Sprintf(PackURLFormat, sticker.GetPackId(), sticker.GetPackKey()),
+ }
+ }
converted.Type = event.EventSticker
converted.Content.MsgType = ""
- if converted.Extra == nil {
- converted.Extra = map[string]any{}
- }
- // TODO fetch full pack metadata like the old bridge did?
- converted.Extra["fi.mau.signal.sticker"] = map[string]any{
- "id": sticker.GetStickerId(),
- "emoji": sticker.GetEmoji(),
- "pack": map[string]any{
- "id": sticker.GetPackId(),
- "key": sticker.GetPackKey(),
- },
- }
return converted
}
diff --git a/pkg/msgconv/imagepack.go b/pkg/msgconv/imagepack.go
new file mode 100644
index 0000000..a2529af
--- /dev/null
+++ b/pkg/msgconv/imagepack.go
@@ -0,0 +1,199 @@
+// mautrix-signal - A Matrix-Signal puppeting bridge.
+// Copyright (C) 2026 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 msgconv
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "fmt"
+ "net/url"
+ "strconv"
+ "strings"
+
+ "go.mau.fi/util/emojishortcodes"
+ "google.golang.org/protobuf/proto"
+ "maunium.net/go/mautrix"
+ "maunium.net/go/mautrix/bridgev2"
+ "maunium.net/go/mautrix/bridgev2/database"
+ "maunium.net/go/mautrix/event"
+ "maunium.net/go/mautrix/id"
+
+ "go.mau.fi/mautrix-signal/pkg/signalid"
+ "go.mau.fi/mautrix-signal/pkg/signalmeow"
+ signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
+)
+
+const StickerSourceID = "signal"
+const PackURLFormat = "https://signal.art/addstickers/#pack_id=%x&pack_key=%x"
+
+const PackIDLength = 16
+const PackKeyLength = 32
+const PackURLLength = len(PackURLFormat) - len("%x")*2 + PackIDLength*2 + PackKeyLength*2
+
+var zeroPackID = make([]byte, PackIDLength)
+
+func ParseStickerMeta(info *event.BridgedSticker) *signalpb.DataMessage_Sticker {
+ if info == nil || info.Network != StickerSourceID || len(info.PackURL) != PackURLLength {
+ return nil
+ }
+ stickerID, err := strconv.ParseUint(info.ID, 10, 32)
+ if err != nil {
+ return nil
+ }
+ packID, packKey, err := parsePackURL(info.PackURL)
+ if err != nil || len(packID) != PackIDLength || len(packKey) != PackKeyLength || bytes.Equal(packID, zeroPackID) {
+ return nil
+ }
+ return &signalpb.DataMessage_Sticker{
+ PackId: packID,
+ PackKey: packKey,
+ StickerId: proto.Uint32(uint32(stickerID)),
+ Emoji: &info.Emoji,
+ }
+}
+
+func parsePackURL(rawURL string) (packID, packKey []byte, err error) {
+ parsed, err := url.Parse(rawURL)
+ if err != nil {
+ return nil, nil, fmt.Errorf("invalid URL: %w", err)
+ } else if parsed.Host != "signal.art" || !strings.HasPrefix(parsed.Path, "/addstickers") {
+ return nil, nil, fmt.Errorf("invalid host or path in URL")
+ }
+ q, err := url.ParseQuery(parsed.Fragment)
+ if err != nil {
+ return nil, nil, fmt.Errorf("invalid URL fragment: %w", err)
+ }
+ packID, err = hex.DecodeString(q.Get("pack_id"))
+ if err != nil {
+ return nil, nil, fmt.Errorf("invalid pack ID in URL: %w", err)
+ }
+ packKey, err = hex.DecodeString(q.Get("pack_key"))
+ if err != nil {
+ return nil, nil, fmt.Errorf("invalid pack key in URL: %w", err)
+ }
+ return
+}
+
+func (mc *MessageConverter) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
+ packID, packKey, err := parsePackURL(url)
+ if err != nil {
+ return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
+ }
+ manifest, err := signalmeow.DownloadStickerPackManifest(ctx, packID, packKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to download sticker pack manifest: %w", err)
+ }
+ topLevelExtra := map[string]any{
+ "fi.mau.signal.stickerpack": map[string]any{
+ "pack_id": hex.EncodeToString(packID),
+ "pack_key": hex.EncodeToString(packKey),
+ },
+ }
+ content := &event.ImagePackEventContent{
+ Images: make(map[string]*event.ImagePackImage, len(manifest.Stickers)),
+ Metadata: event.ImagePackMetadata{
+ DisplayName: manifest.GetTitle(),
+ AvatarURL: "",
+ Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
+ Attribution: manifest.GetAuthor(),
+ BridgedPack: &event.BridgedStickerPack{
+ Network: StickerSourceID,
+ URL: fmt.Sprintf(PackURLFormat, packID, packKey),
+ },
+ },
+ }
+ imagesByID := make(map[uint32]id.ContentURIString, len(manifest.Stickers))
+ uploadImage := func(sticker *signalpb.Pack_Sticker) (id.ContentURIString, error) {
+ stickerID := sticker.GetId()
+ existing, ok := imagesByID[stickerID]
+ if ok {
+ return existing, nil
+ }
+ var mxc id.ContentURIString
+ if mc.DirectMedia {
+ mediaID, err := signalid.DirectMediaSticker{
+ PackID: packID,
+ PackKey: packKey,
+ StickerID: stickerID,
+ }.AsMediaID()
+ if err != nil {
+ return "", fmt.Errorf("failed to create media ID for sticker %d: %w", stickerID, err)
+ }
+ mxc, err = mc.Bridge.Matrix.GenerateContentURI(ctx, mediaID)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate content URI for sticker %d: %w", stickerID, err)
+ }
+ } else {
+ dbKey := database.Key(fmt.Sprintf("stickercache:%x:%d", packID, stickerID))
+ if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
+ mxc = id.ContentURIString(cached)
+ imagesByID[stickerID] = mxc
+ return mxc, nil
+ }
+ data, err := signalmeow.DownloadStickerPackItem(ctx, packID, packKey, stickerID)
+ if err != nil {
+ return "", fmt.Errorf("failed to download sticker %d: %w", stickerID, err)
+ }
+ mxc, _, err = mc.Bridge.Bot.UploadMedia(ctx, "", data, "", sticker.GetContentType())
+ if err != nil {
+ return "", fmt.Errorf("failed to upload sticker %d: %w", stickerID, err)
+ }
+ mc.Bridge.DB.KV.Set(ctx, dbKey, string(mxc))
+ }
+ imagesByID[stickerID] = mxc
+ return mxc, nil
+ }
+ for _, sticker := range manifest.Stickers {
+ mxc, err := uploadImage(sticker)
+ if err != nil {
+ return nil, err
+ }
+ shortcode := emojishortcodes.Get(sticker.GetEmoji())
+ realShortcode := shortcode
+ i := 2
+ for _, alreadyExists := content.Images[realShortcode]; alreadyExists; i++ {
+ realShortcode = fmt.Sprintf("%s_%d", shortcode, i)
+ }
+ content.Images[realShortcode] = &event.ImagePackImage{
+ URL: mxc,
+ Body: sticker.GetEmoji(),
+ Info: &event.FileInfo{
+ MimeType: sticker.GetContentType(),
+ Width: 200,
+ Height: 200,
+ BridgedSticker: &event.BridgedSticker{
+ Network: StickerSourceID,
+ ID: strconv.FormatUint(uint64(sticker.GetId()), 10),
+ Emoji: sticker.GetEmoji(),
+ PackURL: content.Metadata.BridgedPack.URL,
+ },
+ },
+ }
+ }
+ if manifest.Cover != nil {
+ content.Metadata.AvatarURL, err = uploadImage(manifest.Cover)
+ if err != nil {
+ return nil, fmt.Errorf("failed to upload sticker pack cover: %w", err)
+ }
+ }
+ return &bridgev2.ImportedImagePack{
+ Content: content,
+ Extra: topLevelExtra,
+ Shortcode: hex.EncodeToString(packID),
+ }, nil
+}
diff --git a/pkg/signalid/media.go b/pkg/signalid/media.go
index 8c91b6a..a530c22 100644
--- a/pkg/signalid/media.go
+++ b/pkg/signalid/media.go
@@ -34,6 +34,7 @@ const (
directMediaTypeGroupAvatar directMediaType = 1
directMediaTypeProfileAvatar directMediaType = 2
directMediaTypePlaintextDigestAttachment directMediaType = 3
+ directMediaTypeSticker directMediaType = 4
)
type DirectMediaInfo interface {
@@ -44,6 +45,7 @@ var (
_ DirectMediaInfo = (*DirectMediaAttachment)(nil)
_ DirectMediaInfo = (*DirectMediaGroupAvatar)(nil)
_ DirectMediaInfo = (*DirectMediaProfileAvatar)(nil)
+ _ DirectMediaInfo = (*DirectMediaSticker)(nil)
)
type DirectMediaAttachment struct {
@@ -127,6 +129,30 @@ func (m DirectMediaProfileAvatar) AsMediaID() (mediaID networkid.MediaID, err er
return networkid.MediaID(buf.Bytes()), nil
}
+type DirectMediaSticker struct {
+ PackID []byte
+ PackKey []byte
+ StickerID uint32
+}
+
+const packIDLen = 16
+const packKeyLen = 32
+const directMediaStickerLen = 1 + packIDLen + packKeyLen + 4
+
+func (m DirectMediaSticker) AsMediaID() (mediaID networkid.MediaID, err error) {
+ if len(m.PackID) != packIDLen {
+ return nil, fmt.Errorf("invalid pack ID length: %d", len(m.PackID))
+ } else if len(m.PackKey) != packKeyLen {
+ return nil, fmt.Errorf("invalid pack key length: %d", len(m.PackKey))
+ }
+ mediaID = make(networkid.MediaID, directMediaStickerLen)
+ mediaID[0] = byte(directMediaTypeSticker)
+ copy(mediaID[1:], m.PackID)
+ copy(mediaID[1+packIDLen:], m.PackKey)
+ binary.BigEndian.PutUint32(mediaID[1+packIDLen+packKeyLen:], m.StickerID)
+ return mediaID, nil
+}
+
func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err error) {
mediaIDLen := len(mediaID)
if mediaIDLen == 0 {
@@ -200,6 +226,15 @@ func ParseDirectMediaInfo(mediaID networkid.MediaID) (_ DirectMediaInfo, err err
info.ProfileAvatarPath = string(profileAvatarPath)
}
return &info, nil
+ case directMediaTypeSticker:
+ var info DirectMediaSticker
+ if len(mediaID) != directMediaStickerLen {
+ return info, fmt.Errorf("invalid media ID length for sticker: %d", len(mediaID))
+ }
+ info.PackID = mediaID[1 : 1+packIDLen]
+ info.PackKey = mediaID[1+packIDLen : 1+packIDLen+packKeyLen]
+ info.StickerID = binary.BigEndian.Uint32(mediaID[1+packIDLen+packKeyLen:])
+ return &info, nil
}
return nil, fmt.Errorf("invalid direct media type %d", mediaType)
diff --git a/pkg/signalmeow/attachments.go b/pkg/signalmeow/attachments.go
index a48414e..c091827 100644
--- a/pkg/signalmeow/attachments.go
+++ b/pkg/signalmeow/attachments.go
@@ -35,6 +35,7 @@ import (
"github.com/rs/zerolog"
"go.mau.fi/util/fallocate"
+ "go.mau.fi/util/pkcs7"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"
@@ -136,6 +137,15 @@ func DownloadAttachment(
const MACLength = 32
const IVLength = 16
+func macAndAESDecrypt(body, key []byte) ([]byte, error) {
+ l := len(body) - MACLength
+ if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
+ return nil, ErrInvalidMACForAttachment
+ }
+
+ return aesDecrypt(key[:MACLength], body[:l])
+}
+
func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint32) ([]byte, error) {
if !plaintextDigest {
hash := sha256.Sum256(body)
@@ -143,12 +153,7 @@ func decryptAttachment(body, key, digest []byte, plaintextDigest bool, size uint
return nil, ErrInvalidDigestForAttachment
}
}
- l := len(body) - MACLength
- if !verifyMAC(key[MACLength:], body[:l], body[l:]) {
- return nil, ErrInvalidMACForAttachment
- }
-
- decrypted, err := aesDecrypt(key[:MACLength], body[:l])
+ decrypted, err := macAndAESDecrypt(body, key)
if err != nil {
return nil, err
}
@@ -240,6 +245,14 @@ func extend(data []byte, paddedLen int) []byte {
}
}
+func macAndAESEncrypt(keys, plaintext []byte) ([]byte, error) {
+ encrypted, err := aesEncrypt(keys[:32], plaintext)
+ if err != nil {
+ return nil, err
+ }
+ return appendMAC(keys[32:], encrypted), nil
+}
+
func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb.AttachmentPointer, error) {
log := zerolog.Ctx(ctx).With().Str("func", "upload attachment").Logger()
keys := random.Bytes(64) // combined AES and MAC keys
@@ -255,11 +268,10 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
}
body = extend(body, paddedLen)
- encrypted, err := aesEncrypt(keys[:32], body)
+ encryptedWithMAC, err := macAndAESEncrypt(keys, body)
if err != nil {
return nil, err
}
- encryptedWithMAC := appendMAC(keys[32:], encrypted)
// Get upload attributes from Signal server
attributesPath := "/v4/attachments/form/upload"
@@ -467,13 +479,10 @@ func aesDecrypt(key, ciphertext []byte) ([]byte, error) {
}
iv := ciphertext[:IVLength]
+ ciphertext = ciphertext[IVLength:]
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(ciphertext, ciphertext)
- pad := ciphertext[len(ciphertext)-1]
- if pad > aes.BlockSize {
- return nil, fmt.Errorf("pad value (%d) larger than AES blocksize (%d)", pad, aes.BlockSize)
- }
- return ciphertext[aes.BlockSize : len(ciphertext)-int(pad)], nil
+ return pkcs7.Unpad(ciphertext)
}
func aesDecryptFile(key []byte, file *os.File, downloadedSize int64) (int64, error) {
@@ -533,14 +542,11 @@ func aesEncrypt(key, plaintext []byte) ([]byte, error) {
return nil, err
}
- pad := aes.BlockSize - len(plaintext)%aes.BlockSize
- plaintext = append(plaintext, bytes.Repeat([]byte{byte(pad)}, pad)...)
-
- ciphertext := make([]byte, len(plaintext))
+ plaintext = pkcs7.Pad(plaintext, aes.BlockSize)
iv := random.Bytes(16)
mode := cipher.NewCBCEncrypter(block, iv)
- mode.CryptBlocks(ciphertext, plaintext)
+ mode.CryptBlocks(plaintext, plaintext)
- return append(iv, ciphertext...), nil
+ return append(iv, plaintext...), nil
}
diff --git a/pkg/signalmeow/keys.go b/pkg/signalmeow/keys.go
index f1801e5..5439755 100644
--- a/pkg/signalmeow/keys.go
+++ b/pkg/signalmeow/keys.go
@@ -413,6 +413,10 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
if cli.Store.RecipientStore.IsUnregistered(ctx, theirServiceID) {
return fmt.Errorf("%w (cached)", ErrUnregisteredUser)
}
+ localAddress, err := cli.Store.ACIServiceID().Address(uint(cli.Store.DeviceID))
+ if err != nil {
+ return fmt.Errorf("failed to get own address: %w", err)
+ }
// Fetch prekey
deviceIDPath := "/*"
if specificDeviceID >= 0 {
@@ -518,6 +522,7 @@ func (cli *Client) FetchAndProcessPreKey(ctx context.Context, theirServiceID lib
ctx,
preKeyBundle,
address,
+ localAddress,
cli.Store.ACISessionStore,
cli.Store.ACIIdentityStore,
)
diff --git a/pkg/signalmeow/protobuf/ContactDiscovery.pb.go b/pkg/signalmeow/protobuf/ContactDiscovery.pb.go
index 637a2d2..5cb232c 100644
--- a/pkg/signalmeow/protobuf/ContactDiscovery.pb.go
+++ b/pkg/signalmeow/protobuf/ContactDiscovery.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: ContactDiscovery.proto
// Copyright 2021 Signal Messenger, LLC
diff --git a/pkg/signalmeow/protobuf/DeviceName.pb.go b/pkg/signalmeow/protobuf/DeviceName.pb.go
index 5666b7e..31b5704 100644
--- a/pkg/signalmeow/protobuf/DeviceName.pb.go
+++ b/pkg/signalmeow/protobuf/DeviceName.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: DeviceName.proto
// Copyright 2018 Signal Messenger, LLC
diff --git a/pkg/signalmeow/protobuf/Groups.pb.go b/pkg/signalmeow/protobuf/Groups.pb.go
index 0c2b81b..8d4e2e3 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.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: Groups.proto
package signalpb
diff --git a/pkg/signalmeow/protobuf/Provisioning.pb.go b/pkg/signalmeow/protobuf/Provisioning.pb.go
index c925fe6..88ebe90 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.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: Provisioning.proto
package signalpb
diff --git a/pkg/signalmeow/protobuf/SignalService.pb.go b/pkg/signalmeow/protobuf/SignalService.pb.go
index 32842bf..c4268dd 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.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: SignalService.proto
package signalpb
diff --git a/pkg/signalmeow/protobuf/StickerResources.pb.go b/pkg/signalmeow/protobuf/StickerResources.pb.go
index e83cda1..f8194aa 100644
--- a/pkg/signalmeow/protobuf/StickerResources.pb.go
+++ b/pkg/signalmeow/protobuf/StickerResources.pb.go
@@ -6,7 +6,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: StickerResources.proto
package signalpb
diff --git a/pkg/signalmeow/protobuf/StorageService.pb.go b/pkg/signalmeow/protobuf/StorageService.pb.go
index 619221f..bbe88ef 100644
--- a/pkg/signalmeow/protobuf/StorageService.pb.go
+++ b/pkg/signalmeow/protobuf/StorageService.pb.go
@@ -6,7 +6,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: StorageService.proto
package signalpb
@@ -1401,6 +1401,7 @@ type GroupV2Record struct {
HideStory bool `protobuf:"varint,8,opt,name=hideStory,proto3" json:"hideStory,omitempty"`
StorySendMode GroupV2Record_StorySendMode `protobuf:"varint,10,opt,name=storySendMode,proto3,enum=signalservice.GroupV2Record_StorySendMode" json:"storySendMode,omitempty"`
AvatarColor *AvatarColor `protobuf:"varint,11,opt,name=avatarColor,proto3,enum=signalservice.AvatarColor,oneof" json:"avatarColor,omitempty"`
+ VerifiedNameHash []byte `protobuf:"bytes,12,opt,name=verifiedNameHash,proto3" json:"verifiedNameHash,omitempty"` // SHA-256 of UTF-8 encoded decrypted group title that was last verified
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -1505,6 +1506,13 @@ func (x *GroupV2Record) GetAvatarColor() AvatarColor {
return AvatarColor_A100
}
+func (x *GroupV2Record) GetVerifiedNameHash() []byte {
+ if x != nil {
+ return x.VerifiedNameHash
+ }
+ return nil
+}
+
type Payments struct {
state protoimpl.MessageState `protogen:"open.v1"`
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
@@ -3195,7 +3203,7 @@ const file_StorageService_proto_rawDesc = "" +
"\vwhitelisted\x18\x03 \x01(\bR\vwhitelisted\x12\x1a\n" +
"\barchived\x18\x04 \x01(\bR\barchived\x12\"\n" +
"\fmarkedUnread\x18\x05 \x01(\bR\fmarkedUnread\x120\n" +
- "\x13mutedUntilTimestamp\x18\x06 \x01(\x04R\x13mutedUntilTimestamp\"\xa1\x04\n" +
+ "\x13mutedUntilTimestamp\x18\x06 \x01(\x04R\x13mutedUntilTimestamp\"\xcd\x04\n" +
"\rGroupV2Record\x12\x1c\n" +
"\tmasterKey\x18\x01 \x01(\fR\tmasterKey\x12\x18\n" +
"\ablocked\x18\x02 \x01(\bR\ablocked\x12 \n" +
@@ -3207,7 +3215,8 @@ const file_StorageService_proto_rawDesc = "" +
"\thideStory\x18\b \x01(\bR\thideStory\x12P\n" +
"\rstorySendMode\x18\n" +
" \x01(\x0e2*.signalservice.GroupV2Record.StorySendModeR\rstorySendMode\x12A\n" +
- "\vavatarColor\x18\v \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\"7\n" +
+ "\vavatarColor\x18\v \x01(\x0e2\x1a.signalservice.AvatarColorH\x00R\vavatarColor\x88\x01\x01\x12*\n" +
+ "\x10verifiedNameHash\x18\f \x01(\fR\x10verifiedNameHash\"7\n" +
"\rStorySendMode\x12\v\n" +
"\aDEFAULT\x10\x00\x12\f\n" +
"\bDISABLED\x10\x01\x12\v\n" +
diff --git a/pkg/signalmeow/protobuf/StorageService.proto b/pkg/signalmeow/protobuf/StorageService.proto
index d22babc..dd232ca 100644
--- a/pkg/signalmeow/protobuf/StorageService.proto
+++ b/pkg/signalmeow/protobuf/StorageService.proto
@@ -172,6 +172,7 @@ message GroupV2Record {
reserved /* storySendEnabled */ 9;
StorySendMode storySendMode = 10;
optional AvatarColor avatarColor = 11;
+ bytes verifiedNameHash = 12; // SHA-256 of UTF-8 encoded decrypted group title that was last verified
}
message Payments {
diff --git a/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go b/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go
index e30f6d6..5979a4c 100644
--- a/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go
+++ b/pkg/signalmeow/protobuf/UnidentifiedDelivery.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: UnidentifiedDelivery.proto
// Copyright 2018 Signal Messenger, LLC
diff --git a/pkg/signalmeow/protobuf/WebSocketResources.pb.go b/pkg/signalmeow/protobuf/WebSocketResources.pb.go
index f35110d..66520eb 100644
--- a/pkg/signalmeow/protobuf/WebSocketResources.pb.go
+++ b/pkg/signalmeow/protobuf/WebSocketResources.pb.go
@@ -6,7 +6,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: WebSocketResources.proto
package signalpb
diff --git a/pkg/signalmeow/protobuf/backuppb/Backup.pb.go b/pkg/signalmeow/protobuf/backuppb/Backup.pb.go
index 326c170..bc488e7 100644
--- a/pkg/signalmeow/protobuf/backuppb/Backup.pb.go
+++ b/pkg/signalmeow/protobuf/backuppb/Backup.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
-// protoc v6.33.5
+// protoc v7.34.1
// source: backuppb/Backup.proto
package backuppb
diff --git a/pkg/signalmeow/protobuf/update-protos.sh b/pkg/signalmeow/protobuf/update-protos.sh
index ffaa697..f9c86fc 100755
--- a/pkg/signalmeow/protobuf/update-protos.sh
+++ b/pkg/signalmeow/protobuf/update-protos.sh
@@ -1,8 +1,8 @@
#!/bin/bash
set -euo pipefail
-ANDROID_GIT_REVISION=${1:-dfd2f7baf96825834f784900ce644e9ead8a9a89}
-DESKTOP_GIT_REVISION=${1:-60a1e125452ee672d8747564d0055d5bfec9f679}
+ANDROID_GIT_REVISION=${1:-439760e7732585bfd078d92d93732c04cc31e29e}
+DESKTOP_GIT_REVISION=${1:-1b2a3e7b283c32c5654a39da12fc04139fd26dbd}
update_proto() {
case "$1" in
diff --git a/pkg/signalmeow/receiving_decrypt.go b/pkg/signalmeow/receiving_decrypt.go
index 6296f00..1d2c8cc 100644
--- a/pkg/signalmeow/receiving_decrypt.go
+++ b/pkg/signalmeow/receiving_decrypt.go
@@ -243,11 +243,16 @@ func (cli *Client) decryptCiphertextEnvelope(
if identityStore == nil {
return nil, fmt.Errorf("no identity store for destination service ID %s", destinationServiceID)
}
+ destinationAddress, err := destinationServiceID.Address(uint(cli.Store.DeviceID))
+ if err != nil {
+ return nil, fmt.Errorf("failed to get own address: %w", err)
+ }
plaintext, ciphertextHash, err := cli.bufferedDecryptTxn(ctx, ciphertext, serverTimestamp, func(ctx context.Context) ([]byte, error) {
return libsignalgo.Decrypt(
ctx,
message,
senderAddress,
+ destinationAddress,
sessionStore,
identityStore,
)
diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go
index 769798a..a485b54 100644
--- a/pkg/signalmeow/sending.go
+++ b/pkg/signalmeow/sending.go
@@ -384,15 +384,7 @@ func syncMessageFromSoloEditMessage(editMessage *signalpb.EditMessage, result Su
}
func syncMessageFromReadReceiptMessage(ctx context.Context, receiptMessage *signalpb.ReceiptMessage, messageSender libsignalgo.ServiceID) *signalpb.Content {
- if *receiptMessage.Type != signalpb.ReceiptMessage_READ {
- zerolog.Ctx(ctx).Warn().
- Any("receipt_message_type", receiptMessage.Type).
- Msg("syncMessageFromReadReceiptMessage called with non-read receipt message")
- return nil
- } else if messageSender.Type != libsignalgo.ServiceIDTypeACI {
- zerolog.Ctx(ctx).Warn().
- Stringer("message_sender", messageSender).
- Msg("syncMessageFromReadReceiptMessage called with non-ACI message sender")
+ if *receiptMessage.Type != signalpb.ReceiptMessage_READ || messageSender.Type != libsignalgo.ServiceIDTypeACI {
return nil
}
read := []*signalpb.SyncMessage_Read{}
diff --git a/pkg/signalmeow/sticker.go b/pkg/signalmeow/sticker.go
new file mode 100644
index 0000000..2759d18
--- /dev/null
+++ b/pkg/signalmeow/sticker.go
@@ -0,0 +1,251 @@
+// mautrix-signal - A Matrix-signal puppeting bridge.
+// Copyright (C) 2026 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 signalmeow
+
+import (
+ "bytes"
+ "context"
+ "crypto/hkdf"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "net/textproto"
+ "sync"
+
+ "go.mau.fi/util/exerrors"
+ "go.mau.fi/util/random"
+ "golang.org/x/sync/semaphore"
+ "google.golang.org/protobuf/proto"
+
+ signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
+ "go.mau.fi/mautrix-signal/pkg/signalmeow/web"
+)
+
+func DownloadStickerPackManifest(ctx context.Context, packID, packKey []byte) (*signalpb.Pack, error) {
+ if len(packID) != 16 {
+ return nil, fmt.Errorf("invalid pack ID length: %d", len(packID))
+ }
+ resp, err := downloadStickerData(ctx, fmt.Sprintf("/stickers/%x/manifest.proto", packID), packKey)
+ if err != nil {
+ return nil, err
+ }
+ var pack signalpb.Pack
+ err = proto.Unmarshal(resp, &pack)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal decrypted manifest: %w", err)
+ }
+ return &pack, nil
+}
+
+func DownloadStickerPackItem(ctx context.Context, packID, packKey []byte, stickerID uint32) ([]byte, error) {
+ if len(packID) != 16 {
+ return nil, fmt.Errorf("invalid pack ID length: %d", len(packID))
+ }
+ return downloadStickerData(ctx, fmt.Sprintf("/stickers/%x/full/%d", packID, stickerID), packKey)
+}
+
+func downloadStickerData(ctx context.Context, path string, packKey []byte) ([]byte, error) {
+ if len(packKey) != 32 {
+ return nil, fmt.Errorf("invalid pack key length: %d", len(packKey))
+ }
+ var body, decrypted []byte
+ resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodGet, path, nil)
+ defer web.CloseBody(resp)
+ if err != nil {
+ return nil, fmt.Errorf("failed to make request: %w", err)
+ } else if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
+ } else if body, err = io.ReadAll(resp.Body); err != nil {
+ return nil, fmt.Errorf("failed to read response: %w", err)
+ } else if decrypted, err = decryptSticker(packKey, body); err != nil {
+ return nil, fmt.Errorf("failed to decrypt response: %w", err)
+ } else {
+ return decrypted, nil
+ }
+}
+
+type stickerUploadAttributes struct {
+ ACL string `json:"acl"`
+ Algorithm string `json:"algorithm"`
+ Credential string `json:"credential"`
+ Date string `json:"date"`
+ ID int `json:"id"`
+ Key string `json:"key"`
+ Policy string `json:"policy"`
+ Signature string `json:"signature"`
+}
+
+func (sua *stickerUploadAttributes) makeFormBody(encryptedData []byte) (*web.HTTPReqOpt, error) {
+ var buf bytes.Buffer
+ writer := multipart.NewWriter(&buf)
+ var closed bool
+ // This isn't necessary in practice, just do it to avoid linter warnings
+ defer func() {
+ if !closed {
+ _ = writer.Close()
+ }
+ }()
+ fields := map[string]string{
+ "key": sua.Key,
+ "acl": sua.ACL,
+ "policy": sua.Policy,
+ "x-amz-algorithm": sua.Algorithm,
+ "x-amz-credential": sua.Credential,
+ "x-amz-date": sua.Date,
+ "Content-Type": "application/octet-stream",
+ }
+ for key, value := range fields {
+ err := writer.WriteField(key, value)
+ if err != nil {
+ return nil, fmt.Errorf("failed to write multipart field %s: %w", key, err)
+ }
+ }
+ filePart, err := writer.CreatePart(textproto.MIMEHeader{
+ "Content-Type": []string{"application/octet-stream"},
+ "Content-Disposition": []string{`form-data; name="file"`},
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create multipart file part: %w", err)
+ }
+ _, err = filePart.Write(encryptedData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to write file data to multipart body: %w", err)
+ }
+ err = writer.Close()
+ if err != nil {
+ return nil, fmt.Errorf("failed to close multipart writer: %w", err)
+ }
+ closed = true
+ return &web.HTTPReqOpt{
+ Body: buf.Bytes(),
+ ContentType: web.ContentType(writer.FormDataContentType()),
+ }, nil
+}
+
+func (sua *stickerUploadAttributes) upload(ctx context.Context, packKey, fileData []byte) error {
+ encryptedData, err := macAndAESEncrypt(fileData, deriveStickerPackKey(packKey))
+ if err != nil {
+ return fmt.Errorf("failed to encrypt sticker data: %w", err)
+ }
+ req, err := sua.makeFormBody(encryptedData)
+ if err != nil {
+ return fmt.Errorf("failed to prepare request: %w", err)
+ }
+ resp, err := web.SendHTTPRequest(ctx, web.CDN1Hostname, http.MethodPost, "/", req)
+ if err != nil {
+ return err
+ }
+ _ = resp.Body.Close()
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("unexpected status code %d", resp.StatusCode)
+ }
+ return nil
+}
+
+func (sua *stickerUploadAttributes) uploadAsync(
+ ctx context.Context,
+ packKey []byte,
+ getFileData func(context.Context) ([]byte, error),
+ sema *semaphore.Weighted,
+ done func(),
+ onError func(error),
+) {
+ defer done()
+ err := sema.Acquire(ctx, 1)
+ if err != nil {
+ return
+ }
+ defer sema.Release(1)
+ fileData, err := getFileData(ctx)
+ if err == nil {
+ err = sua.upload(ctx, packKey, fileData)
+ }
+ if err != nil {
+ onError(err)
+ }
+}
+
+type stickerPackUploadAttributes struct {
+ PackID string `json:"packId"`
+ Manifest *stickerUploadAttributes `json:"manifest"`
+ Stickers []*stickerUploadAttributes `json:"stickers"`
+}
+
+var StickerUploadParallelism = 4
+
+func (cli *Client) UploadStickerPack(ctx context.Context, pack *signalpb.Pack, stickerData []func(context.Context) ([]byte, error)) (packID, packKey []byte, err error) {
+ for i, sticker := range pack.Stickers {
+ if sticker.GetId() >= uint32(len(stickerData)) {
+ return nil, nil, fmt.Errorf("sticker ID %d at index %d is out of bounds, only %d sticker blobs provided", sticker.GetId(), i, len(stickerData))
+ }
+ }
+ marshaledPack, err := proto.Marshal(pack)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to marshal pack: %w", err)
+ }
+ packKey = random.Bytes(32)
+ resp, err := cli.AuthedWS.SendRequest(ctx, http.MethodGet, fmt.Sprintf("/v1/sticker/pack/form/%d", len(stickerData)), nil, nil)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get upload form: %w", err)
+ }
+ var packAttributes stickerPackUploadAttributes
+ err = web.DecodeWSResponseBody(ctx, &packAttributes, resp)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to decode pack attributes: %w", err)
+ }
+ if len(packAttributes.Stickers) != len(stickerData) {
+ return nil, nil, fmt.Errorf("expected %d sticker upload attribute sets, got %d", len(stickerData), len(packAttributes.Stickers))
+ }
+ packID, err = hex.DecodeString(packAttributes.PackID)
+ if err != nil {
+ return nil, nil, fmt.Errorf("invalid pack ID in response: %w", err)
+ }
+ err = packAttributes.Manifest.upload(ctx, packKey, marshaledPack)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to upload manifest: %w", err)
+ }
+ var wg sync.WaitGroup
+ wg.Add(len(packAttributes.Stickers))
+ sema := semaphore.NewWeighted(int64(StickerUploadParallelism))
+ var errorList []error
+ var errorLock sync.Mutex
+ for i, attrs := range packAttributes.Stickers {
+ go attrs.uploadAsync(ctx, packKey, stickerData[i], sema, wg.Done, func(err error) {
+ errorLock.Lock()
+ errorList = append(errorList, fmt.Errorf("failed to upload sticker #%d: %w", i+1, err))
+ errorLock.Unlock()
+ })
+ }
+ wg.Wait()
+ err = ctx.Err()
+ if err == nil {
+ err = errors.Join(errorList...)
+ }
+ return
+}
+
+func decryptSticker(packKey, ciphertext []byte) ([]byte, error) {
+ return macAndAESDecrypt(ciphertext, deriveStickerPackKey(packKey))
+}
+
+func deriveStickerPackKey(key []byte) []byte {
+ return exerrors.Must(hkdf.Key(sha256.New, key, make([]byte, 32), "Sticker Pack", 2*32))
+}
diff --git a/pkg/signalmeow/web/web.go b/pkg/signalmeow/web/web.go
index d0fcd01..f211b77 100644
--- a/pkg/signalmeow/web/web.go
+++ b/pkg/signalmeow/web/web.go
@@ -28,6 +28,7 @@ import (
"net/http"
"runtime"
"strings"
+ "time"
"github.com/rs/zerolog"
@@ -129,6 +130,7 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe
} else {
req.Header.Set("Content-Type", string(ContentTypeJSON))
}
+ req.ContentLength = int64(len(opt.Body))
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(opt.Body)))
req.Header.Set("User-Agent", UserAgent)
req.Header.Set("X-Signal-Agent", SignalAgent)
@@ -139,12 +141,14 @@ func SendHTTPRequest(ctx context.Context, host, method, path string, opt *HTTPRe
httpReqCounter++
log = log.With().Int("request_number", httpReqCounter).Logger()
log.Trace().Msg("Sending HTTP request")
+ start := time.Now()
resp, err := SignalHTTPClient.Do(req)
+ dur := time.Since(start)
if err != nil {
- log.Err(err).Msg("Error sending request")
+ log.Err(err).Dur("duration", dur).Msg("Error sending request")
return nil, err
}
- log.Debug().Int("status_code", resp.StatusCode).Msg("received HTTP response")
+ log.Debug().Int("status_code", resp.StatusCode).Dur("duration", dur).Msg("Received HTTP response")
return resp, nil
}