-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
WhatsAppWeb.Session.js
203 lines (178 loc) · 8.1 KB
/
WhatsAppWeb.Session.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
const WebSocket = require('ws')
const Curve = require ('curve25519-js')
const HKDF = require('futoin-hkdf')
const Utils = require('./WhatsAppWeb.Utils')
const QR = require('qrcode-terminal')
/*
Contains the code for connecting to WhatsApp Web, establishing a new session & logging back in
*/
module.exports = function (WhatsAppWeb) {
const Status = WhatsAppWeb.Status
// connect to the WhatsApp Web servers
WhatsAppWeb.prototype.connect = function () {
if (this.status != Status.notConnected) {
return this.gotError([1, "already connected or connecting"])
}
this.status = Status.connecting
this.conn = new WebSocket("wss://web.whatsapp.com/ws", {origin: "https://web.whatsapp.com"})
this.conn.on('open', () => this.onConnect())
this.conn.on('message', (m) => this.onMessageRecieved(m)) // in WhatsAppWeb.Recv.js
this.conn.on('error', (error) => { // if there was an error in the WebSocket
this.close()
this.gotError([20, error])
})
this.conn.on('close', () => { })
}
// once a connection has been successfully established
WhatsAppWeb.prototype.onConnect = function () {
console.log("connected to WhatsApp Web")
this.status = Status.creatingNewConnection
if (!this.authInfo) { // if no auth info is present, that is, a new session has to be established
this.authInfo = { clientID: Utils.generateClientID() } // generate a client ID
}
const data = ["admin", "init", WhatsAppWeb.version, WhatsAppWeb.browserDescriptions, this.authInfo.clientID, true]
this.sendJSON( data )
}
// restore a previously closed session using the given authentication information
WhatsAppWeb.prototype.login = function (authInfo) {
this.authInfo = {
clientID: authInfo.clientID,
serverToken: authInfo.serverToken,
clientToken: authInfo.clientToken,
encKey: Buffer.from( authInfo.encKey, 'base64' ),
macKey: Buffer.from( authInfo.macKey, 'base64' )
}
this.connect()
}
// once the QR code is scanned and we can validate our connection,
// or we resolved the challenge when logging back in
WhatsAppWeb.prototype.validateNewConnection = function (json) {
if (json.connected) { // only if we're connected
if (!json.secret) { // if we didn't get a secret, that is we don't need it
return this.didConnectSuccessfully()
}
const secret = Buffer.from(json.secret, 'base64')
if (secret.length !== 144) {
return this.gotError([4, "incorrect secret length: " + secret.length])
}
// generate shared key from our private key & the secret shared by the server
const sharedKey = Curve.sharedKey( this.curveKeys.private, secret.slice(0, 32) )
// expand the key to 80 bytes using HKDF
const expandedKey = HKDF(sharedKey, 80, [ Buffer.alloc(32), '', 'SHA-256' ])
// perform HMAC validation.
const hmacValidationKey = expandedKey.slice(32, 64)
const hmacValidationMessage = Buffer.concat( [ secret.slice(0, 32), secret.slice(64, secret.length) ] )
const hmac = Utils.hmacSign(hmacValidationMessage, hmacValidationKey)
if ( hmac.equals(secret.slice(32, 64)) ) { // computed HMAC should equal secret[32:64]
// expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp
// they are encrypted using key: expandedKey[0:32]
const encryptedAESKeys = Buffer.concat([ expandedKey.slice(64, expandedKey.length), secret.slice(64, secret.length) ])
const decryptedKeys = Utils.aesDecrypt(encryptedAESKeys, expandedKey.slice(0,32))
// this data is required to restore closed sessions
this.authInfo = {
encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages
macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages
clientToken: json.clientToken,
serverToken: json.serverToken,
clientID: this.authInfo.clientID
}
this.userMetaData = {
id: json.wid, // one's WhatsApp ID [cc][number]@s.whatsapp.net
name: json.pushname, // name set on whatsapp
phone: json.phone // information about the phone one has logged in to
}
this.status = Status.CONNECTED
this.didConnectSuccessfully()
} else { // if the checksums didn't match
this.close()
this.gotError([5, "HMAC validation failed"])
}
} else { // if we didn't get the connected field (usually we get this message when one opens WhatsApp on their phone)
if (this.status !== Status.connected) { // and we're not already connected
this.close()
this.gotError([6, "json connection failed", json])
}
}
}
/*
when logging back in (restoring a previously closed session), WhatsApp may challenge one to check if one still has the encryption keys
WhatsApp does that by asking for us to sign a string it sends with our macKey
*/
WhatsAppWeb.prototype.respondToChallenge = function (challenge) {
const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string
const signed = Utils.hmacSign(bytes, this.authInfo.macKey).toString('base64') // sign the challenge string with our macKey
const data = ["admin", "challenge", signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID
console.log( "resolving challenge" )
this.sendJSON( data )
}
/*
when starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends
*/
WhatsAppWeb.prototype.generateKeysForAuth = function (ref) {
this.curveKeys = Curve.generateKeyPair( Utils.randomBytes(32) )
const publicKeyStr = Buffer.from(this.curveKeys.public).toString('base64')
//console.log ("private key: " + Buffer.from(this.curveKeys.private) )
let str = ref + "," + publicKeyStr + "," + this.authInfo.clientID
console.log("authenticating... Converting to QR: " + str)
QR.generate(str, {small: true})
}
// send a keep alive request every 25 seconds, server updates & responds with last seen
WhatsAppWeb.prototype.startKeepAliveRequest = function () {
this.keepAliveReq = setInterval(() => {
const diff = (new Date().getTime()-this.lastSeen.getTime())/1000
/*
check if it's been a suspicious amount of time since the server responded with our last seen
could be that the network is down, or the phone got disconnected or unpaired
*/
if (diff > 25+10) {
console.log("disconnected")
this.close()
if (this.handlers.onDisconnect)
this.handlers.onDisconnect()
if (this.autoReconnect) { // attempt reconnecting if the user wants us to
// keep trying to connect
this.reconnectLoop = setInterval( () => {
// only connect if we're not already in the prcoess of connectin
if (this.status === Status.notConnected) {
this.connect()
}
}, 10 * 1000)
}
} else { // if its all good, send a keep alive request
this.send( "?,," )
}
}, 25 * 1000)
}
// disconnect from the phone. Your auth credentials become invalid after sending a disconnect request.
// use close() if you just want to close the connection
WhatsAppWeb.prototype.disconnect = function () {
if (this.status === Status.connected) {
this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => {
this.conn.close()
if (this.handlers.onDisconnect)
this.handlers.onDisconnect()
})
} else if (this.conn) {
this.close()
}
}
// close the connection
WhatsAppWeb.prototype.close = function () {
this.conn.close()
this.conn = null
this.status = Status.notConnected
this.msgCount = 0
this.chats = {}
if (this.keepAliveReq) {
clearInterval(this.keepAliveReq)
}
}
// request a new QR code from the server (HAVEN'T TESTED THIS OUT YET)
WhatsAppWeb.prototype.requestNewQRCode = function () {
if (this.status !== Status.creatingNewConnection) { // if we're not in the process of connecting
return
}
const json = ["admin", "Conn", "reref"]
this.sendJSON(json)
}
}