245 lines
7.6 KiB
Go
245 lines
7.6 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 (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"go.mau.fi/libsignal/ecc"
|
|
|
|
waBinary "go.mau.fi/whatsmeow/binary"
|
|
waProto "go.mau.fi/whatsmeow/binary/proto"
|
|
"go.mau.fi/whatsmeow/types"
|
|
"go.mau.fi/whatsmeow/types/events"
|
|
"go.mau.fi/whatsmeow/util/keys"
|
|
)
|
|
|
|
const qrScanTimeout = 30 * time.Second
|
|
|
|
func (cli *Client) handleIQ(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(node)
|
|
case "pair-success":
|
|
cli.handlePairSuccess(node)
|
|
}
|
|
}
|
|
|
|
func (cli *Client) handlePairDevice(node *waBinary.Node) {
|
|
pairDevice := node.GetChildByTag("pair-device")
|
|
err := cli.sendNode(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(string(content)))
|
|
}
|
|
|
|
cli.dispatchEvent(evt)
|
|
}
|
|
|
|
func (cli *Client) makeQRData(ref string) 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 strings.Join([]string{ref, noise, identity, adv}, ",")
|
|
}
|
|
|
|
func (cli *Client) handlePairSuccess(node *waBinary.Node) {
|
|
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)
|
|
platform, _ := pairSuccess.GetChildByTag("platform").Attrs["name"].(string)
|
|
|
|
go func() {
|
|
err := cli.handlePair(deviceIdentityBytes, id, businessName, platform, jid)
|
|
if err != nil {
|
|
cli.Log.Errorf("Failed to pair device: %v", err)
|
|
cli.Disconnect()
|
|
cli.dispatchEvent(&events.PairError{ID: jid, BusinessName: businessName, Platform: platform, Error: err})
|
|
} else {
|
|
cli.Log.Infof("Successfully paired %s", cli.Store.ID)
|
|
cli.dispatchEvent(&events.PairSuccess{ID: jid, BusinessName: businessName, Platform: platform})
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (cli *Client) handlePair(deviceIdentityBytes []byte, reqID, businessName, platform string, jid types.JID) error {
|
|
var deviceIdentityContainer waProto.ADVSignedDeviceIdentityHMAC
|
|
err := proto.Unmarshal(deviceIdentityBytes, &deviceIdentityContainer)
|
|
if err != nil {
|
|
cli.sendIQError(reqID, 500, "internal-error")
|
|
return fmt.Errorf("failed to parse device identity container in pair success message: %w", err)
|
|
}
|
|
|
|
h := hmac.New(sha256.New, cli.Store.AdvSecretKey)
|
|
h.Write(deviceIdentityContainer.Details)
|
|
if !bytes.Equal(h.Sum(nil), deviceIdentityContainer.Hmac) {
|
|
cli.Log.Warnf("Invalid HMAC from pair success message")
|
|
cli.sendIQError(reqID, 401, "not-authorized")
|
|
return fmt.Errorf("invalid device identity HMAC in pair success message")
|
|
}
|
|
|
|
var deviceIdentity waProto.ADVSignedDeviceIdentity
|
|
err = proto.Unmarshal(deviceIdentityContainer.Details, &deviceIdentity)
|
|
if err != nil {
|
|
cli.sendIQError(reqID, 500, "internal-error")
|
|
return fmt.Errorf("failed to parse signed device identity in pair success message: %w", err)
|
|
}
|
|
|
|
if !verifyDeviceIdentityAccountSignature(&deviceIdentity, cli.Store.IdentityKey) {
|
|
cli.sendIQError(reqID, 401, "not-authorized")
|
|
return fmt.Errorf("invalid device signature in pair success message")
|
|
}
|
|
|
|
deviceIdentity.DeviceSignature = generateDeviceSignature(&deviceIdentity, cli.Store.IdentityKey)[:]
|
|
|
|
var deviceIdentityDetails waProto.ADVDeviceIdentity
|
|
err = proto.Unmarshal(deviceIdentity.Details, &deviceIdentityDetails)
|
|
if err != nil {
|
|
cli.sendIQError(reqID, 500, "internal-error")
|
|
return fmt.Errorf("failed to parse device identity details in pair success message: %w", err)
|
|
}
|
|
|
|
mainDeviceJID := jid
|
|
mainDeviceJID.Device = 0
|
|
mainDeviceIdentity := *(*[32]byte)(deviceIdentity.AccountSignatureKey)
|
|
deviceIdentity.AccountSignatureKey = nil
|
|
|
|
cli.Store.Account = proto.Clone(&deviceIdentity).(*waProto.ADVSignedDeviceIdentity)
|
|
|
|
selfSignedDeviceIdentity, err := proto.Marshal(&deviceIdentity)
|
|
if err != nil {
|
|
cli.sendIQError(reqID, 500, "internal-error")
|
|
return fmt.Errorf("failed to marshal self-signed device identity: %w", err)
|
|
}
|
|
|
|
cli.Store.ID = &jid
|
|
cli.Store.BusinessName = businessName
|
|
cli.Store.Platform = platform
|
|
err = cli.Store.Save()
|
|
if err != nil {
|
|
cli.sendIQError(reqID, 500, "internal-error")
|
|
return fmt.Errorf("failed to save device store: %w", err)
|
|
}
|
|
err = cli.Store.Identities.PutIdentity(mainDeviceJID.SignalAddress().String(), mainDeviceIdentity)
|
|
if err != nil {
|
|
_ = cli.Store.Delete()
|
|
cli.sendIQError(reqID, 500, "internal-error")
|
|
return fmt.Errorf("failed to store main device identity: %w", err)
|
|
}
|
|
|
|
// Expect a disconnect after this and don't dispatch the usual Disconnected event
|
|
cli.expectDisconnect()
|
|
|
|
err = cli.sendNode(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()
|
|
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 verifyDeviceIdentityAccountSignature(deviceIdentity *waProto.ADVSignedDeviceIdentity, ikp *keys.KeyPair) bool {
|
|
if len(deviceIdentity.AccountSignatureKey) != 32 || len(deviceIdentity.AccountSignature) != 64 {
|
|
return false
|
|
}
|
|
|
|
signatureKey := ecc.NewDjbECPublicKey(*(*[32]byte)(deviceIdentity.AccountSignatureKey))
|
|
signature := *(*[64]byte)(deviceIdentity.AccountSignature)
|
|
|
|
message := concatBytes([]byte{6, 0}, deviceIdentity.Details, ikp.Pub[:])
|
|
return ecc.VerifySignature(signatureKey, message, signature)
|
|
}
|
|
|
|
func generateDeviceSignature(deviceIdentity *waProto.ADVSignedDeviceIdentity, ikp *keys.KeyPair) *[64]byte {
|
|
message := concatBytes([]byte{6, 1}, deviceIdentity.Details, ikp.Pub[:], deviceIdentity.AccountSignatureKey)
|
|
sig := ecc.CalculateSignature(ecc.NewDjbECPrivateKey(*ikp.Priv), message)
|
|
return &sig
|
|
}
|
|
|
|
func (cli *Client) sendIQError(id string, code int, text string) waBinary.Node {
|
|
return 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,
|
|
},
|
|
}},
|
|
}
|
|
}
|