summaryrefslogtreecommitdiffstats
path: root/vendor/go.mau.fi/whatsmeow/pair-code.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/pair-code.go')
-rw-r--r--vendor/go.mau.fi/whatsmeow/pair-code.go252
1 files changed, 252 insertions, 0 deletions
diff --git a/vendor/go.mau.fi/whatsmeow/pair-code.go b/vendor/go.mau.fi/whatsmeow/pair-code.go
new file mode 100644
index 00000000..183c5304
--- /dev/null
+++ b/vendor/go.mau.fi/whatsmeow/pair-code.go
@@ -0,0 +1,252 @@
+// 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
+}