Watch
1
0
Fork
You've already forked mautrix-signal
0
mirror of https://github.com/mautrix/signal.git synced 2026-08-22 12:04:54 -04:00

Compare commits

...
Author SHA1 Message Date
Tulir Asokan
0d5f3f457d Bump version to v26.08 2026-08-16 15:48:54 +03:00
Nick Mills-Barrett
77a6664bc5
dependencies: update mautrix-go 2026-08-14 14:52:41 +01:00
Nick Mills-Barrett
771150ee4a
signalmeow/storageservice: store own profile key from account record (#664) 2026-08-14 14:02:14 +01:00
Nick Mills-Barrett
4eaacd6c56
login: use ErrInvalidLoginFlowID for unknown login flow IDs (#663) 2026-08-12 18:22:22 +01:00
Tulir Asokan
8c7333a033 msgconv/matrixfmt: fix tests
[skip cd]
2026-08-07 15:21:11 +03:00
Tulir Asokan
979d48aeb1 libsignalgo/conversions: use unsafe.Pointer for go -> c string too 2026-08-07 15:15:39 +03:00
Tulir Asokan
9142786157 libsignal: update to v0.100.0 2026-08-07 15:04:29 +03:00
Tulir Asokan
542e5a3284 msgconv/matrixfmt: fix bridging code blocks 2026-08-07 13:27:17 +03:00
Tulir Asokan
4f4f00423c login: remove unused step type constant 2026-08-07 13:27:06 +03:00
Nick Mills-Barrett
3ddb32dd23
handle*: insert stub message rows for old edit events (#661)
Co-authored-by: Tulir Asokan <tulir@maunium.net>
2026-08-06 19:15:21 +03:00
Bradley Birch
6e57bf3956
capabilities: add more details for polls (#659) 2026-07-23 14:46:26 +01:00
Nick Mills-Barrett
f25f588bc4
login: implement proper error handling (#657) 2026-07-21 17:58:23 +01:00
Tulir Asokan
bed128ab84 dependencies: update mautrix-go 2026-07-19 16:34:51 +03:00
Tulir Asokan
4bbfe7e2cc msgconv/from-signal: handle attachment mime types more like signal desktop 2026-07-16 21:07:56 +03:00
39 changed files with 6774 additions and 3448 deletions

View file

@ -1,3 +1,10 @@
# v26.08
* Updated libsignal to v0.100.0
* Added support for handling own profile key changes.
* Changed attachment bridging behavior to match Signal Desktop.
* Fixed edited messages being bridged twice if certain race conditions occurred.
# v26.07
* Updated Docker image to Alpine 3.24.

View file

@ -37,7 +37,7 @@ var m = mxmain.BridgeMain{
Name: "mautrix-signal",
URL: "https://github.com/mautrix/signal",
Description: "A Matrix-Signal puppeting bridge.",
Version: "26.07",
Version: "26.08",
SemCalVer: true,
Connector: &connector.SignalConnector{},

24
go.mod
View file

@ -2,7 +2,7 @@ module go.mau.fi/mautrix-signal
go 1.25.0
toolchain go1.26.5
toolchain go1.26.6
tool go.mau.fi/util/cmd/maubuild
@ -14,14 +14,14 @@ require (
github.com/rs/zerolog v1.35.1
github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.9.11
golang.org/x/crypto v0.54.0
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597
golang.org/x/net v0.57.0
go.mau.fi/util v0.10.0
golang.org/x/crypto v0.55.0
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297
golang.org/x/net v0.58.0
golang.org/x/sync v0.22.0
google.golang.org/protobuf v1.36.11
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.29.0
maunium.net/go/mautrix v0.30.0
)
require (
@ -32,8 +32,8 @@ 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.48 // indirect
github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
@ -41,11 +41,11 @@ require (
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.8.4 // indirect
github.com/yuin/goldmark v1.8.5 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/mod v0.40.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/text v0.41.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect

44
go.sum
View file

@ -30,10 +30,10 @@ 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.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs=
github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E=
github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -59,29 +59,29 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
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.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.11 h1:Cus1Lu/t7d3OG6VF4aYWvlUUS0Q4O1/lcpPNJZ0jsw0=
go.mau.fi/util v0.9.11/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.10.0 h1:vH9IXZmfBKa96p47HxrVqEPkrj02zDJg3o4EF172+Lk=
go.mau.fi/util v0.10.0/go.mod h1:uZwpm9sK4wO2Qqy+t6QoVq29szMsRxWXp9/BkQLG4xk=
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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g=
golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -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.29.0 h1:OkcBJF1dvp+93EgahxMxOUZZOrGTYculI9IprvRIMOQ=
maunium.net/go/mautrix v0.29.0/go.mod h1:LynuVr8N9nWsE1N4WAE+vItRACDB1pt9M3gN4SIBpeY=
maunium.net/go/mautrix v0.30.0 h1:bad+q7w5tLqiHpr+oUxVI+8m8ePbV3AvoFKg2jQzPyo=
maunium.net/go/mautrix v0.30.0/go.mod h1:bb0gjxbTFOqTaAYKGw5E7j9XROUR2Sl1Etm3IbmYXbo=

View file

@ -38,7 +38,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
base := "fi.mau.signal.capabilities.2026_07_14"
base := "fi.mau.signal.capabilities.2026_07_22"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@ -77,6 +77,7 @@ var signalCaps = &event.RoomFeatures{
"image/jpeg": event.CapLevelFullySupported,
"image/webp": event.CapLevelFullySupported,
"image/bmp": event.CapLevelFullySupported,
"image/avif": event.CapLevelFullySupported,
},
MaxWidth: 4096,
MaxHeight: 4096,
@ -98,6 +99,8 @@ var signalCaps = &event.RoomFeatures{
MimeTypes: map[string]event.CapabilitySupportLevel{
"audio/aac": event.CapLevelFullySupported,
"audio/mpeg": event.CapLevelFullySupported,
"audio/mp3": event.CapLevelFullySupported,
"audio/flac": event.CapLevelFullySupported,
},
MaxSize: MaxFileSize,
},
@ -154,6 +157,11 @@ var signalCaps = &event.RoomFeatures{
MaxTextLength: MaxTextLength, // TODO support arbitrary sized text messages with files
LocationMessage: event.CapLevelPartialSupport,
Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported,
PollHiddenVotes: event.CapLevelUnsupported,
PollDuplicateOptions: event.CapLevelFullySupported,
PollMaxOptions: 10,
PollOptionMaxLength: 100,
Thread: event.CapLevelUnsupported,
Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported,
@ -237,5 +245,5 @@ func (s *SignalConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities
}
func (s *SignalConnector) GetBridgeInfoVersion() (info, capabilities int) {
return 1, 9
return 1, 11
}

View file

@ -180,12 +180,43 @@ func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.Matri
if err != nil {
return bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
}
prevID := msg.EditTarget.ID
msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts)
msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0}
msg.EditTarget.EditCount++
if prevID != msg.EditTarget.ID {
err = s.Main.Bridge.DB.Message.Update(ctx, msg.EditTarget)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save message after editing")
} else {
saveEditStub(ctx, s.Main.Bridge, prevID, msg.EditTarget)
}
}
return nil
}
// saveEditStub saves a placeholder message row pointing at the pre-edit ID of a message, such that
// duplicate checks on incoming edits find it and are dropped. This is necessary because the first
// time we see an edit it modifies the ID in place.
func saveEditStub(ctx context.Context, bridge *bridgev2.Bridge, prevID networkid.MessageID, target *database.Message) {
stub := &database.Message{
ID: prevID,
PartID: editStubPartID,
Room: target.Room,
SenderID: target.SenderID,
SenderMXID: target.SenderMXID,
Timestamp: target.Timestamp,
}
stub.SetFakeMXID()
err := bridge.DB.Message.Insert(ctx, stub)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
Str("prev_message_id", string(prevID)).
Str("message_id", string(target.ID)).
Msg("Failed to save stub row for pre-edit message ID")
}
}
func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
return bridgev2.MatrixReactionPreResponse{
SenderID: signalid.MakeUserID(s.Client.Store.ACI),

View file

@ -20,6 +20,7 @@ import (
"context"
"encoding/base64"
"fmt"
"slices"
"strings"
"time"
@ -358,20 +359,52 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
return converted, nil
}
const editStubPartID networkid.PartID = "editstub"
func isEditStub(msg *database.Message) bool {
return msg.PartID == editStubPartID
}
// editStubMessage returns a non-bridged message part which is saved as a placeholder row pointing
// at the pre-edit ID of a message, such that duplicate checks on incoming edits find it and are
// dropped. This is necessary because the first time we see an edit it modifies the ID in place.
func editStubMessage() *bridgev2.ConvertedMessage {
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
ID: editStubPartID,
Type: event.EventMessage,
Content: &event.MessageEventContent{},
DontBridge: true,
}},
}
}
func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
editMsg, ok := evt.Event.(*signalpb.EditMessage)
if !ok {
return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
}
existing = slices.DeleteFunc(slices.Clone(existing), isEditStub)
if len(existing) == 0 {
return nil, fmt.Errorf("%w: edit target has already been edited", bridgev2.ErrIgnoringRemoteEvent)
}
// TODO tell converter about existing parts to avoid reupload?
converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, editMsg.GetDataMessage(), nil)
// TODO can anything other than the text be edited?
editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
prevID := editPart.Part.ID
// Clone the database message struct to avoid mutating the ID.
// The ID from the original struct is used for AddedParts (we specifically want the old ID for that)
editPart.Part = ptr.Clone(editPart.Part)
editPart.Part.EditCount++
editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
return &bridgev2.ConvertedEdit{
convertedEdit := &bridgev2.ConvertedEdit{
ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
}, nil
}
if prevID != editPart.Part.ID {
convertedEdit.AddedParts = editStubMessage()
}
return convertedEdit, nil
}
func (evt *Bv2ChatEvent) GetStreamOrder() int64 {

View file

@ -18,9 +18,12 @@ package connector
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/coder/websocket"
"github.com/google/uuid"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
@ -41,7 +44,7 @@ func (s *SignalConnector) GetLoginFlows() []bridgev2.LoginFlow {
func (s *SignalConnector) CreateLogin(ctx context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
if flowID != "qr" {
return nil, fmt.Errorf("invalid login flow ID")
return nil, bridgev2.ErrInvalidLoginFlowID
}
return &QRLogin{User: user, Main: s}, nil
}
@ -66,10 +69,78 @@ func (qr *QRLogin) Cancel() {
const (
LoginStepQR = "fi.mau.signal.login.qr"
LoginStepProcess = "fi.mau.signal.login.processing"
LoginStepComplete = "fi.mau.signal.login.complete"
)
const (
qrRefreshInterval = 45 * time.Second
maxQRRefreshes = 20
)
var (
ErrLoginTimedOut = bridgev2.RespError{
ErrCode: "FI.MAU.BRIDGE.LOGIN_TIMED_OUT",
Err: "The QR code wasn't scanned in time, please start a new login",
StatusCode: http.StatusGone,
}
ErrLoginCancelled = bridgev2.RespError{
ErrCode: "FI.MAU.BRIDGE.LOGIN_CANCELLED",
Err: "Login process was cancelled",
StatusCode: http.StatusGone,
}
ErrDeviceLinkMissingCapability = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_MISSING_CAPABILITY",
Err: "Signal rejected linking because the bridge is missing a capability required by your account's other devices. Please try again later",
StatusCode: http.StatusConflict,
}
ErrDeviceLimitReached = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LIMIT_REACHED",
Err: "Your Signal account already has the maximum number of linked devices. Remove one in the Signal app and try again",
StatusCode: http.StatusBadRequest,
}
ErrDeviceLinkCodeInvalid = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_CODE_INVALID",
Err: "The scanned QR code was invalid or already used, please start a new login",
StatusCode: http.StatusForbidden,
}
ErrDeviceLinkRateLimited = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_RATE_LIMITED",
Err: "Signal rate-limited the linking attempt, please wait a few minutes and try again",
StatusCode: http.StatusTooManyRequests,
}
ErrDeviceLinkRejected = bridgev2.RespError{
ErrCode: "FI.MAU.SIGNAL.DEVICE_LINK_REJECTED",
Err: "Signal rejected linking the device",
StatusCode: http.StatusBadRequest,
}
)
// Statuses of PUT /v1/devices/link, per Signal-Server's DeviceController
func wrapProvisioningError(err error) error {
var linkErr signalmeow.DeviceLinkError
if errors.As(err, &linkErr) {
switch linkErr.StatusCode {
case http.StatusConflict:
return ErrDeviceLinkMissingCapability
case http.StatusLengthRequired:
return ErrDeviceLimitReached
case http.StatusForbidden:
return ErrDeviceLinkCodeInvalid
case http.StatusTooManyRequests:
return ErrDeviceLinkRateLimited
default:
if linkErr.Message != "" {
return ErrDeviceLinkRejected.AppendMessage(" (HTTP %d: %s)", linkErr.StatusCode, linkErr.Message)
}
return ErrDeviceLinkRejected.AppendMessage(" (HTTP %d)", linkErr.StatusCode)
}
}
if websocket.CloseStatus(err) == websocket.StatusGoingAway {
return ErrLoginTimedOut
}
return err
}
func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
log := qr.Main.Bridge.Log.With().
Str("action", "login").
@ -85,14 +156,16 @@ func (qr *QRLogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
select {
case resp = <-qr.ProvChan:
if resp.Err != nil {
return nil, resp.Err
return nil, wrapProvisioningError(resp.Err)
} else if resp.State != signalmeow.StateProvisioningURLReceived {
return nil, fmt.Errorf("unexpected state %v", resp.State)
}
case <-ctx.Done():
cancel()
return nil, ctx.Err()
// TODO separate timeout here?
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, ErrLoginTimedOut
}
return nil, ErrLoginCancelled
}
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeDisplayAndWait,
@ -114,7 +187,7 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
case resp := <-qr.ProvChan:
if resp.Err != nil {
qr.cancelChan()
return nil, resp.Err
return nil, wrapProvisioningError(resp.Err)
} else if resp.State != signalmeow.StateProvisioningDataReceived {
qr.cancelChan()
return nil, fmt.Errorf("unexpected state %v", resp.State)
@ -126,17 +199,20 @@ func (qr *QRLogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
// Server will timeout the request after 60 seconds, but Signal Desktop opens
// a new socket and gets a new QR code after 45 seconds. We should do the same.
case <-time.After(45 * time.Second):
case <-time.After(qrRefreshInterval):
qr.cancelChan()
qr.newQRCount++
if qr.newQRCount >= 6 {
return nil, fmt.Errorf("too many QR code refreshes")
if qr.newQRCount >= maxQRRefreshes {
return nil, ErrLoginTimedOut
}
return qr.Start(ctx)
case <-ctx.Done():
qr.cancelChan()
return nil, ctx.Err()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, ErrLoginTimedOut
}
return nil, ErrLoginCancelled
}
}

View file

@ -20,19 +20,17 @@ package libsignalgo
#include "./libsignal-ffi.h"
*/
import "C"
import (
"runtime"
"unsafe"
)
import "runtime"
type AccountEntropyPool string
type SVRKey = fixedArray32
func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
var out [C.SignalSVR_KEY_LEN]byte
var out SVRKey
aepC, free := GoStringToCString(string(aep))
defer free()
signalFfiError := C.signal_account_entropy_pool_derive_svr_key(
(*[C.SignalSVR_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
out.cFixedArray(),
aepC,
)
runtime.KeepAlive(aep)
@ -43,11 +41,11 @@ func (aep AccountEntropyPool) DeriveSVRKey() ([]byte, error) {
}
func (aep AccountEntropyPool) DeriveBackupKey() ([]byte, error) {
var out [C.SignalBACKUP_KEY_LEN]byte
var out BackupKey
aepC, free := GoStringToCString(string(aep))
defer free()
signalFfiError := C.signal_account_entropy_pool_derive_backup_key(
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
out.cFixedArray(),
aepC,
)
runtime.KeepAlive(aep)

View file

@ -24,15 +24,16 @@ package libsignalgo
import "C"
import (
"fmt"
"unsafe"
"github.com/google/uuid"
)
// type AuthCredential [C.SignalAUTH_CREDENTIAL_LEN]byte
// type AuthCredentialResponse [C.SignalAUTH_CREDENTIAL_RESPONSE_LEN]byte
type AuthCredentialWithPni [C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN]byte
type AuthCredentialWithPniResponse [C.SignalAUTH_CREDENTIAL_WITH_PNI_RESPONSE_LEN]byte
// type AuthCredential [181]byte
// type AuthCredentialResponse [361]byte
const AuthCredentialWithPniLength = 265
type AuthCredentialWithPni [AuthCredentialWithPniLength]byte
type AuthCredentialWithPniResponse [425]byte
type AuthCredentialPresentation []byte
func (ac *AuthCredentialWithPni) Slice() []byte {
@ -51,8 +52,8 @@ func ReceiveAuthCredentialWithPni(
signalFfiError := C.signal_server_public_params_receive_auth_credential_with_pni_as_service_id(
&c_result,
C.SignalConstPointerServerPublicParams{serverPublicParams},
NewACIServiceID(aci).CFixedBytes(),
NewPNIServiceID(pni).CFixedBytes(),
NewACIServiceID(aci).cConstFixedArray(),
NewPNIServiceID(pni).cConstFixedArray(),
C.uint64_t(redemptionTime),
BytesToBuffer(authCredResponse[:]),
)
@ -60,8 +61,8 @@ func ReceiveAuthCredentialWithPni(
return nil, wrapError(signalFfiError)
}
resultBytes := CopySignalOwnedBufferToBytes(c_result)
if len(resultBytes) != C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN {
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), C.SignalAUTH_CREDENTIAL_WITH_PNI_LEN)
if len(resultBytes) != AuthCredentialWithPniLength {
return nil, fmt.Errorf("invalid response length %d (expected %d)", len(resultBytes), AuthCredentialWithPniLength)
}
return (*AuthCredentialWithPni)(resultBytes), nil
}
@ -83,14 +84,12 @@ func CreateAuthCredentialWithPniPresentation(
authCredWithPni AuthCredentialWithPni,
) (*AuthCredentialPresentation, error) {
var c_result C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
c_randomness := (*[C.SignalRANDOMNESS_LEN]C.uchar)(unsafe.Pointer(&randomness[0]))
c_groupSecretParams := (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(&groupSecretParams[0]))
signalFfiError := C.signal_server_public_params_create_auth_credential_with_pni_presentation_deterministic(
&c_result,
C.SignalConstPointerServerPublicParams{serverPublicParams},
c_randomness,
c_groupSecretParams,
randomness.cConstFixedArray(),
groupSecretParams.cConstFixedArray(),
BytesToBuffer(authCredWithPni[:]),
)
if signalFfiError != nil {

View file

@ -27,7 +27,9 @@ import (
"go.mau.fi/util/random"
)
type BackupKey [C.SignalBACKUP_KEY_LEN]byte
const BackupKeyLength = 32
type BackupKey [BackupKeyLength]byte
func (bk *BackupKey) Slice() []byte {
if bk == nil {
@ -38,17 +40,25 @@ func (bk *BackupKey) Slice() []byte {
const BackupIDLength = 16
type BackupID [BackupIDLength]byte
type BackupMetadataKey [C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]byte
type BackupMediaID [C.SignalMEDIA_ID_LEN]byte
type BackupMediaKey [C.SignalMEDIA_ENCRYPTION_KEY_LEN]byte
type BackupID = fixedArray16
type BackupMetadataKey = fixedArray32
type BackupMediaID = fixedArray15
type BackupMediaKey = fixedArray64
func (bk *BackupKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(bk))
}
func (bk *BackupKey) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(bk.cFixedArray())
}
func GenerateRandomBackupKey() *BackupKey {
return (*BackupKey)(random.Bytes(C.SignalBACKUP_KEY_LEN))
return (*BackupKey)(random.Bytes(BackupKeyLength))
}
func BytesToBackupKey(bytes []byte) *BackupKey {
if len(bytes) != C.SignalBACKUP_KEY_LEN {
if len(bytes) != BackupKeyLength {
return nil
}
return (*BackupKey)(bytes)
@ -57,9 +67,9 @@ func BytesToBackupKey(bytes []byte) *BackupKey {
func (bk *BackupKey) DeriveBackupID(aci ServiceID) (*BackupID, error) {
var out BackupID
signalFfiError := C.signal_backup_key_derive_backup_id(
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
aci.CFixedBytes(),
out.cFixedArray(),
bk.cConstFixedArray(),
aci.cConstFixedArray(),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -72,8 +82,8 @@ func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
var out C.SignalMutPointerPrivateKey
signalFfiError := C.signal_backup_key_derive_ec_key(
&out,
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(&bk)),
aci.CFixedBytes(),
bk.cConstFixedArray(),
aci.cConstFixedArray(),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -85,8 +95,8 @@ func (bk *BackupKey) DeriveECKey(aci ServiceID) (*PrivateKey, error) {
func (bk *BackupKey) DeriveLocalBackupMetadataKey() (*BackupMetadataKey, error) {
var out BackupMetadataKey
signalFfiError := C.signal_backup_key_derive_local_backup_metadata_key(
(*[C.SignalLOCAL_BACKUP_METADATA_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
out.cFixedArray(),
bk.cConstFixedArray(),
)
runtime.KeepAlive(bk)
if signalFfiError != nil {
@ -100,8 +110,8 @@ func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
mediaNameStr, mediaNameFree := GoStringToCString(mediaName)
defer mediaNameFree()
signalFfiError := C.signal_backup_key_derive_media_id(
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
out.cFixedArray(),
bk.cConstFixedArray(),
mediaNameStr,
)
runtime.KeepAlive(bk)
@ -114,9 +124,9 @@ func (bk *BackupKey) DeriveMediaID(mediaName string) (*BackupMediaID, error) {
func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
var out BackupMediaKey
signalFfiError := C.signal_backup_key_derive_media_encryption_key(
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
out.cFixedArray(),
bk.cConstFixedArray(),
mediaID.cConstFixedArray(),
)
runtime.KeepAlive(bk)
runtime.KeepAlive(mediaID)
@ -129,9 +139,9 @@ func (bk *BackupKey) DeriveMediaEncryptionKey(mediaID *BackupMediaID) (*BackupMe
func (bk *BackupKey) DeriveThumbnailTransitEncryptionKey(mediaID *BackupMediaID) (*BackupMediaKey, error) {
var out BackupMediaKey
signalFfiError := C.signal_backup_key_derive_thumbnail_transit_encryption_key(
(*[C.SignalMEDIA_ENCRYPTION_KEY_LEN]C.uint8_t)(unsafe.Pointer(&out)),
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(bk)),
(*[C.SignalMEDIA_ID_LEN]C.uint8_t)(unsafe.Pointer(mediaID)),
out.cFixedArray(),
bk.cConstFixedArray(),
mediaID.cConstFixedArray(),
)
runtime.KeepAlive(bk)
runtime.KeepAlive(mediaID)

View file

@ -18,19 +18,20 @@ package libsignalgo
/*
#include "./libsignal-ffi.h"
#include <stdlib.h>
*/
import "C"
import "unsafe"
func GoStringToCString(str string) (C.SignalCStringPtr, func()) {
cStr := C.CString(str)
return cStr, func() {
return C.SignalCStringPtr(unsafe.Pointer(cStr)), func() {
C.free(unsafe.Pointer(cStr))
}
}
func CopyCStringToString(cString C.SignalCStringPtr) (s string) {
s = C.GoString(cString)
s = C.GoString((*C.char)(unsafe.Pointer(cString)))
C.signal_free_string(cString)
return
}

View file

@ -0,0 +1,159 @@
// 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 <https://www.gnu.org/licenses/>.
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
import "unsafe"
type fixedArray15 [15]byte
type fixedArray16 [16]byte
type fixedArray17 [17]byte
type fixedArray32 [32]byte
type fixedArray64 [64]byte
type fixedArray65 [65]byte
type fixedArray97 [97]byte
type fixedArray129 [129]byte
type fixedArray153 [153]byte
type fixedArray177 [177]byte
type fixedArray289 [289]byte
type fixedArray329 [329]byte
type fixedArray409 [409]byte
type fixedArray473 [473]byte
type fixedArray497 [497]byte
func (a *fixedArray15) cFixedArray() *C.SignalType_FixedArray15_uint8_t {
return (*C.SignalType_FixedArray15_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray15) cConstFixedArray() cFixedArray15Compat {
return cFixedArray15Compat(a.cFixedArray())
}
func (a *fixedArray16) cFixedArray() *C.SignalType_FixedArray16_uint8_t {
return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray16) cConstFixedArray() cFixedArray16Compat {
return cFixedArray16Compat(a.cFixedArray())
}
func (a *fixedArray17) cFixedArray() *C.SignalType_FixedArray17_uint8_t {
return (*C.SignalType_FixedArray17_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray17) cConstFixedArray() cFixedArray17Compat {
return cFixedArray17Compat(a.cFixedArray())
}
func (a *fixedArray32) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray32) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(a.cFixedArray())
}
func (a *fixedArray64) cFixedArray() *C.SignalType_FixedArray64_uint8_t {
return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray64) cConstFixedArray() cFixedArray64Compat {
return cFixedArray64Compat(a.cFixedArray())
}
func (a *fixedArray65) cFixedArray() *C.SignalType_FixedArray65_uint8_t {
return (*C.SignalType_FixedArray65_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray65) cConstFixedArray() cFixedArray65Compat {
return cFixedArray65Compat(a.cFixedArray())
}
func (a *fixedArray97) cFixedArray() *C.SignalType_FixedArray97_uint8_t {
return (*C.SignalType_FixedArray97_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray97) cConstFixedArray() cFixedArray97Compat {
return cFixedArray97Compat(a.cFixedArray())
}
func (a *fixedArray129) cFixedArray() *C.SignalType_FixedArray129_uint8_t {
return (*C.SignalType_FixedArray129_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray129) cConstFixedArray() cFixedArray129Compat {
return cFixedArray129Compat(a.cFixedArray())
}
func (a *fixedArray153) cFixedArray() *C.SignalType_FixedArray153_uint8_t {
return (*C.SignalType_FixedArray153_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray153) cConstFixedArray() cFixedArray153Compat {
return cFixedArray153Compat(a.cFixedArray())
}
func (a *fixedArray177) cFixedArray() *C.SignalType_FixedArray177_uint8_t {
return (*C.SignalType_FixedArray177_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray177) cConstFixedArray() cFixedArray177Compat {
return cFixedArray177Compat(a.cFixedArray())
}
func (a *fixedArray289) cFixedArray() *C.SignalType_FixedArray289_uint8_t {
return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray289) cConstFixedArray() cFixedArray289Compat {
return cFixedArray289Compat(a.cFixedArray())
}
func (a *fixedArray329) cFixedArray() *C.SignalType_FixedArray329_uint8_t {
return (*C.SignalType_FixedArray329_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray329) cConstFixedArray() cFixedArray329Compat {
return cFixedArray329Compat(a.cFixedArray())
}
func (a *fixedArray409) cFixedArray() *C.SignalType_FixedArray409_uint8_t {
return (*C.SignalType_FixedArray409_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray409) cConstFixedArray() cFixedArray409Compat {
return cFixedArray409Compat(a.cFixedArray())
}
func (a *fixedArray473) cFixedArray() *C.SignalType_FixedArray473_uint8_t {
return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray473) cConstFixedArray() cFixedArray473Compat {
return cFixedArray473Compat(a.cFixedArray())
}
func (a *fixedArray497) cFixedArray() *C.SignalType_FixedArray497_uint8_t {
return (*C.SignalType_FixedArray497_uint8_t)(unsafe.Pointer(a))
}
func (a *fixedArray497) cConstFixedArray() cFixedArray497Compat {
return cFixedArray497Compat(a.cFixedArray())
}

View file

@ -0,0 +1,24 @@
//go:build darwin || android || ios || (windows && arm64)
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
type cFixedArray15Compat = *C.SignalType_FixedArray15_uint8_t
type cFixedArray16Compat = *C.SignalType_FixedArray16_uint8_t
type cFixedArray17Compat = *C.SignalType_FixedArray17_uint8_t
type cFixedArray32Compat = *C.SignalType_FixedArray32_uint8_t
type cFixedArray64Compat = *C.SignalType_FixedArray64_uint8_t
type cFixedArray65Compat = *C.SignalType_FixedArray65_uint8_t
type cFixedArray97Compat = *C.SignalType_FixedArray97_uint8_t
type cFixedArray129Compat = *C.SignalType_FixedArray129_uint8_t
type cFixedArray153Compat = *C.SignalType_FixedArray153_uint8_t
type cFixedArray177Compat = *C.SignalType_FixedArray177_uint8_t
type cFixedArray289Compat = *C.SignalType_FixedArray289_uint8_t
type cFixedArray329Compat = *C.SignalType_FixedArray329_uint8_t
type cFixedArray409Compat = *C.SignalType_FixedArray409_uint8_t
type cFixedArray473Compat = *C.SignalType_FixedArray473_uint8_t
type cFixedArray497Compat = *C.SignalType_FixedArray497_uint8_t

View file

@ -0,0 +1,26 @@
//go:build !(darwin || android || ios || (windows && arm64))
package libsignalgo
/*
#include "./libsignal-ffi.h"
*/
import "C"
// Hack for https://github.com/golang/go/issues/7270
// The clang version is more correct, but doesn't work with gcc.
type cFixedArray15Compat = *[15]C.uint8_t
type cFixedArray16Compat = *[16]C.uint8_t
type cFixedArray17Compat = *[17]C.uint8_t
type cFixedArray32Compat = *[32]C.uint8_t
type cFixedArray64Compat = *[64]C.uint8_t
type cFixedArray65Compat = *[65]C.uint8_t
type cFixedArray97Compat = *[97]C.uint8_t
type cFixedArray129Compat = *[129]C.uint8_t
type cFixedArray153Compat = *[153]C.uint8_t
type cFixedArray177Compat = *[177]C.uint8_t
type cFixedArray289Compat = *[289]C.uint8_t
type cFixedArray329Compat = *[329]C.uint8_t
type cFixedArray409Compat = *[409]C.uint8_t
type cFixedArray473Compat = *[473]C.uint8_t
type cFixedArray497Compat = *[497]C.uint8_t

View file

@ -31,7 +31,9 @@ import (
"github.com/google/uuid"
)
type Randomness [C.SignalRANDOMNESS_LEN]byte
const RandomnessLength = 32
type Randomness = fixedArray32
func GenerateRandomness() Randomness {
var randomness Randomness
@ -42,14 +44,39 @@ func GenerateRandomness() Randomness {
return randomness
}
const GroupMasterKeyLength = C.SignalGROUP_MASTER_KEY_LEN
const GroupIdentifierLength = C.SignalGROUP_IDENTIFIER_LEN
const GroupMasterKeyLength = 32
const GroupIdentifierLength = 32
const GroupSecretParamsLength = 289
type GroupMasterKey [GroupMasterKeyLength]byte
type GroupSecretParams [C.SignalGROUP_SECRET_PARAMS_LEN]byte
type GroupPublicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]byte
type GroupSecretParams [GroupSecretParamsLength]byte
type GroupPublicParams = fixedArray97
type GroupIdentifier [GroupIdentifierLength]byte
func (gmk *GroupMasterKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gmk))
}
func (gmk *GroupMasterKey) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(gmk.cFixedArray())
}
func (gsp *GroupSecretParams) cFixedArray() *C.SignalType_FixedArray289_uint8_t {
return (*C.SignalType_FixedArray289_uint8_t)(unsafe.Pointer(gsp))
}
func (gsp *GroupSecretParams) cConstFixedArray() cFixedArray289Compat {
return cFixedArray289Compat(gsp.cFixedArray())
}
func (gid *GroupIdentifier) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(gid))
}
func (gid *GroupIdentifier) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(gid.cFixedArray())
}
func (gid *GroupIdentifier) String() string {
if gid == nil {
return ""
@ -57,8 +84,8 @@ func (gid *GroupIdentifier) String() string {
return base64.StdEncoding.EncodeToString(gid[:])
}
type UUIDCiphertext [C.SignalUUID_CIPHERTEXT_LEN]byte
type ProfileKeyCiphertext [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]byte
type UUIDCiphertext = fixedArray65
type ProfileKeyCiphertext = fixedArray65
func GenerateGroupSecretParams() (GroupSecretParams, error) {
return GenerateGroupSecretParamsWithRandomness(GenerateRandomness())
@ -81,51 +108,43 @@ func (gmk GroupMasterKey) SecretParams() (GroupSecretParams, error) {
}
func GenerateGroupSecretParamsWithRandomness(randomness Randomness) (GroupSecretParams, error) {
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_generate_deterministic(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
var params GroupSecretParams
signalFfiError := C.signal_group_secret_params_generate_deterministic(params.cFixedArray(), randomness.cConstFixedArray())
runtime.KeepAlive(randomness)
if signalFfiError != nil {
return GroupSecretParams{}, wrapError(signalFfiError)
}
var groupSecretParams GroupSecretParams
copy(groupSecretParams[:], C.GoBytes(unsafe.Pointer(&params), C.int(C.SignalGROUP_SECRET_PARAMS_LEN)))
return groupSecretParams, nil
return params, nil
}
func DeriveGroupSecretParamsFromMasterKey(groupMasterKey GroupMasterKey) (GroupSecretParams, error) {
var params [C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_derive_from_master_key(&params, (*[C.SignalGROUP_MASTER_KEY_LEN]C.uint8_t)(unsafe.Pointer(&groupMasterKey)))
var params GroupSecretParams
signalFfiError := C.signal_group_secret_params_derive_from_master_key(params.cFixedArray(), groupMasterKey.cConstFixedArray())
runtime.KeepAlive(groupMasterKey)
if signalFfiError != nil {
return GroupSecretParams{}, wrapError(signalFfiError)
}
var groupSecretParams GroupSecretParams
copy(groupSecretParams[:], C.GoBytes(unsafe.Pointer(&params), C.int(C.SignalGROUP_SECRET_PARAMS_LEN)))
return groupSecretParams, nil
return params, nil
}
func (gsp *GroupSecretParams) GetPublicParams() (*GroupPublicParams, error) {
var publicParams [C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uchar
signalFfiError := C.signal_group_secret_params_get_public_params(&publicParams, (*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)))
var publicParams GroupPublicParams
signalFfiError := C.signal_group_secret_params_get_public_params(publicParams.cFixedArray(), gsp.cConstFixedArray())
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var groupPublicParams GroupPublicParams
copy(groupPublicParams[:], C.GoBytes(unsafe.Pointer(&publicParams), C.int(C.SignalGROUP_PUBLIC_PARAMS_LEN)))
return &groupPublicParams, nil
return &publicParams, nil
}
func GetGroupIdentifier(groupPublicParams GroupPublicParams) (*GroupIdentifier, error) {
var groupIdentifier [C.SignalGROUP_IDENTIFIER_LEN]C.uchar
signalFfiError := C.signal_group_public_params_get_group_identifier(&groupIdentifier, (*[C.SignalGROUP_PUBLIC_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(&groupPublicParams)))
var groupIdentifier GroupIdentifier
signalFfiError := C.signal_group_public_params_get_group_identifier(groupIdentifier.cFixedArray(), groupPublicParams.cConstFixedArray())
runtime.KeepAlive(groupPublicParams)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result GroupIdentifier
copy(result[:], C.GoBytes(unsafe.Pointer(&groupIdentifier), C.int(C.SignalGROUP_IDENTIFIER_LEN)))
return &result, nil
return &groupIdentifier, nil
}
func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error) {
@ -133,7 +152,7 @@ func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error
borrowedBlob := BytesToBuffer(blob)
signalFfiError := C.signal_group_secret_params_decrypt_blob_with_padding(
&plaintext,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
gsp.cConstFixedArray(),
borrowedBlob,
)
runtime.KeepAlive(gsp)
@ -149,8 +168,8 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
borrowedPlaintext := BytesToBuffer(plaintext)
signalFfiError := C.signal_group_secret_params_encrypt_blob_with_padding_deterministic(
&ciphertext,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
gsp.cConstFixedArray(),
randomness.cConstFixedArray(),
borrowedPlaintext,
(C.uint32_t)(padding_len),
)
@ -165,11 +184,11 @@ func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Ran
}
func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertext) (ServiceID, error) {
u := C.SignalServiceIdFixedWidthBinaryBytes{}
var serviceIDBytes ServiceIDFixedBytes
signalFfiError := C.signal_group_secret_params_decrypt_service_id(
&u,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalUUID_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextServiceID)),
serviceIDBytes.cFixedArray(),
gsp.cConstFixedArray(),
ciphertextServiceID.cConstFixedArray(),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextServiceID)
@ -177,33 +196,31 @@ func (gsp *GroupSecretParams) DecryptServiceID(ciphertextServiceID UUIDCiphertex
return EmptyServiceID, wrapError(signalFfiError)
}
serviceID := ServiceIDFromCFixedBytes(&u)
serviceID := ServiceIDFromCFixedBytes(serviceIDBytes.cFixedArray())
return serviceID, nil
}
func (gsp *GroupSecretParams) EncryptServiceID(serviceID ServiceID) (*UUIDCiphertext, error) {
var cipherTextServiceID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
var cipherTextServiceID UUIDCiphertext
signalFfiError := C.signal_group_secret_params_encrypt_service_id(
&cipherTextServiceID,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
serviceID.CFixedBytes(),
cipherTextServiceID.cFixedArray(),
gsp.cConstFixedArray(),
serviceID.cConstFixedArray(),
)
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result UUIDCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextServiceID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
return &result, nil
return &cipherTextServiceID, nil
}
func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyCiphertext, u uuid.UUID) (*ProfileKey, error) {
profileKey := [C.SignalPROFILE_KEY_LEN]C.uchar{}
var profileKey ProfileKey
signalFfiError := C.signal_group_secret_params_decrypt_profile_key(
&profileKey,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uint8_t)(unsafe.Pointer(&ciphertextProfileKey)),
NewACIServiceID(u).CFixedBytes(),
profileKey.cFixedArray(),
gsp.cConstFixedArray(),
ciphertextProfileKey.cConstFixedArray(),
NewACIServiceID(u).cConstFixedArray(),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(ciphertextProfileKey)
@ -211,27 +228,23 @@ func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyC
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result ProfileKey
copy(result[:], C.GoBytes(unsafe.Pointer(&profileKey), C.int(C.SignalPROFILE_KEY_LEN)))
return &result, nil
return &profileKey, nil
}
func (gsp *GroupSecretParams) EncryptProfileKey(profileKey ProfileKey, u uuid.UUID) (*ProfileKeyCiphertext, error) {
ciphertextProfileKey := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
var ciphertextProfileKey ProfileKeyCiphertext
signalFfiError := C.signal_group_secret_params_encrypt_profile_key(
&ciphertextProfileKey,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
(*[C.SignalPROFILE_KEY_LEN]C.uint8_t)(unsafe.Pointer(&profileKey)),
NewACIServiceID(u).CFixedBytes(),
ciphertextProfileKey.cFixedArray(),
gsp.cConstFixedArray(),
profileKey.cConstFixedArray(),
NewACIServiceID(u).cConstFixedArray(),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(profileKey)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result ProfileKeyCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&ciphertextProfileKey), C.int(C.SignalPROFILE_KEY_CIPHERTEXT_LEN)))
return &result, nil
return &ciphertextProfileKey, nil
}
func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp *ServerPublicParams, credential ExpiringProfileKeyCredential) (*ProfileKeyCredentialPresentation, error) {
@ -240,9 +253,9 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
signalFfiError := C.signal_server_public_params_create_expiring_profile_key_credential_presentation_deterministic(
&out,
C.SignalConstPointerServerPublicParams{spp},
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar)(unsafe.Pointer(&credential)),
randomness.cConstFixedArray(),
gsp.cConstFixedArray(),
credential.cConstFixedArray(),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(credential)
@ -256,16 +269,14 @@ func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp
}
func (gsp *GroupSecretParams) GetMasterKey() (*GroupMasterKey, error) {
masterKeyBytes := [C.SignalGROUP_MASTER_KEY_LEN]C.uchar{}
var masterKey GroupMasterKey
signalFfiError := C.signal_group_secret_params_get_master_key(
&masterKeyBytes,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uchar)(unsafe.Pointer(gsp)),
masterKey.cFixedArray(),
gsp.cConstFixedArray(),
)
runtime.KeepAlive(gsp)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var groupMasterKey GroupMasterKey
copy(groupMasterKey[:], C.GoBytes(unsafe.Pointer(&masterKeyBytes), C.int(C.SignalGROUP_MASTER_KEY_LEN)))
return &groupMasterKey, nil
return &masterKey, nil
}

View file

@ -24,7 +24,6 @@ import (
"encoding/base64"
"runtime"
"time"
"unsafe"
)
type GroupSendFullToken []byte
@ -91,7 +90,7 @@ func (gse GroupSendEndorsement) ToToken(groupSecretParams *GroupSecretParams) (G
signalFfiError := C.signal_group_send_endorsement_to_token(
&token,
BytesToBuffer(gse),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(groupSecretParams)),
groupSecretParams.cConstFixedArray(),
)
runtime.KeepAlive(gse)
runtime.KeepAlive(groupSecretParams)
@ -180,17 +179,17 @@ func (gser GroupSendEndorsementsResponse) ReceiveWithServiceIDs(
groupMembers []ServiceID, localUser ServiceID, params *GroupSecretParams, spp *ServerPublicParams,
) (GroupSendEndorsement, map[ServiceID]GroupSendEndorsement, error) {
var out C.SignalBytestringArray = C.SignalBytestringArray{}
concatenatedMembers := make([]byte, len(groupMembers)*17)
concatenatedMembers := make([]byte, len(groupMembers)*ServiceIDFixedBytesLength)
for i, member := range groupMembers {
copy(concatenatedMembers[i*17:(i+1)*17], member.FixedBytes()[:])
copy(concatenatedMembers[i*ServiceIDFixedBytesLength:(i+1)*ServiceIDFixedBytesLength], member.FixedBytes()[:])
}
signalFfiError := C.signal_group_send_endorsements_response_receive_and_combine_with_service_ids(
&out,
BytesToBuffer(gser),
BytesToBuffer(concatenatedMembers),
localUser.CFixedBytes(),
localUser.cConstFixedArray(),
C.uint64_t(time.Now().Unix()),
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(params)),
params.cConstFixedArray(),
C.SignalConstPointerServerPublicParams{spp},
)
runtime.KeepAlive(gser)

@ -1 +1 @@
Subproject commit 4e9bd5d7feca2f61cf2ce0c7d99a60eb8b9e0102
Subproject commit 857c4dca03537dc5e395a5e1eda6bf18f59c3601

File diff suppressed because it is too large Load diff

View file

@ -33,7 +33,7 @@ var ffiLogger Logger
//export signal_log_callback
func signal_log_callback(ctx unsafe.Pointer, level C.SignalLogLevel, file C.SignalCStringPtr, line C.uint32_t, message C.SignalCStringPtr) {
ffiLogger.Log(LogLevel(int(level)), C.GoString(file), uint(line), C.GoString(message))
ffiLogger.Log(LogLevel(int(level)), CopyCStringToString(file), uint(line), CopyCStringToString(message))
}
//export signal_log_flush_callback

View file

@ -71,10 +71,10 @@ func Decrypt(ctx context.Context, message *Message, fromAddress, localAddress *A
type Message struct {
nc noCopy
ptr *C.SignalMessage
ptr *C.SignalSignalMessage
}
func wrapMessage(ptr *C.SignalMessage) *Message {
func wrapMessage(ptr *C.SignalSignalMessage) *Message {
message := &Message{ptr: ptr}
runtime.SetFinalizer(message, (*Message).Destroy)
return message

View file

@ -20,16 +20,17 @@ package libsignalgo
#include "./libsignal-ffi.h"
*/
import "C"
import (
"runtime"
"unsafe"
)
import "runtime"
type MessageBackupKey struct {
nc noCopy
ptr *C.SignalMessageBackupKey
}
const MessageBackupKeyBytesLength = 32
type messageBackupKeyBytes = fixedArray32
func wrapMessageBackupKey(ptr *C.SignalMessageBackupKey) *MessageBackupKey {
backupKey := &MessageBackupKey{ptr: ptr}
runtime.SetFinalizer(backupKey, (*MessageBackupKey).Destroy)
@ -43,7 +44,7 @@ func MessageBackupKeyFromAccountEntropyPool(aep AccountEntropyPool, aci ServiceI
signalFfiError := C.signal_message_backup_key_from_account_entropy_pool(
&bk,
aepC,
aci.CFixedBytes(),
aci.cConstFixedArray(),
nil, // TODO what's a forward secrecy token?
)
runtime.KeepAlive(aep)
@ -57,8 +58,8 @@ func MessageBackupKeyFromBackupKeyAndID(backupKey *BackupKey, backupID *BackupID
var bk C.SignalMutPointerMessageBackupKey
signalFfiError := C.signal_message_backup_key_from_backup_key_and_backup_id(
&bk,
(*[C.SignalBACKUP_KEY_LEN]C.uint8_t)(unsafe.Pointer(backupKey)),
(*[BackupIDLength]C.uint8_t)(unsafe.Pointer(backupID)),
backupKey.cConstFixedArray(),
backupID.cConstFixedArray(),
nil, // TODO what's a forward secrecy token?
)
runtime.KeepAlive(backupKey)
@ -82,26 +83,26 @@ func (bk *MessageBackupKey) Destroy() error {
return wrapError(C.signal_message_backup_key_destroy(bk.mutPtr()))
}
func (bk *MessageBackupKey) GetHMACKey() ([32]byte, error) {
var out [32]byte
func (bk *MessageBackupKey) GetHMACKey() ([MessageBackupKeyBytesLength]byte, error) {
var out messageBackupKeyBytes
signalFfiError := C.signal_message_backup_key_get_hmac_key(
(*[32]C.uint8_t)(unsafe.Pointer(&out)),
out.cFixedArray(),
bk.constPtr(),
)
if signalFfiError != nil {
return out, wrapError(signalFfiError)
return [MessageBackupKeyBytesLength]byte(out), wrapError(signalFfiError)
}
return out, nil
return [MessageBackupKeyBytesLength]byte(out), nil
}
func (bk *MessageBackupKey) GetAESKey() ([32]byte, error) {
var out [32]byte
func (bk *MessageBackupKey) GetAESKey() ([MessageBackupKeyBytesLength]byte, error) {
var out messageBackupKeyBytes
signalFfiError := C.signal_message_backup_key_get_aes_key(
(*[32]C.uint8_t)(unsafe.Pointer(&out)),
out.cFixedArray(),
bk.constPtr(),
)
if signalFfiError != nil {
return out, wrapError(signalFfiError)
return [MessageBackupKeyBytesLength]byte(out), wrapError(signalFfiError)
}
return out, nil
return [MessageBackupKeyBytesLength]byte(out), nil
}

View file

@ -32,12 +32,38 @@ import (
"go.mau.fi/util/random"
)
const ProfileKeyLength = C.SignalPROFILE_KEY_LEN
const ProfileKeyLength = 32
const AccessKeyLength = 16
const ProfileKeyVersionLength = 64
type ProfileKey [ProfileKeyLength]byte
type ProfileKeyCommitment [C.SignalPROFILE_KEY_COMMITMENT_LEN]byte
type ProfileKeyVersion [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]byte
type AccessKey [C.SignalACCESS_KEY_LEN]byte
type ProfileKeyCommitment = fixedArray97
type ProfileKeyVersion [ProfileKeyVersionLength]byte
type AccessKey [AccessKeyLength]byte
func (pk *ProfileKey) cFixedArray() *C.SignalType_FixedArray32_uint8_t {
return (*C.SignalType_FixedArray32_uint8_t)(unsafe.Pointer(pk))
}
func (pk *ProfileKey) cConstFixedArray() cFixedArray32Compat {
return cFixedArray32Compat(pk.cFixedArray())
}
func (pkv *ProfileKeyVersion) cFixedArray() *C.SignalType_FixedArray64_uint8_t {
return (*C.SignalType_FixedArray64_uint8_t)(unsafe.Pointer(pkv))
}
func (pkv *ProfileKeyVersion) cConstFixedArray() cFixedArray64Compat {
return cFixedArray64Compat(pkv.cFixedArray())
}
func (ak *AccessKey) cFixedArray() *C.SignalType_FixedArray16_uint8_t {
return (*C.SignalType_FixedArray16_uint8_t)(unsafe.Pointer(ak))
}
func (ak *AccessKey) cConstFixedArray() cFixedArray16Compat {
return cFixedArray16Compat(ak.cFixedArray())
}
func DeserializeProfileKey(bytes []byte) (*ProfileKey, error) {
if len(bytes) == 0 {
@ -73,7 +99,7 @@ func (ak *AccessKey) Xor(other *AccessKey) *AccessKey {
return ak
}
var result AccessKey
for i := 0; i < C.SignalACCESS_KEY_LEN; i++ {
for i := 0; i < AccessKeyLength; i++ {
result[i] = ak[i] ^ other[i]
}
return &result
@ -84,13 +110,12 @@ func (ak *AccessKey) String() string {
}
func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error) {
c_result := [C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
c_uuid := NewACIServiceID(u).CFixedBytes()
var result ProfileKeyCommitment
c_uuid := NewACIServiceID(u).cConstFixedArray()
signalFfiError := C.signal_profile_key_get_commitment(
&c_result,
c_profileKey,
result.cFixedArray(),
pk.cConstFixedArray(),
c_uuid,
)
runtime.KeepAlive(pk)
@ -100,19 +125,16 @@ func (pk *ProfileKey) GetCommitment(u uuid.UUID) (*ProfileKeyCommitment, error)
return nil, wrapError(signalFfiError)
}
var result ProfileKeyCommitment
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_COMMITMENT_LEN)))
return &result, nil
}
func (pk *ProfileKey) GetProfileKeyVersion(u uuid.UUID) (*ProfileKeyVersion, error) {
c_result := [C.SignalPROFILE_KEY_VERSION_ENCODED_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
c_uuid := NewACIServiceID(u).CFixedBytes()
var result ProfileKeyVersion
c_uuid := NewACIServiceID(u).cConstFixedArray()
signalFfiError := C.signal_profile_key_get_profile_key_version(
&c_result,
c_profileKey,
result.cFixedArray(),
pk.cConstFixedArray(),
c_uuid,
)
runtime.KeepAlive(pk)
@ -122,18 +144,15 @@ func (pk *ProfileKey) GetProfileKeyVersion(u uuid.UUID) (*ProfileKeyVersion, err
return nil, wrapError(signalFfiError)
}
var result ProfileKeyVersion
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_VERSION_ENCODED_LEN)))
return &result, nil
}
func (pk *ProfileKey) DeriveAccessKey() (*AccessKey, error) {
c_result := [C.SignalACCESS_KEY_LEN]C.uchar{}
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(pk))
var result AccessKey
signalFfiError := C.signal_profile_key_derive_access_key(
&c_result,
c_profileKey,
result.cFixedArray(),
pk.cConstFixedArray(),
)
runtime.KeepAlive(pk)
@ -141,31 +160,35 @@ func (pk *ProfileKey) DeriveAccessKey() (*AccessKey, error) {
return nil, wrapError(signalFfiError)
}
var result AccessKey
copy(result[:], C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalACCESS_KEY_LEN)))
return &result, nil
}
type ProfileKeyCredentialRequestContext [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]byte
type ProfileKeyCredentialRequest [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]byte
type ProfileKeyCredentialRequestContext [473]byte
type ProfileKeyCredentialRequest = fixedArray329
type ProfileKeyCredentialResponse []byte
type ProfileKeyCredentialPresentation []byte
type ExpiringProfileKeyCredential [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]byte
type ExpiringProfileKeyCredentialResponse [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]byte
type ExpiringProfileKeyCredential = fixedArray153
type ExpiringProfileKeyCredentialResponse = fixedArray497
func (p *ProfileKeyCredentialRequestContext) cFixedArray() *C.SignalType_FixedArray473_uint8_t {
return (*C.SignalType_FixedArray473_uint8_t)(unsafe.Pointer(p))
}
func (p *ProfileKeyCredentialRequestContext) cConstFixedArray() cFixedArray473Compat {
return cFixedArray473Compat(p.cFixedArray())
}
func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicParams, u uuid.UUID, profileKey ProfileKey) (*ProfileKeyCredentialRequestContext, error) {
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar{}
randBytes := [32]byte(random.Bytes(32))
c_random := (*[32]C.uchar)(unsafe.Pointer(&randBytes[0]))
c_profileKey := (*[C.SignalPROFILE_KEY_LEN]C.uchar)(unsafe.Pointer(&profileKey[0]))
c_uuid := NewACIServiceID(u).CFixedBytes()
var result ProfileKeyCredentialRequestContext
randBytes := Randomness(random.Bytes(RandomnessLength))
c_uuid := NewACIServiceID(u).cConstFixedArray()
signalFfiError := C.signal_server_public_params_create_profile_key_credential_request_context_deterministic(
&c_result,
result.cFixedArray(),
C.SignalConstPointerServerPublicParams{serverPublicParams},
c_random,
randBytes.cConstFixedArray(),
c_uuid,
c_profileKey,
profileKey.cConstFixedArray(),
)
runtime.KeepAlive(u)
runtime.KeepAlive(profileKey)
@ -173,23 +196,20 @@ func CreateProfileKeyCredentialRequestContext(serverPublicParams *ServerPublicPa
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
result := ProfileKeyCredentialRequestContext(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN)))
return &result, nil
}
func (p *ProfileKeyCredentialRequestContext) ProfileKeyCredentialRequestContextGetRequest() (*ProfileKeyCredentialRequest, error) {
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]C.uchar{}
c_context := (*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(p))
var result ProfileKeyCredentialRequest
signalFfiError := C.signal_profile_key_credential_request_context_get_request(
&c_result,
c_context,
result.cFixedArray(),
p.cConstFixedArray(),
)
runtime.KeepAlive(p)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
result := ProfileKeyCredentialRequest(C.GoBytes(unsafe.Pointer(&c_result), C.int(C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN)))
return &result, nil
}
@ -205,12 +225,12 @@ func NewExpiringProfileKeyCredentialResponse(b []byte) (*ExpiringProfileKeyCrede
}
func ReceiveExpiringProfileKeyCredential(spp *ServerPublicParams, requestContext *ProfileKeyCredentialRequestContext, response *ExpiringProfileKeyCredentialResponse, currentTimeInSeconds uint64) (*ExpiringProfileKeyCredential, error) {
c_credential := [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar{}
var credential ExpiringProfileKeyCredential
signalFfiError := C.signal_server_public_params_receive_expiring_profile_key_credential(
&c_credential,
credential.cFixedArray(),
C.SignalConstPointerServerPublicParams{spp},
(*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(requestContext)),
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]C.uchar)(unsafe.Pointer(response)),
requestContext.cConstFixedArray(),
response.cConstFixedArray(),
(C.uint64_t)(currentTimeInSeconds),
)
runtime.KeepAlive(requestContext)
@ -219,8 +239,6 @@ func ReceiveExpiringProfileKeyCredential(spp *ServerPublicParams, requestContext
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
credential := ExpiringProfileKeyCredential{}
copy(credential[:], C.GoBytes(unsafe.Pointer(&c_credential), C.int(C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN)))
return &credential, nil
}
@ -231,25 +249,21 @@ func (a ProfileKeyCredentialPresentation) CheckValidContents() error {
}
func (a ProfileKeyCredentialPresentation) UUIDCiphertext() (UUIDCiphertext, error) {
out := [C.SignalUUID_CIPHERTEXT_LEN]C.uchar{}
signalFfiError := C.signal_profile_key_credential_presentation_get_uuid_ciphertext(&out, BytesToBuffer(a))
var out UUIDCiphertext
signalFfiError := C.signal_profile_key_credential_presentation_get_uuid_ciphertext(out.cFixedArray(), BytesToBuffer(a))
runtime.KeepAlive(a)
if signalFfiError != nil {
return UUIDCiphertext{}, wrapError(signalFfiError)
}
var result UUIDCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&out), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
return result, nil
return out, nil
}
func (a ProfileKeyCredentialPresentation) ProfileKeyCiphertext() (ProfileKeyCiphertext, error) {
out := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
signalFfiError := C.signal_profile_key_credential_presentation_get_profile_key_ciphertext(&out, BytesToBuffer(a))
var out ProfileKeyCiphertext
signalFfiError := C.signal_profile_key_credential_presentation_get_profile_key_ciphertext(out.cFixedArray(), BytesToBuffer(a))
runtime.KeepAlive(a)
if signalFfiError != nil {
return ProfileKeyCiphertext{}, wrapError(signalFfiError)
}
var result ProfileKeyCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&out), C.int(C.SignalPROFILE_KEY_CIPHERTEXT_LEN)))
return result, nil
return out, nil
}

View file

@ -24,15 +24,16 @@ import "C"
import (
"fmt"
"runtime"
"unsafe"
)
type ServerPublicParams = C.SignalServerPublicParams
type NotarySignature [C.SignalSIGNATURE_LEN]byte
type NotarySignature = fixedArray64
const ServerPublicParamsLength = 673
func DeserializeServerPublicParams(params []byte) (*ServerPublicParams, error) {
if len(params) != C.SignalSERVER_PUBLIC_PARAMS_LEN {
return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), int(C.SignalSERVER_PUBLIC_PARAMS_LEN))
if len(params) != ServerPublicParamsLength {
return nil, fmt.Errorf("invalid server public params length: %d (expected %d)", len(params), ServerPublicParamsLength)
}
var out C.SignalMutPointerServerPublicParams
signalFfiError := C.signal_server_public_params_deserialize(&out, BytesToBuffer(params[:]))
@ -47,11 +48,10 @@ func ServerPublicParamsVerifySignature(
messageBytes []byte,
NotarySignature NotarySignature,
) error {
c_notarySignature := (*[C.SignalSIGNATURE_LEN]C.uint8_t)(unsafe.Pointer(&NotarySignature[0]))
signalFfiError := C.signal_server_public_params_verify_signature(
C.SignalConstPointerServerPublicParams{serverPublicParams},
BytesToBuffer(messageBytes),
c_notarySignature,
NotarySignature.cConstFixedArray(),
)
runtime.KeepAlive(messageBytes)
return wrapError(signalFfiError)

View file

@ -133,7 +133,10 @@ func (s ServiceID) MarshalZerologObject(e *zerolog.Event) {
e.Stringer("uuid", s.UUID)
}
type ServiceIDFixedBytes [17]byte
const ServiceIDUUIDLength = 16
const ServiceIDFixedBytesLength = 17
type ServiceIDFixedBytes = fixedArray17
func (s ServiceID) FixedBytes() *ServiceIDFixedBytes {
var result ServiceIDFixedBytes
@ -162,18 +165,18 @@ func ServiceIDFromString(val string) (ServiceID, error) {
}
func ServiceIDFromBytes(bytes []byte) (ServiceID, error) {
if len(bytes) == 16 {
if len(bytes) == ServiceIDUUIDLength {
return NewACIServiceID(uuid.UUID(bytes)), nil
} else if len(bytes) == 17 {
} else if len(bytes) == ServiceIDFixedBytesLength {
return ServiceID{
Type: ServiceIDType(bytes[0]),
UUID: uuid.UUID(bytes[1:]),
}, nil
}
return EmptyServiceID, fmt.Errorf("invalid ServiceID byte length: %d (expected 16 or 17)", len(bytes))
return EmptyServiceID, fmt.Errorf("invalid ServiceID byte length: %d (expected %d or %d)", len(bytes), ServiceIDUUIDLength, ServiceIDFixedBytesLength)
}
func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes) ServiceID {
func ServiceIDFromCFixedBytes(serviceID *C.SignalType_FixedArray17_uint8_t) ServiceID {
var id ServiceID
fixedBytes := (*ServiceIDFixedBytes)(unsafe.Pointer(serviceID))
id.Type = ServiceIDType(fixedBytes[0])
@ -181,6 +184,10 @@ func ServiceIDFromCFixedBytes(serviceID *C.SignalServiceIdFixedWidthBinaryBytes)
return id
}
func (s ServiceID) CFixedBytes() cPNIType {
return cPNIType(unsafe.Pointer(s.FixedBytes()))
func (s ServiceID) cFixedArray() *C.SignalType_FixedArray17_uint8_t {
return s.FixedBytes().cFixedArray()
}
func (s ServiceID) cConstFixedArray() cFixedArray17Compat {
return cFixedArray17Compat(s.cFixedArray())
}

View file

@ -1,11 +0,0 @@
//go:build darwin || android || ios || (windows && arm64)
package libsignalgo
/*
#include "./libsignal-ffi.h"
#include <stdlib.h>
*/
import "C"
type cPNIType = *C.SignalServiceIdFixedWidthBinaryBytes

View file

@ -1,14 +0,0 @@
//go:build !(darwin || android || ios || (windows && arm64))
package libsignalgo
/*
#include "./libsignal-ffi.h"
#include <stdlib.h>
*/
import "C"
// Hack for https://github.com/golang/go/issues/7270
// The clang version is more correct, but doesn't work with gcc
type cPNIType = *[17]C.uint8_t

View file

@ -105,7 +105,6 @@ func (sr *SessionRecord) HasCurrentState() (bool, error) {
signalFfiError := C.signal_session_record_has_usable_sender_chain(
&result,
sr.constPtr(),
C.double(0.0),
C.uint64_t(time.Now().Unix()),
)
runtime.KeepAlive(sr)

View file

@ -1,11 +1,10 @@
#!/bin/sh
cd /data
export RUSTFLAGS="-Ctarget-feature=-crt-static" RUSTC_WRAPPER=""
apk add --no-cache git make cmake protobuf-dev musl-dev g++ clang-dev cbindgen
apk add --no-cache git make cmake protobuf-dev musl-dev g++ clang-dev
cd libsignal
cargo build -p libsignal-ffi --release
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
cd ..
mv libsignal/target/release/libsignal_ffi.a .
mv libsignal/libsignal-ffi.h .
cp libsignal/swift/Sources/SignalFfi/signal_ffi.h libsignal-ffi.h
chown 1000:1000 libsignal_ffi.a libsignal-ffi.h version.go

View file

@ -28,14 +28,11 @@ echo "const Version = \"$(git describe --tags --always)\"" >> ../version.go
# Build libsignal
cargo build -p libsignal-ffi --release
# Regenerate the header file
cbindgen --profile release rust/bridge/ffi -o libsignal-ffi.h
# Navigate back to the original directory
cd "$ORIGINAL_DIR"
# Copy files from the libsignal directory
cp "${LIBSIGNAL_DIRECTORY}/target/release/libsignal_ffi.a" .
cp "${LIBSIGNAL_DIRECTORY}/libsignal-ffi.h" .
cp "${LIBSIGNAL_DIRECTORY}/swift/Sources/SignalFfi/signal_ffi.h" libsignal-ffi.h
echo "Files copied successfully."

View file

@ -2,4 +2,4 @@
package libsignalgo
const Version = "v0.97.2"
const Version = "v0.100.0"

View file

@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
"mime"
"net/http"
"os"
"strconv"
@ -569,16 +570,21 @@ func (mc *MessageConverter) reuploadAttachment(ctx context.Context, att *signalp
content.Info.Blurhash = att.GetBlurHash()
content.Info.AnoaBlurhash = att.GetBlurHash()
}
switch strings.Split(content.Info.MimeType, "/")[0] {
case "image":
plainMime, _, _ := mime.ParseMediaType(content.Info.MimeType)
// Supported mime types from https://github.com/signalapp/Signal-Desktop/blob/main/ts/util/GoogleChrome.std.ts
switch plainMime {
case "image/avif", "image/bmp", "image/gif", "image/jpeg", "image/webp", "image/x-xbitmap",
"image/vnd.microsoft.icon", "image/ico", "image/icon", "image/x-icon", "image/png", "image/apng":
content.MsgType = event.MsgImage
case "video":
case "video/mp4", "video/ogg", "video/webm":
content.MsgType = event.MsgVideo
case "audio":
content.MsgType = event.MsgAudio
default:
if strings.HasPrefix(plainMime, "audio/") && !strings.HasSuffix(plainMime, "aiff") {
content.MsgType = event.MsgAudio
} else {
content.MsgType = event.MsgFile
}
}
var extra map[string]any
if att.GetFlags()&uint32(signalpb.AttachmentPointer_GIF) != 0 {
content.Info.MauGIF = true

View file

@ -111,22 +111,13 @@ func TestParse_HTML(t *testing.T) {
{
name: "List",
in: "<ul><li>woof</li><li><strong>meow</strong></li><li><pre><code>hmm\nmeow</code></pre></li><li><blockquote>meow<br><h1>meow</h1></blockquote></li></ul>",
out: "* woof\n* meow\n* hmm\n meow\n* > meow\n > \n > # meow",
out: "* woof\n* meow\n* ```\n hmm\n meow\n ```\n* > meow\n > \n > # meow",
ent: signalfmt.BodyRangeList{{
Start: 9,
Length: 4,
Value: signalfmt.StyleBold,
}, {
Start: 16,
Length: 3,
Value: signalfmt.StyleMonospace,
}, {
// FIXME optimally this would be a single range with the previous one so the indent is also monospace
Start: 22,
Length: 4,
Value: signalfmt.StyleMonospace,
}, {
Start: 45,
Start: 57,
Length: 6,
Value: signalfmt.StyleBold,
}},
@ -134,21 +125,13 @@ func TestParse_HTML(t *testing.T) {
{
name: "OrderedList",
in: "<ol start=9><li>woof</li><li><strong>meow</strong></li><li><pre><code>hmm\nmeow</code></pre></li><li><blockquote>meow<br><h1>meow</h1></blockquote></li></ol>",
out: "9. woof\n10. meow\n11. hmm\n meow\n12. > meow\n > \n > # meow",
out: "9. woof\n10. meow\n11. ```\n hmm\n meow\n ```\n12. > meow\n > \n > # meow",
ent: signalfmt.BodyRangeList{{
Start: 13,
Length: 4,
Value: signalfmt.StyleBold,
}, {
Start: 22,
Length: 3,
Value: signalfmt.StyleMonospace,
}, {
Start: 30,
Length: 4,
Value: signalfmt.StyleMonospace,
}, {
Start: 59,
Start: 75,
Length: 6,
Value: signalfmt.StyleBold,
}},

View file

@ -404,17 +404,17 @@ func (parser *HTMLParser) tagToString(node *html.Node, ctx Context) *EntityStrin
return NewEntityString("---")
case "pre":
var preStr *EntityString
//var language string
var language string
if node.FirstChild != nil && node.FirstChild.Type == html.ElementNode && node.FirstChild.Data == "code" {
//class := parser.getAttribute(node.FirstChild, "class")
//if strings.HasPrefix(class, "language-") {
// language = class[len("language-"):]
//}
class := parser.getAttribute(node.FirstChild, "class")
if strings.HasPrefix(class, "language-") {
language = class[len("language-"):]
}
preStr = parser.nodeToString(node.FirstChild.FirstChild, ctx.WithWhitespace())
} else {
preStr = parser.nodeToString(node.FirstChild, ctx.WithWhitespace())
}
return preStr.Format(signalfmt.StyleMonospace)
return NewEntityString(fmt.Sprintf("```%s\n", language)).Append(preStr).AppendString("\n```")
default:
return parser.nodeToTagAwareString(node.FirstChild, ctx)
}

View file

@ -76,6 +76,15 @@ type ProvisioningResponse struct {
Err error
}
type DeviceLinkError struct {
StatusCode int
Message string
}
func (dle DeviceLinkError) Error() string {
return fmt.Sprintf("non-200 status code (%d) from devices response: %s", dle.StatusCode, dle.Message)
}
func PerformProvisioning(ctx context.Context, deviceStore store.DeviceStore, deviceName string, allowBackup bool) chan ProvisioningResponse {
log := zerolog.Ctx(ctx).With().Str("action", "perform provisioning").Logger()
c := make(chan ProvisioningResponse, 4)
@ -441,9 +450,9 @@ func confirmDevice(
return nil, fmt.Errorf("failed to read from websocket after devices call: %w", err)
}
status := int(*receivedMsg.Response.Status)
status := int(receivedMsg.GetResponse().GetStatus())
if status < 200 || status >= 300 {
return nil, fmt.Errorf("non-200 status code (%d) from devices response: %s", status, *receivedMsg.Response.Message)
return nil, DeviceLinkError{StatusCode: status, Message: receivedMsg.GetResponse().GetMessage()}
}
// unmarshal JSON response into ConfirmDeviceResponse

View file

@ -137,6 +137,12 @@ func (cli *Client) processStorageInTxn(ctx context.Context, update *StorageUpdat
case *signalpb.StorageRecord_Account:
log.Trace().Any("account_record", data.Account).Msg("Found account record")
cli.Store.AccountRecord = data.Account
if len(data.Account.ProfileKey) == libsignalgo.ProfileKeyLength {
err := cli.Store.RecipientStore.StoreProfileKey(ctx, cli.Store.ACI, libsignalgo.ProfileKey(data.Account.ProfileKey))
if err != nil {
return fmt.Errorf("failed to store own profile key: %w", err)
}
}
err := cli.Store.DeviceStore.PutDevice(ctx, &cli.Store.DeviceData)
if err != nil {
return fmt.Errorf("failed to save device after receiving account record: %w", err)

View file

@ -58,8 +58,7 @@ func deleteExtraPrekeys(ctx context.Context, db *dbutil.Database, selectQuery, d
return nil
}
func init() {
Table.Register(-1, 16, 13, "Remove extra prekeys", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) error {
var upgradeV16 = dbutil.WrapUpgrade(-1, 16, 13, "Remove extra prekeys", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) error {
err := deleteExtraPrekeys(ctx, db, `
SELECT account_id, service_id, COUNT(*), MAX(key_id) FROM signalmeow_pre_keys WHERE is_signed=false GROUP BY 1, 2
`, `
@ -78,4 +77,3 @@ func init() {
}
return nil
})
}

View file

@ -22,8 +22,7 @@ import (
"go.mau.fi/util/dbutil"
)
func init() {
Table.Register(-1, 20, 13, "Add missing columns for backup chat table", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) (err error) {
var upgradeV20 = dbutil.WrapUpgrade(-1, 20, 13, "Add missing columns for backup chat table", dbutil.TxnModeOn, func(ctx context.Context, db *dbutil.Database) (err error) {
var exists bool
if exists, err = db.ColumnExists(ctx, "signalmeow_backup_chat", "latest_message_id"); err == nil && !exists {
_, err = db.Exec(ctx, `
@ -33,4 +32,3 @@ func init() {
}
return
})
}

View file

@ -22,11 +22,11 @@ import (
"go.mau.fi/util/dbutil"
)
var Table dbutil.UpgradeTable
//go:embed *.sql
var rawUpgrades embed.FS
func init() {
Table.RegisterFS(rawUpgrades)
}
var Table = dbutil.BuildUpgradeTable().
WithFS(rawUpgrades).
With(upgradeV16).
With(upgradeV20).
Finish()