diff options
Diffstat (limited to 'vendor/go.mau.fi/whatsmeow/pair-code.go')
-rw-r--r-- | vendor/go.mau.fi/whatsmeow/pair-code.go | 252 |
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 +} |