matterbridge/vendor/go.mau.fi/whatsmeow/pair-code.go

253 lines
9.0 KiB
Go

// Copyright (c) 2023 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 (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base32"
"fmt"
"regexp"
"strconv"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/pbkdf2"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/hkdfutil"
"go.mau.fi/whatsmeow/util/keys"
"go.mau.fi/whatsmeow/util/randbytes"
)
// PairClientType is the type of client to use with PairCode.
// The type is automatically filled based on store.DeviceProps.PlatformType (which is what QR login uses).
type PairClientType int
const (
PairClientUnknown PairClientType = iota
PairClientChrome
PairClientEdge
PairClientFirefox
PairClientIE
PairClientOpera
PairClientSafari
PairClientElectron
PairClientUWP
PairClientOtherWebClient
)
func platformTypeToPairClientType(platformType waProto.DeviceProps_PlatformType) PairClientType {
switch platformType {
case waProto.DeviceProps_CHROME:
return PairClientChrome
case waProto.DeviceProps_EDGE:
return PairClientEdge
case waProto.DeviceProps_FIREFOX:
return PairClientFirefox
case waProto.DeviceProps_IE:
return PairClientIE
case waProto.DeviceProps_OPERA:
return PairClientOpera
case waProto.DeviceProps_SAFARI:
return PairClientSafari
case waProto.DeviceProps_DESKTOP:
return PairClientElectron
case waProto.DeviceProps_UWP:
return PairClientUWP
default:
return PairClientOtherWebClient
}
}
var notNumbers = regexp.MustCompile("[^0-9]")
var linkingBase32 = base32.NewEncoding("123456789ABCDEFGHJKLMNPQRSTVWXYZ")
type phoneLinkingCache struct {
jid types.JID
keyPair *keys.KeyPair
linkingCode string
pairingRef string
}
func generateCompanionEphemeralKey() (ephemeralKeyPair *keys.KeyPair, ephemeralKey []byte, encodedLinkingCode string) {
ephemeralKeyPair = keys.NewKeyPair()
salt := randbytes.Make(32)
iv := randbytes.Make(16)
linkingCode := randbytes.Make(5)
encodedLinkingCode = linkingBase32.EncodeToString(linkingCode)
linkCodeKey := pbkdf2.Key([]byte(encodedLinkingCode), salt, 2<<16, 32, sha256.New)
linkCipherBlock, _ := aes.NewCipher(linkCodeKey)
encryptedPubkey := ephemeralKeyPair.Pub[:]
cipher.NewCTR(linkCipherBlock, iv).XORKeyStream(encryptedPubkey, encryptedPubkey)
ephemeralKey = make([]byte, 80)
copy(ephemeralKey[0:32], salt)
copy(ephemeralKey[32:48], iv)
copy(ephemeralKey[48:80], encryptedPubkey)
return
}
// PairPhone generates a pairing code that can be used to link to a phone without scanning a QR code.
//
// The exact expiry of pairing codes is unknown, but QR codes are always generated and the login websocket is closed
// after the QR codes run out, which means there's a 160-second time limit. It is recommended to generate the pairing
// code immediately after connecting to the websocket to have the maximum time.
//
// See https://faq.whatsapp.com/1324084875126592 for more info
func (cli *Client) PairPhone(phone string, showPushNotification bool) (string, error) {
clientType := platformTypeToPairClientType(store.DeviceProps.GetPlatformType())
clientDisplayName := store.DeviceProps.GetOs()
ephemeralKeyPair, ephemeralKey, encodedLinkingCode := generateCompanionEphemeralKey()
phone = notNumbers.ReplaceAllString(phone, "")
jid := types.NewJID(phone, types.DefaultUserServer)
resp, err := cli.sendIQ(infoQuery{
Namespace: "md",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "link_code_companion_reg",
Attrs: waBinary.Attrs{
"jid": jid,
"stage": "companion_hello",
"should_show_push_notification": strconv.FormatBool(showPushNotification),
},
Content: []waBinary.Node{
{Tag: "link_code_pairing_wrapped_companion_ephemeral_pub", Content: ephemeralKey},
{Tag: "companion_server_auth_key_pub", Content: cli.Store.NoiseKey.Pub[:]},
{Tag: "companion_platform_id", Content: strconv.Itoa(int(clientType))},
{Tag: "companion_platform_display", Content: clientDisplayName},
{Tag: "link_code_pairing_nonce", Content: []byte{0}},
},
}},
})
if err != nil {
return "", err
}
pairingRefNode, ok := resp.GetOptionalChildByTag("link_code_companion_reg", "link_code_pairing_ref")
if !ok {
return "", &ElementMissingError{Tag: "link_code_pairing_ref", In: "code link registration response"}
}
pairingRef, ok := pairingRefNode.Content.([]byte)
if !ok {
return "", fmt.Errorf("unexpected type %T in content of link_code_pairing_ref tag", pairingRefNode.Content)
}
cli.phoneLinkingCache = &phoneLinkingCache{
jid: jid,
keyPair: ephemeralKeyPair,
linkingCode: encodedLinkingCode,
pairingRef: string(pairingRef),
}
return encodedLinkingCode[0:4] + "-" + encodedLinkingCode[4:], nil
}
func (cli *Client) tryHandleCodePairNotification(parentNode *waBinary.Node) {
err := cli.handleCodePairNotification(parentNode)
if err != nil {
cli.Log.Errorf("Failed to handle code pair notification: %s", err)
}
}
func (cli *Client) handleCodePairNotification(parentNode *waBinary.Node) error {
node, ok := parentNode.GetOptionalChildByTag("link_code_companion_reg")
if !ok {
return &ElementMissingError{
Tag: "link_code_companion_reg",
In: "notification",
}
}
linkCache := cli.phoneLinkingCache
if linkCache == nil {
return fmt.Errorf("received code pair notification without a pending pairing")
}
linkCodePairingRef, _ := node.GetChildByTag("link_code_pairing_ref").Content.([]byte)
if string(linkCodePairingRef) != linkCache.pairingRef {
return fmt.Errorf("pairing ref mismatch in code pair notification")
}
wrappedPrimaryEphemeralPub, ok := node.GetChildByTag("link_code_pairing_wrapped_primary_ephemeral_pub").Content.([]byte)
if !ok {
return &ElementMissingError{
Tag: "link_code_pairing_wrapped_primary_ephemeral_pub",
In: "notification",
}
}
primaryIdentityPub, ok := node.GetChildByTag("primary_identity_pub").Content.([]byte)
if !ok {
return &ElementMissingError{
Tag: "primary_identity_pub",
In: "notification",
}
}
advSecretRandom := randbytes.Make(32)
keyBundleSalt := randbytes.Make(32)
keyBundleNonce := randbytes.Make(12)
// Decrypt the primary device's ephemeral public key, which was encrypted with the 8-character pairing code,
// then compute the DH shared secret using our ephemeral private key we generated earlier.
primarySalt := wrappedPrimaryEphemeralPub[0:32]
primaryIV := wrappedPrimaryEphemeralPub[32:48]
primaryEncryptedPubkey := wrappedPrimaryEphemeralPub[48:80]
linkCodeKey := pbkdf2.Key([]byte(linkCache.linkingCode), primarySalt, 2<<16, 32, sha256.New)
linkCipherBlock, err := aes.NewCipher(linkCodeKey)
if err != nil {
return fmt.Errorf("failed to create link cipher: %w", err)
}
primaryDecryptedPubkey := make([]byte, 32)
cipher.NewCTR(linkCipherBlock, primaryIV).XORKeyStream(primaryDecryptedPubkey, primaryEncryptedPubkey)
ephemeralSharedSecret, err := curve25519.X25519(linkCache.keyPair.Priv[:], primaryDecryptedPubkey)
if err != nil {
return fmt.Errorf("failed to compute ephemeral shared secret: %w", err)
}
// Encrypt and wrap key bundle containing our identity key, the primary device's identity key and the randomness used for the adv key.
keyBundleEncryptionKey := hkdfutil.SHA256(ephemeralSharedSecret, keyBundleSalt, []byte("link_code_pairing_key_bundle_encryption_key"), 32)
keyBundleCipherBlock, err := aes.NewCipher(keyBundleEncryptionKey)
if err != nil {
return fmt.Errorf("failed to create key bundle cipher: %w", err)
}
keyBundleGCM, err := cipher.NewGCM(keyBundleCipherBlock)
if err != nil {
return fmt.Errorf("failed to create key bundle GCM: %w", err)
}
plaintextKeyBundle := concatBytes(cli.Store.IdentityKey.Pub[:], primaryIdentityPub, advSecretRandom)
encryptedKeyBundle := keyBundleGCM.Seal(nil, keyBundleNonce, plaintextKeyBundle, nil)
wrappedKeyBundle := concatBytes(keyBundleSalt, keyBundleNonce, encryptedKeyBundle)
// Compute the adv secret key (which is used to authenticate the pair-success event later)
identitySharedKey, err := curve25519.X25519(cli.Store.IdentityKey.Priv[:], primaryIdentityPub)
if err != nil {
return fmt.Errorf("failed to compute identity shared key: %w", err)
}
advSecretInput := append(append(ephemeralSharedSecret, identitySharedKey...), advSecretRandom...)
advSecret := hkdfutil.SHA256(advSecretInput, nil, []byte("adv_secret"), 32)
cli.Store.AdvSecretKey = advSecret
_, err = cli.sendIQ(infoQuery{
Namespace: "md",
Type: iqSet,
To: types.ServerJID,
Content: []waBinary.Node{{
Tag: "link_code_companion_reg",
Attrs: waBinary.Attrs{
"jid": linkCache.jid,
"stage": "companion_finish",
},
Content: []waBinary.Node{
{Tag: "link_code_pairing_wrapped_key_bundle", Content: wrappedKeyBundle},
{Tag: "companion_identity_public", Content: cli.Store.IdentityKey.Pub[:]},
{Tag: "link_code_pairing_ref", Content: linkCodePairingRef},
},
}},
})
return err
}