Skip to content

Commit

Permalink
bridge matrix -> signal group actions (except pendingMember stuff)
Browse files Browse the repository at this point in the history
  • Loading branch information
maltee1 committed Mar 14, 2024
1 parent 78b9e84 commit e8b6f9f
Show file tree
Hide file tree
Showing 14 changed files with 1,163 additions and 24 deletions.
15 changes: 8 additions & 7 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@
* [x] Message edits
* [x] Message reactions
* [x] Message redactions
* [ ] Group info changes
* [ ] Name
* [ ] Avatar
* [ ] Topic
* [x] Group info changes
* [x] Name
* [x] Avatar
* [x] Topic
* [ ] Membership actions
* [ ] Join (accepting invites)
* [ ] Invite
* [ ] Leave
* [ ] Kick/Ban/Unban
* [x] Invite
* [x] Leave
* [x] Kick/Ban/Unban
* [x] Group permissions
* [x] Typing notifications
* [x] Read receipts
* [x] Delivery receipts (sent after message is bridged)
Expand Down
1 change: 1 addition & 0 deletions config/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ type BridgeConfig struct {
PublicPortals bool `yaml:"public_portals"`
CaptionInMessage bool `yaml:"caption_in_message"`
FederateRooms bool `yaml:"federate_rooms"`
BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"`

DoublePuppetConfig bridgeconfig.DoublePuppetConfig `yaml:",inline"`

Expand Down
1 change: 1 addition & 0 deletions config/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ func DoUpgrade(helper *up.Helper) {
helper.Copy(up.Int, "bridge", "encryption", "rotation", "milliseconds")
helper.Copy(up.Int, "bridge", "encryption", "rotation", "messages")
helper.Copy(up.Bool, "bridge", "encryption", "rotation", "disable_device_change_key_rotation")
helper.Copy(up.Bool, "bridge", "bridge_matrix_leave")

helper.Copy(up.Str, "bridge", "provisioning", "prefix")
if secret, ok := helper.Get(up.Str, "bridge", "provisioning", "shared_secret"); !ok || secret == "generate" {
Expand Down
3 changes: 2 additions & 1 deletion example-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ bridge:
# Disable rotating keys when a user's devices change?
# You should not enable this option unless you understand all the implications.
disable_device_change_key_rotation: false

# Should leaving the room on Matrix make the user leave on Signal?
bridge_matrix_leave: true
# Settings for provisioning API
provisioning:
# Prefix for the provisioning API paths.
Expand Down
85 changes: 85 additions & 0 deletions pkg/libsignalgo/groupsecretparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,26 @@ func (gsp *GroupSecretParams) DecryptBlobWithPadding(blob []byte) ([]byte, error
return CopySignalOwnedBufferToBytes(plaintext), nil
}

func (gsp *GroupSecretParams) EncryptBlobWithPaddingDeterministic(randomness Randomness, plaintext []byte, padding_len uint32) ([]byte, error) {
var ciphertext C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
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)),
borrowedPlaintext,
(C.uint32_t)(padding_len),
)
runtime.KeepAlive(randomness)
runtime.KeepAlive(gsp)
runtime.KeepAlive(plaintext)
runtime.KeepAlive(padding_len)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return CopySignalOwnedBufferToBytes(ciphertext), nil
}

func (gsp *GroupSecretParams) DecryptUUID(ciphertextUUID UUIDCiphertext) (uuid.UUID, error) {
u := C.SignalServiceIdFixedWidthBinaryBytes{}
signalFfiError := C.signal_group_secret_params_decrypt_service_id(
Expand All @@ -136,6 +156,27 @@ func (gsp *GroupSecretParams) DecryptUUID(ciphertextUUID UUIDCiphertext) (uuid.U
return result, nil
}

func (gsp *GroupSecretParams) EncryptUUID(uuid uuid.UUID) (*UUIDCiphertext, error) {
var cipherTextUUID [C.SignalUUID_CIPHERTEXT_LEN]C.uchar
serviceId, err := SignalServiceIDFromUUID(uuid)
if err != nil {
return nil, err
}
signalFfiError := C.signal_group_secret_params_encrypt_service_id(
&cipherTextUUID,
(*[C.SignalGROUP_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(gsp)),
serviceId,
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(serviceId)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result UUIDCiphertext
copy(result[:], C.GoBytes(unsafe.Pointer(&cipherTextUUID), C.int(C.SignalUUID_CIPHERTEXT_LEN)))
return &result, nil
}

func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyCiphertext, u uuid.UUID) (*ProfileKey, error) {
profileKey := [C.SignalPROFILE_KEY_LEN]C.uchar{}
serviceId, err := SignalServiceIDFromUUID(u)
Expand All @@ -157,3 +198,47 @@ func (gsp *GroupSecretParams) DecryptProfileKey(ciphertextProfileKey ProfileKeyC
copy(result[:], C.GoBytes(unsafe.Pointer(&profileKey), C.int(C.SignalPROFILE_KEY_LEN)))
return &result, nil
}

func (gsp *GroupSecretParams) EncryptProfileKey(profileKey ProfileKey, u uuid.UUID) (*ProfileKeyCiphertext, error) {
ciphertextProfileKey := [C.SignalPROFILE_KEY_CIPHERTEXT_LEN]C.uchar{}
serviceId, err := SignalServiceIDFromUUID(u)
if err != nil {
return nil, err
}
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)),
serviceId,
)
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
}

func (gsp *GroupSecretParams) CreateExpiringProfileKeyCredentialPresentation(spp ServerPublicParams, credential ExpiringProfileKeyCredential) (*ProfileKeyCredentialPresentation, error) {
var out C.SignalOwnedBuffer = C.SignalOwnedBuffer{}
randomness := GenerateRandomness()
signalFfiError := C.signal_server_public_params_create_expiring_profile_key_credential_presentation_deterministic(
&out,
(*[C.SignalSERVER_PUBLIC_PARAMS_LEN]C.uchar)(unsafe.Pointer(&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)),
)
runtime.KeepAlive(gsp)
runtime.KeepAlive(spp)
runtime.KeepAlive(credential)
runtime.KeepAlive(randomness)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
presentationBytes := CopySignalOwnedBufferToBytes(out)
presentation := ProfileKeyCredentialPresentation(presentationBytes)
return &presentation, nil
}
28 changes: 26 additions & 2 deletions pkg/libsignalgo/profilekey.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ type ProfileKeyCredentialRequest [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]byt
type ProfileKeyCredentialResponse []byte
type ProfileKeyCredentialPresentation []byte
type ServerPublicParams [C.SignalSERVER_PUBLIC_PARAMS_LEN]byte
type ExpiringProfileKeyCredential [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]byte
type ExpiringProfileKeyCredentialResponse [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]byte

func CreateProfileKeyCredentialRequestContext(serverPublicParams ServerPublicParams, u uuid.UUID, profileKey ProfileKey) (*ProfileKeyCredentialRequestContext, error) {
c_result := [C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar{}
Expand Down Expand Up @@ -176,14 +178,36 @@ func (p *ProfileKeyCredentialRequestContext) ProfileKeyCredentialRequestContextG
return &result, nil
}

func NewProfileKeyCredentialResponse(b []byte) (ProfileKeyCredentialResponse, error) {
func NewExpiringProfileKeyCredentialResponse(b []byte) (*ExpiringProfileKeyCredentialResponse, error) {
borrowedBuffer := BytesToBuffer(b)
signalFfiError := C.signal_expiring_profile_key_credential_response_check_valid_contents(borrowedBuffer)
runtime.KeepAlive(b)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
return ProfileKeyCredentialResponse(b), nil
response := ExpiringProfileKeyCredentialResponse(b)
return &response, nil
}

func ReceiveExpiringProfileKeyCredential(spp ServerPublicParams, requestContext *ProfileKeyCredentialRequestContext, response *ExpiringProfileKeyCredentialResponse, currentTimeInSeconds uint64) (*ExpiringProfileKeyCredential, error) {
c_credential := [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_LEN]C.uchar{}
signalFfiError := C.signal_server_public_params_receive_expiring_profile_key_credential(
&c_credential,
(*[C.SignalSERVER_PUBLIC_PARAMS_LEN]C.uchar)(unsafe.Pointer(&spp[0])),
(*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_CONTEXT_LEN]C.uchar)(unsafe.Pointer(requestContext)),
(*[C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]C.uchar)(unsafe.Pointer(response)),
(C.uint64_t)(currentTimeInSeconds),
)
runtime.KeepAlive(spp)
runtime.KeepAlive(requestContext)
runtime.KeepAlive(response)
runtime.KeepAlive(currentTimeInSeconds)
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
}

//func NewProfileKeyCredentialPresentation(b []byte) (ProfileKeyCredentialPresentation, error) {
Expand Down
77 changes: 77 additions & 0 deletions pkg/libsignalgo/serversecretparams.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2024 Malte Eggers
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package libsignalgo

/*
#cgo LDFLAGS: -lsignal_ffi -ldl -lm
#include "./libsignal-ffi.h"
*/
import "C"
import (
"runtime"
"unsafe"

"github.com/google/uuid"
)

type ServerSecretParams [C.SignalSERVER_SECRET_PARAMS_LEN]byte

func GenerateServerSecretParams() (ServerSecretParams, error) {
return GenerateServerSecretParamsWithRandomness(GenerateRandomness())
}

func GenerateServerSecretParamsWithRandomness(randomness Randomness) (ServerSecretParams, error) {
var params [C.SignalSERVER_SECRET_PARAMS_LEN]C.uchar
signalFfiError := C.signal_server_secret_params_generate_deterministic(&params, (*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)))
runtime.KeepAlive(randomness)
if signalFfiError != nil {
return ServerSecretParams{}, wrapError(signalFfiError)
}
var serverSecretParams ServerSecretParams
copy(serverSecretParams[:], C.GoBytes(unsafe.Pointer(&params), C.int(C.SignalSERVER_SECRET_PARAMS_LEN)))
return serverSecretParams, nil
}

func (ssp *ServerSecretParams) IssueExpiringProfileKeyCredential(request ProfileKeyCredentialRequest, uuid uuid.UUID, commitment ProfileKeyCommitment, expiration uint64) (*ExpiringProfileKeyCredentialResponse, error) {
var response [C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN]C.uchar
randomness := GenerateRandomness()
serviceID, err := SignalServiceIDFromUUID(uuid)
if err != nil {
return nil, err
}
signalFfiError := C.signal_server_secret_params_issue_expiring_profile_key_credential_deterministic(
&response,
(*[C.SignalSERVER_SECRET_PARAMS_LEN]C.uint8_t)(unsafe.Pointer(ssp)),
(*[C.SignalRANDOMNESS_LEN]C.uint8_t)(unsafe.Pointer(&randomness)),
(*[C.SignalPROFILE_KEY_CREDENTIAL_REQUEST_LEN]C.uchar)(unsafe.Pointer(&request)),
serviceID,
(*[C.SignalPROFILE_KEY_COMMITMENT_LEN]C.uchar)(unsafe.Pointer(&commitment)),
(C.uint64_t)(expiration),
)
runtime.KeepAlive(ssp)
runtime.KeepAlive(randomness)
runtime.KeepAlive(request)
runtime.KeepAlive(serviceID)
runtime.KeepAlive(commitment)
runtime.KeepAlive(expiration)
if signalFfiError != nil {
return nil, wrapError(signalFfiError)
}
var result *ExpiringProfileKeyCredentialResponse
copy(result[:], C.GoBytes(unsafe.Pointer(&response), C.int(C.SignalEXPIRING_PROFILE_KEY_CREDENTIAL_RESPONSE_LEN)))
return result, nil
}
2 changes: 1 addition & 1 deletion pkg/libsignalgo/verifysignature.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2023 Scott Weber
// Copyright (C) 2024 Malte Eggers
//
// 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
Expand Down
79 changes: 79 additions & 0 deletions pkg/signalmeow/attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ import (
"fmt"
"io"
"math"
"mime/multipart"
"net/http"

"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.mau.fi/util/random"
"google.golang.org/protobuf/proto"

"go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)

Expand Down Expand Up @@ -200,6 +205,80 @@ func (cli *Client) UploadAttachment(ctx context.Context, body []byte) (*signalpb
return attachmentPointer, nil
}

func (cli *Client) UploadGroupAvatar(ctx context.Context, avatarBytes []byte, gid types.GroupIdentifier) (*string, error) {
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil {
log.Err(err).Msg("Could not get master key from group id")
return nil, err
}
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil {
log.Err(err).Msg("Failed to get Authorization for today")
return nil, err
}
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyToBytes(groupMasterKey))
if err != nil {
log.Err(err).Msg("Could not get groupSecretParams from master key")
return nil, err
}
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Avatar{Avatar: avatarBytes}}
encryptedAvatar, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not encrypt avatar into Group Property")
return nil, err
}

// Get upload form from Signal server
formPath := "/v1/groups/avatar/form"
opts := &web.HTTPReqOpt{Username: &groupAuth.Username, Password: &groupAuth.Password, ContentType: web.ContentTypeProtobuf, Host: web.StorageHostname}
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, formPath, opts)
if err != nil {
log.Err(err).Msg("Error sending request fetching avatar upload form")
return nil, err
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Err(err).Msg("Error decoding response body fetching upload attributes")
return nil, err
}
uploadForm := signalpb.AvatarUploadAttributes{}
err = proto.Unmarshal(body, &uploadForm)
if err != nil {
log.Err(err).Msg("failed to unmarshal group avatar upload form")
return nil, err
}
requestBody := &bytes.Buffer{}
w := multipart.NewWriter(requestBody)
w.WriteField("key", uploadForm.Key)
w.WriteField("x-amz-credential", uploadForm.Credential)
w.WriteField("acl", uploadForm.Acl)
w.WriteField("x-amz-algorithm", uploadForm.Algorithm)
w.WriteField("x-amz-date", uploadForm.Date)
w.WriteField("policy", uploadForm.Policy)
w.WriteField("x-amz-signature", uploadForm.Signature)
w.WriteField("Content-Type", "application/octet-stream")
filewriter, _ := w.CreateFormFile("file", "file")
filewriter.Write(*encryptedAvatar)
w.Close()

// Upload avatar to CDN
resp, err = web.SendHTTPRequest(ctx, http.MethodPost, "", &web.HTTPReqOpt{
Body: requestBody.Bytes(),
ContentType: web.ContentType(w.FormDataContentType()),
Host: web.CDN1Hostname,
})
if err != nil {
log.Err(err).Msg("Error sending request uploading attachment")
return nil, err
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
log.Error().Int("status_code", resp.StatusCode).Msg("Error uploading attachment")
return nil, fmt.Errorf("error uploading attachment: %s", resp.Status)
}

return &uploadForm.Key, nil
}

func verifyMAC(key, body, mac []byte) bool {
m := hmac.New(sha256.New, key)
m.Write(body)
Expand Down
Loading

0 comments on commit e8b6f9f

Please # to comment.