// 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 }