Files
2026-07-13 14:14:17 +03:00

315 lines
10 KiB
Go

// Copyright (c) 2021 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package whatsmeow
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"time"
"go.mau.fi/libsignal/ecc"
"google.golang.org/protobuf/proto"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/proto/waAdv"
"go.mau.fi/whatsmeow/proto/waCompanionReg"
"go.mau.fi/whatsmeow/proto/waWa6"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"go.mau.fi/whatsmeow/util/keys"
)
var (
AdvAccountSignaturePrefix = []byte{6, 0}
AdvDeviceSignaturePrefix = []byte{6, 1}
AdvHostedAccountSignaturePrefix = []byte{6, 5}
AdvHostedDeviceSignaturePrefix = []byte{6, 6}
)
func (cli *Client) handleIQ(ctx context.Context, node *waBinary.Node) {
children := node.GetChildren()
if len(children) != 1 || node.Attrs["from"] != types.ServerJID {
return
}
switch children[0].Tag {
case "pair-device":
cli.handlePairDevice(ctx, node)
case "pair-success":
cli.handlePairSuccess(ctx, node)
}
}
func (cli *Client) handlePairDevice(ctx context.Context, node *waBinary.Node) {
cli.paired.Store(false)
pairDevice := node.GetChildByTag("pair-device")
err := cli.sendNode(ctx, waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": node.Attrs["from"],
"id": node.Attrs["id"],
"type": "result",
},
})
if err != nil {
cli.Log.Warnf("Failed to send acknowledgement for pair-device request: %v", err)
}
evt := &events.QR{Codes: make([]string, 0, len(pairDevice.GetChildren()))}
for i, child := range pairDevice.GetChildren() {
if child.Tag != "ref" {
cli.Log.Warnf("pair-device node contains unexpected child tag %s at index %d", child.Tag, i)
continue
}
content, ok := child.Content.([]byte)
if !ok {
cli.Log.Warnf("pair-device node contains unexpected child content type %T at index %d", child, i)
continue
}
evt.Codes = append(evt.Codes, cli.makeQRData(content, cli.getQRClientType()))
}
cli.dispatchEvent(evt)
}
func (cli *Client) getQRClientType() PairClientType {
if cli.QRClientType != "" {
return cli.QRClientType
}
switch store.DeviceProps.GetPlatformType() {
case waCompanionReg.DeviceProps_CHROME:
return PairClientChrome
case waCompanionReg.DeviceProps_FIREFOX:
return PairClientFirefox
case waCompanionReg.DeviceProps_EDGE:
return PairClientEdge
case waCompanionReg.DeviceProps_IE:
return PairClientIE
case waCompanionReg.DeviceProps_OPERA:
return PairClientOpera
case waCompanionReg.DeviceProps_SAFARI:
return PairClientSafari
case waCompanionReg.DeviceProps_UWP:
return PairClientUWP
case waCompanionReg.DeviceProps_ANDROID_PHONE:
return PairClientAndroid
}
switch store.BaseClientPayload.UserAgent.GetPlatform() {
case waWa6.ClientPayload_UserAgent_WEB:
return PairClientOtherWebClient
case waWa6.ClientPayload_UserAgent_MACOS:
return PairClientMacOS
default:
return PairClientUnknown
}
}
func (cli *Client) makeQRData(ref []byte, clientType PairClientType) string {
noise := base64.StdEncoding.EncodeToString(cli.Store.NoiseKey.Pub[:])
identity := base64.StdEncoding.EncodeToString(cli.Store.IdentityKey.Pub[:])
adv := base64.StdEncoding.EncodeToString(cli.Store.AdvSecretKey)
return fmt.Sprintf("https://wa.me/settings/linked_devices#%s,%s,%s,%s,%s", ref, noise, identity, adv, clientType)
}
func (cli *Client) handlePairSuccess(ctx context.Context, node *waBinary.Node) {
cli.serverTimeOffset.Store(int64(node.AttrGetter().UnixTime("t").Sub(time.Now().Round(time.Second))))
id := node.Attrs["id"].(string)
pairSuccess := node.GetChildByTag("pair-success")
deviceIdentityBytes, _ := pairSuccess.GetChildByTag("device-identity").Content.([]byte)
businessName, _ := pairSuccess.GetChildByTag("biz").Attrs["name"].(string)
jid, _ := pairSuccess.GetChildByTag("device").Attrs["jid"].(types.JID)
lid, _ := pairSuccess.GetChildByTag("device").Attrs["lid"].(types.JID)
platform, _ := pairSuccess.GetChildByTag("platform").Attrs["name"].(string)
clientPropsBytes, _ := pairSuccess.GetChildByTag("client-props").Content.([]byte)
var props waCompanionReg.ClientPairingProps
if err := proto.Unmarshal(clientPropsBytes, &props); err != nil {
cli.Log.Warnf("Failed to parse client pairing props: %v", err)
}
go func() {
err := cli.handlePair(ctx, deviceIdentityBytes, id, businessName, platform, jid, lid)
if err != nil {
cli.Log.Errorf("Failed to pair device: %v", err)
cli.Disconnect()
cli.dispatchEvent(&events.PairError{ID: jid, LID: lid, BusinessName: businessName, Platform: platform, Props: &props, Error: err})
} else {
cli.Log.Infof("Successfully paired %s", cli.Store.ID)
go cli.sendUnifiedSession()
cli.dispatchEvent(&events.PairSuccess{ID: jid, LID: lid, BusinessName: businessName, Platform: platform, Props: &props})
}
}()
}
func (cli *Client) handlePair(ctx context.Context, deviceIdentityBytes []byte, reqID, businessName, platform string, jid, lid types.JID) error {
var deviceIdentityContainer waAdv.ADVSignedDeviceIdentityHMAC
err := proto.Unmarshal(deviceIdentityBytes, &deviceIdentityContainer)
if err != nil {
cli.sendPairError(ctx, reqID, 500, "internal-error")
return &PairProtoError{"failed to parse device identity container in pair success message", err}
}
h := hmac.New(sha256.New, cli.Store.AdvSecretKey)
if deviceIdentityContainer.GetAccountType() == waAdv.ADVEncryptionType_HOSTED {
h.Write(AdvHostedAccountSignaturePrefix)
//cli.Store.IsHosted = true
}
h.Write(deviceIdentityContainer.Details)
if !hmac.Equal(h.Sum(nil), deviceIdentityContainer.HMAC) {
cli.Log.Warnf("Invalid HMAC from pair success message")
cli.sendPairError(ctx, reqID, 401, "hmac-mismatch")
return ErrPairInvalidDeviceIdentityHMAC
}
var deviceIdentity waAdv.ADVSignedDeviceIdentity
err = proto.Unmarshal(deviceIdentityContainer.Details, &deviceIdentity)
if err != nil {
cli.sendPairError(ctx, reqID, 500, "internal-error")
return &PairProtoError{"failed to parse signed device identity in pair success message", err}
}
var deviceIdentityDetails waAdv.ADVDeviceIdentity
err = proto.Unmarshal(deviceIdentity.Details, &deviceIdentityDetails)
if err != nil {
cli.sendPairError(ctx, reqID, 500, "internal-error")
return &PairProtoError{"failed to parse device identity details in pair success message", err}
}
if !verifyAccountSignature(&deviceIdentity, cli.Store.IdentityKey, deviceIdentityDetails.GetDeviceType() == waAdv.ADVEncryptionType_HOSTED) {
cli.sendPairError(ctx, reqID, 401, "signature-mismatch")
return ErrPairInvalidDeviceSignature
}
deviceIdentity.DeviceSignature = generateDeviceSignature(&deviceIdentity, cli.Store.IdentityKey)[:]
if cli.PrePairCallback != nil && !cli.PrePairCallback(jid, platform, businessName) {
cli.sendPairError(ctx, reqID, 500, "internal-error")
return ErrPairRejectedLocally
}
cli.Store.Account = proto.Clone(&deviceIdentity).(*waAdv.ADVSignedDeviceIdentity)
mainDeviceLID := lid
mainDeviceLID.Device = 0
mainDeviceIdentity := *(*[32]byte)(deviceIdentity.AccountSignatureKey)
deviceIdentity.AccountSignatureKey = nil
selfSignedDeviceIdentity, err := proto.Marshal(&deviceIdentity)
if err != nil {
cli.sendPairError(ctx, reqID, 500, "internal-error")
return &PairProtoError{"failed to marshal self-signed device identity", err}
}
cli.Store.ID = &jid
cli.Store.LID = lid
cli.Store.BusinessName = businessName
cli.Store.Platform = platform
err = cli.Store.Save(ctx)
if err != nil {
cli.sendPairError(ctx, reqID, 500, "internal-error")
return &PairDatabaseError{"failed to save device store", err}
}
cli.StoreLIDPNMapping(ctx, lid, jid)
err = cli.Store.Identities.PutIdentity(ctx, mainDeviceLID.SignalAddress().String(), mainDeviceIdentity)
if err != nil {
_ = cli.Store.Delete(ctx)
cli.sendPairError(ctx, reqID, 500, "internal-error")
return &PairDatabaseError{"failed to store main device identity", err}
}
// Expect a disconnect after this and don't dispatch the usual Disconnected event
cli.expectDisconnect()
cli.paired.Store(true)
err = cli.sendNode(ctx, waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": types.ServerJID,
"type": "result",
"id": reqID,
},
Content: []waBinary.Node{{
Tag: "pair-device-sign",
Content: []waBinary.Node{{
Tag: "device-identity",
Attrs: waBinary.Attrs{
"key-index": deviceIdentityDetails.GetKeyIndex(),
},
Content: selfSignedDeviceIdentity,
}},
}},
})
if err != nil {
_ = cli.Store.Delete(ctx)
return fmt.Errorf("failed to send pairing confirmation: %w", err)
}
return nil
}
func concatBytes(data ...[]byte) []byte {
length := 0
for _, item := range data {
length += len(item)
}
output := make([]byte, length)
ptr := 0
for _, item := range data {
ptr += copy(output[ptr:ptr+len(item)], item)
}
return output
}
func verifyAccountSignature(deviceIdentity *waAdv.ADVSignedDeviceIdentity, ikp *keys.KeyPair, isHosted bool) bool {
if len(deviceIdentity.AccountSignatureKey) != 32 || len(deviceIdentity.AccountSignature) != 64 {
return false
}
signatureKey := ecc.NewDjbECPublicKey(*(*[32]byte)(deviceIdentity.AccountSignatureKey))
signature := *(*[64]byte)(deviceIdentity.AccountSignature)
prefix := AdvAccountSignaturePrefix
if isHosted {
prefix = AdvHostedAccountSignaturePrefix
}
message := concatBytes(prefix, deviceIdentity.Details, ikp.Pub[:])
return ecc.VerifySignature(signatureKey, message, signature)
}
func generateDeviceSignature(deviceIdentity *waAdv.ADVSignedDeviceIdentity, ikp *keys.KeyPair) *[64]byte {
prefix := AdvDeviceSignaturePrefix
message := concatBytes(prefix, deviceIdentity.Details, ikp.Pub[:], deviceIdentity.AccountSignatureKey)
sig := ecc.CalculateSignature(ecc.NewDjbECPrivateKey(*ikp.Priv), message)
return &sig
}
func (cli *Client) sendPairError(ctx context.Context, id string, code int, text string) {
err := cli.sendNode(ctx, waBinary.Node{
Tag: "iq",
Attrs: waBinary.Attrs{
"to": types.ServerJID,
"type": "error",
"id": id,
},
Content: []waBinary.Node{{
Tag: "error",
Attrs: waBinary.Attrs{
"code": code,
"text": text,
},
}},
})
if err != nil {
cli.Log.Errorf("Failed to send pair error node: %v", err)
}
}