pair-code.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. // Copyright (c) 2023 Tulir Asokan
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. package whatsmeow
  7. import (
  8. "context"
  9. "crypto/aes"
  10. "crypto/cipher"
  11. "crypto/sha256"
  12. "encoding/base32"
  13. "fmt"
  14. "regexp"
  15. "strconv"
  16. "strings"
  17. "go.mau.fi/util/random"
  18. "golang.org/x/crypto/curve25519"
  19. "golang.org/x/crypto/pbkdf2"
  20. waBinary "git.bobomao.top/joey/testwh/binary"
  21. "git.bobomao.top/joey/testwh/types"
  22. "git.bobomao.top/joey/testwh/util/hkdfutil"
  23. "git.bobomao.top/joey/testwh/util/keys"
  24. )
  25. // PairClientType is the type of client to use with PairCode.
  26. // The type is automatically filled based on store.DeviceProps.PlatformType (which is what QR login uses).
  27. type PairClientType int
  28. const (
  29. PairClientUnknown PairClientType = iota
  30. PairClientChrome
  31. PairClientEdge
  32. PairClientFirefox
  33. PairClientIE
  34. PairClientOpera
  35. PairClientSafari
  36. PairClientElectron
  37. PairClientUWP
  38. PairClientOtherWebClient
  39. )
  40. var notNumbers = regexp.MustCompile("[^0-9]")
  41. var linkingBase32 = base32.NewEncoding("123456789ABCDEFGHJKLMNPQRSTVWXYZ")
  42. type phoneLinkingCache struct {
  43. jid types.JID
  44. keyPair *keys.KeyPair
  45. linkingCode string
  46. pairingRef string
  47. }
  48. func generateCompanionEphemeralKey() (ephemeralKeyPair *keys.KeyPair, ephemeralKey []byte, encodedLinkingCode string) {
  49. ephemeralKeyPair = keys.NewKeyPair()
  50. salt := random.Bytes(32)
  51. iv := random.Bytes(16)
  52. linkingCode := random.Bytes(5)
  53. encodedLinkingCode = linkingBase32.EncodeToString(linkingCode)
  54. linkCodeKey := pbkdf2.Key([]byte(encodedLinkingCode), salt, 2<<16, 32, sha256.New)
  55. linkCipherBlock, _ := aes.NewCipher(linkCodeKey)
  56. encryptedPubkey := ephemeralKeyPair.Pub[:]
  57. cipher.NewCTR(linkCipherBlock, iv).XORKeyStream(encryptedPubkey, encryptedPubkey)
  58. ephemeralKey = make([]byte, 80)
  59. copy(ephemeralKey[0:32], salt)
  60. copy(ephemeralKey[32:48], iv)
  61. copy(ephemeralKey[48:80], encryptedPubkey)
  62. return
  63. }
  64. // PairPhone generates a pairing code that can be used to link to a phone without scanning a QR code.
  65. //
  66. // You must connect the client normally before calling this (which means you'll also receive a QR code
  67. // event, but that can be ignored when doing code pairing). You should also wait for `*events.QR` before
  68. // calling this to ensure the connection is fully established. If using [Client.GetQRChannel], wait for
  69. // the first item in the channel. Alternatively, sleeping for a second after calling Connect will probably work too.
  70. //
  71. // The exact expiry of pairing codes is unknown, but QR codes are always generated and the login websocket is closed
  72. // after the QR codes run out, which means there's a 160-second time limit. It is recommended to generate the pairing
  73. // code immediately after connecting to the websocket to have the maximum time.
  74. //
  75. // The clientType parameter must be one of the PairClient* constants, but which one doesn't matter.
  76. // The client display name must be formatted as `Browser (OS)`, and only common browsers/OSes are allowed
  77. // (the server will validate it and return 400 if it's wrong).
  78. //
  79. // See https://faq.whatsapp.com/1324084875126592 for more info
  80. func (cli *Client) PairPhone(ctx context.Context, phone string, showPushNotification bool, clientType PairClientType, clientDisplayName string) (string, error) {
  81. if cli == nil {
  82. return "", ErrClientIsNil
  83. }
  84. ephemeralKeyPair, ephemeralKey, encodedLinkingCode := generateCompanionEphemeralKey()
  85. phone = notNumbers.ReplaceAllString(phone, "")
  86. if len(phone) <= 6 {
  87. return "", ErrPhoneNumberTooShort
  88. } else if strings.HasPrefix(phone, "0") {
  89. return "", ErrPhoneNumberIsNotInternational
  90. }
  91. jid := types.NewJID(phone, types.DefaultUserServer)
  92. resp, err := cli.sendIQ(ctx, infoQuery{
  93. Namespace: "md",
  94. Type: iqSet,
  95. To: types.ServerJID,
  96. Content: []waBinary.Node{{
  97. Tag: "link_code_companion_reg",
  98. Attrs: waBinary.Attrs{
  99. "jid": jid,
  100. "stage": "companion_hello",
  101. "should_show_push_notification": strconv.FormatBool(showPushNotification),
  102. },
  103. Content: []waBinary.Node{
  104. {Tag: "link_code_pairing_wrapped_companion_ephemeral_pub", Content: ephemeralKey},
  105. {Tag: "companion_server_auth_key_pub", Content: cli.Store.NoiseKey.Pub[:]},
  106. {Tag: "companion_platform_id", Content: strconv.Itoa(int(clientType))},
  107. {Tag: "companion_platform_display", Content: clientDisplayName},
  108. {Tag: "link_code_pairing_nonce", Content: []byte{0}},
  109. },
  110. }},
  111. })
  112. if err != nil {
  113. return "", err
  114. }
  115. pairingRefNode, ok := resp.GetOptionalChildByTag("link_code_companion_reg", "link_code_pairing_ref")
  116. if !ok {
  117. return "", &ElementMissingError{Tag: "link_code_pairing_ref", In: "code link registration response"}
  118. }
  119. pairingRef, ok := pairingRefNode.Content.([]byte)
  120. if !ok {
  121. return "", fmt.Errorf("unexpected type %T in content of link_code_pairing_ref tag", pairingRefNode.Content)
  122. }
  123. cli.phoneLinkingCache = &phoneLinkingCache{
  124. jid: jid,
  125. keyPair: ephemeralKeyPair,
  126. linkingCode: encodedLinkingCode,
  127. pairingRef: string(pairingRef),
  128. }
  129. return encodedLinkingCode[0:4] + "-" + encodedLinkingCode[4:], nil
  130. }
  131. func (cli *Client) tryHandleCodePairNotification(ctx context.Context, parentNode *waBinary.Node) {
  132. err := cli.handleCodePairNotification(ctx, parentNode)
  133. if err != nil {
  134. cli.Log.Errorf("Failed to handle code pair notification: %s", err)
  135. }
  136. }
  137. func (cli *Client) handleCodePairNotification(ctx context.Context, parentNode *waBinary.Node) error {
  138. node, ok := parentNode.GetOptionalChildByTag("link_code_companion_reg")
  139. if !ok {
  140. return &ElementMissingError{
  141. Tag: "link_code_companion_reg",
  142. In: "notification",
  143. }
  144. }
  145. linkCache := cli.phoneLinkingCache
  146. if linkCache == nil {
  147. return fmt.Errorf("received code pair notification without a pending pairing")
  148. }
  149. linkCodePairingRef, _ := node.GetChildByTag("link_code_pairing_ref").Content.([]byte)
  150. if string(linkCodePairingRef) != linkCache.pairingRef {
  151. return fmt.Errorf("pairing ref mismatch in code pair notification")
  152. }
  153. wrappedPrimaryEphemeralPub, ok := node.GetChildByTag("link_code_pairing_wrapped_primary_ephemeral_pub").Content.([]byte)
  154. if !ok {
  155. return &ElementMissingError{
  156. Tag: "link_code_pairing_wrapped_primary_ephemeral_pub",
  157. In: "notification",
  158. }
  159. }
  160. primaryIdentityPub, ok := node.GetChildByTag("primary_identity_pub").Content.([]byte)
  161. if !ok {
  162. return &ElementMissingError{
  163. Tag: "primary_identity_pub",
  164. In: "notification",
  165. }
  166. }
  167. advSecretRandom := random.Bytes(32)
  168. keyBundleSalt := random.Bytes(32)
  169. keyBundleNonce := random.Bytes(12)
  170. // Decrypt the primary device's ephemeral public key, which was encrypted with the 8-character pairing code,
  171. // then compute the DH shared secret using our ephemeral private key we generated earlier.
  172. primarySalt := wrappedPrimaryEphemeralPub[0:32]
  173. primaryIV := wrappedPrimaryEphemeralPub[32:48]
  174. primaryEncryptedPubkey := wrappedPrimaryEphemeralPub[48:80]
  175. linkCodeKey := pbkdf2.Key([]byte(linkCache.linkingCode), primarySalt, 2<<16, 32, sha256.New)
  176. linkCipherBlock, err := aes.NewCipher(linkCodeKey)
  177. if err != nil {
  178. return fmt.Errorf("failed to create link cipher: %w", err)
  179. }
  180. primaryDecryptedPubkey := make([]byte, 32)
  181. cipher.NewCTR(linkCipherBlock, primaryIV).XORKeyStream(primaryDecryptedPubkey, primaryEncryptedPubkey)
  182. ephemeralSharedSecret, err := curve25519.X25519(linkCache.keyPair.Priv[:], primaryDecryptedPubkey)
  183. if err != nil {
  184. return fmt.Errorf("failed to compute ephemeral shared secret: %w", err)
  185. }
  186. // Encrypt and wrap key bundle containing our identity key, the primary device's identity key and the randomness used for the adv key.
  187. keyBundleEncryptionKey := hkdfutil.SHA256(ephemeralSharedSecret, keyBundleSalt, []byte("link_code_pairing_key_bundle_encryption_key"), 32)
  188. keyBundleCipherBlock, err := aes.NewCipher(keyBundleEncryptionKey)
  189. if err != nil {
  190. return fmt.Errorf("failed to create key bundle cipher: %w", err)
  191. }
  192. keyBundleGCM, err := cipher.NewGCM(keyBundleCipherBlock)
  193. if err != nil {
  194. return fmt.Errorf("failed to create key bundle GCM: %w", err)
  195. }
  196. plaintextKeyBundle := concatBytes(cli.Store.IdentityKey.Pub[:], primaryIdentityPub, advSecretRandom)
  197. encryptedKeyBundle := keyBundleGCM.Seal(nil, keyBundleNonce, plaintextKeyBundle, nil)
  198. wrappedKeyBundle := concatBytes(keyBundleSalt, keyBundleNonce, encryptedKeyBundle)
  199. // Compute the adv secret key (which is used to authenticate the pair-success event later)
  200. identitySharedKey, err := curve25519.X25519(cli.Store.IdentityKey.Priv[:], primaryIdentityPub)
  201. if err != nil {
  202. return fmt.Errorf("failed to compute identity shared key: %w", err)
  203. }
  204. advSecretInput := append(append(ephemeralSharedSecret, identitySharedKey...), advSecretRandom...)
  205. advSecret := hkdfutil.SHA256(advSecretInput, nil, []byte("adv_secret"), 32)
  206. cli.Store.AdvSecretKey = advSecret
  207. _, err = cli.sendIQ(ctx, infoQuery{
  208. Namespace: "md",
  209. Type: iqSet,
  210. To: types.ServerJID,
  211. Content: []waBinary.Node{{
  212. Tag: "link_code_companion_reg",
  213. Attrs: waBinary.Attrs{
  214. "jid": linkCache.jid,
  215. "stage": "companion_finish",
  216. },
  217. Content: []waBinary.Node{
  218. {Tag: "link_code_pairing_wrapped_key_bundle", Content: wrappedKeyBundle},
  219. {Tag: "companion_identity_public", Content: cli.Store.IdentityKey.Pub[:]},
  220. {Tag: "link_code_pairing_ref", Content: linkCodePairingRef},
  221. },
  222. }},
  223. })
  224. return err
  225. }