Introducción
Este proyecto pretende proporcionar una descripción completa y una nueva implementación de la API web de WhatsApp, que eventualmente conducirá a un cliente personalizado. WhatsApp Web funciona internamente usando WebSockets; este proyecto también lo hace.
Probando
Para poder ejecutar la aplicación, asegúrese de tener instalado el siguiente software:
- Node.js (al menos la versión 8, ya que
async
await
se usa la sintaxis) - el preprocesador CSS Sass (para el que previamente necesitas Ruby)
- Python 2.7 con los siguientes
pip
paquetes instalados:websocket-client
ygit+https://github.com/dpallot/simple-websocket-server.git
para actuar como servidor y cliente de WebSocketcurve25519-donna
ypycrypto
para las cosas de encriptaciónpyqrcode
para la generación de código QR
Antes de iniciar la aplicación por primera vez, ejecute npm install
para instalar todos los nodos y pip install -r requirements.txt
para todas las dependencias de Python.
Por último, para finalmente lanzarlo, simplemente ejecuta npm start
. Usando fantasía concurrently
y nodemon
magia, los tres componentes locales se iniciarán uno después del otro y cuando edite un archivo, el módulo modificado se reiniciará automáticamente para aplicar los cambios.
Arquitectura de aplicaciones
El proyecto está organizado de la siguiente manera. Tenga en cuenta los puertos usados y asegúrese de que no estén en uso en otro lugar antes de iniciar la aplicación.
Detalles de inicio de sesión y encriptación
WhatsApp Web encripta los datos usando varios algoritmos diferentes. Estos incluyen AES 256 ECB , Curve25519 como esquema de concordancia de claves Diffie-Hellman, HKDF para generar el secreto compartido extendido y HMAC con SHA256.
Iniciar la sesión web de WhatsApp ocurre simplemente conectándose a uno de sus servidores websocket en wss://w[1-8].web.whatsapp.com/ws
( wss://
significa que la conexión websocket es segura, w[1-8]
significa que cualquier número entre 1 y 8 puede seguir w
). También asegúrese de que, al establecer la conexión, Origin: https://web.whatsapp.com
se configure el encabezado HTTP ; de lo contrario, la conexión será rechazada.
Mensajes
Cuando envía mensajes a un websocket Web de WhatsApp, deben estar en un formato específico. Es bastante simple y parece messageTag,JSON
, por ejemplo 1515590796,["data",123]
. Tenga en cuenta que aparentemente la etiqueta del mensaje puede ser cualquier cosa. Esta aplicación usa principalmente la marca de tiempo actual como etiqueta, para ser un poco única. WhatsApp a menudo usa etiquetas de mensajes como s1
, 1234.--0
o algo así. Obviamente, la etiqueta del mensaje puede no contener una coma. Además, los objetos JSON son posibles, así como la carga útil.
Iniciar sesión
Para iniciar sesión en un websocket abierto, siga estos pasos:
- Genere uno propio
clientId
, que debe tener 16 bytes codificados en base64 (es decir, 25 caracteres). Esta aplicación solo usa 16 bytes aleatorios, es decir,base64.b64encode(os.urandom(16))
en Python. - Elija una etiqueta para su mensaje, que es más o menos arbitraria (ver arriba). Esta aplicación usa la marca de tiempo actual (en segundos) para eso. Recuerde esta etiqueta para más tarde.
- El mensaje se envía a la WebSocket se parece a esto:
messageTag,["admin","init",[0,2,7314],["Long browser description","ShortBrowserDesc"],"clientId",true]
.- Obviamente, debe reemplazar
messageTag
yclientId
por los valores que eligió antes - La
[0,2,7314]
parte especifica la versión actual de WhatsApp Web. El último valor cambia con frecuencia. Sin embargo, debería ser bastante retrocompatible. "Long browser description"
es una cadena arbitraria que se mostrará en la aplicación WhatsApp en la lista de clientes web de WhatsApp registrados después de escanear el código QR."ShortBrowserDesc"
no se ha observado en ningún lugar pero también es arbitrario.
- Obviamente, debe reemplazar
- Después de unos momentos, su websocket recibirá un mensaje en el formato especificado con la etiqueta de mensaje que eligió en el paso 2 . El objeto JSON de este mensaje tiene los siguientes atributos:
status
: debe ser 200ref
: en la aplicación, esto se trata como la identificación del servidor; importante para la generación de QR, ver a continuaciónttl
: es 20000, tal vez el tiempo después de que el código QR se vuelva inválidoupdate
: una bandera booleanacurr
: la versión actual de WhatsApp Web, por ejemplo0.2.7314
time
: la marca de tiempo al que respondió el servidor, como milisegundos de coma flotante, por ejemplo1515592039037.0
Generación de código QR
- Genera tu propia clave privada con Curve25519, ej
curve25519.Private()
. - Obtenga la clave pública de su clave privada, ej
privateKey.get_public()
. - Obtenga la cadena codificada posteriormente por el código QR concatenando los siguientes valores con una coma:
- la identificación del servidor, es decir, el
ref
atributo del paso 4 - la versión codificada en base64 de su clave pública, es decir,
base64.b64encode(publicKey.serialize())
- su ID de cliente
- la identificación del servidor, es decir, el
- Convierta esta cadena en una imagen (por ejemplo, usando
pyqrcode
) y escanee usando la aplicación WhatsApp.
Después de escanear el código QR
- Inmediatamente después de escanear el código QR, el websocket recibe varios mensajes JSON importantes que crean los detalles de cifrado. Estos usan el formato de mensaje especificado y tienen una matriz JSON como carga útil. Su etiqueta de mensaje no tiene un significado especial. La primera entrada de la matriz JSON tiene uno de los siguientes valores:
Conn
: array contiene el objeto JSON como segundo elemento con información de conexión que contiene los siguientes atributos y muchos más:battery
: el porcentaje de batería actual de su teléfonobrowserToken
(podría ser importante, pero no usado por la aplicación todavía)clientToken
(podría ser importante, pero no usado por la aplicación todavía)phone
: Un objeto con información detallada acerca de su teléfono, por ejemplodevice_manufacturer
,device_model
,os_build_number
,os_version
platform
: el sistema operativo de tu teléfono, por ejemploandroid
pushname
: el nombre tuyo que proporcionó WhatsAppsecret
(¡recuerda esto!)serverToken
(podría ser importante, pero no usado por la aplicación todavía)wid
: su número de teléfono en el formato de identificación de chat (ver a continuación)
Stream
: array tiene cuatro elementos en total, por lo que toda la carga es como["Stream","update",false,"0.2.7314"]
Props
: array contiene el objeto JSON como segundo elemento con varias propiedades comoimageMaxKBytes
(1024),maxParticipants
(257),videoMaxEdge
(960) y otros
Generación clave
- Ahora está listo para generar las claves de cifrado finales. Comience decodificando el
secret
fromConn
como base64 y guardándolo comosecret
. Este secreto decodificado tendrá 144 bytes de longitud. - Tome los primeros 32 bytes del secreto decodificado y úselo como clave pública. Junto con su clave privada, genere una clave compartida y llámela
sharedSecret
. La aplicación lo hace usandoprivateKey.get_shared_key(curve25519.Public(secret[:32]), lambda a:a)
. - Use una clave que contenga 32 bytes nulos para codificar el secreto compartido usando HMAC SHA256. Tome este valor y extiéndalo a 80 bytes usando HKDF. Llame a este valor
sharedSecretExpanded
. Esto se hace conHKDF(HmacSha256("\0"*32, sharedSecret), 80)
. - Este paso es opcional, valida los datos proporcionados por el servidor. El método se llama validación HMAC . Hazlo primero calculando
HmacSha256(sharedSecretExpanded[32:64], secret[:32] + secret[64:])
. Compare este valor consecret[32:64]
. Si no son iguales, cancela el inicio de sesión. - Ahora tiene las claves cifradas: almacenar
sharedSecretExpanded[64:] + secret[64:]
comokeysEncrypted
. - Las claves encriptadas ahora necesitan ser descifradas usando AES con una
sharedSecretExpanded[:32]
clave, es decir, almacenarAESDecrypt(sharedSecretExpanded[:32], keysEncrypted)
comokeysDecrypted
. - La
keysDecrypted
variable tiene 64 bytes de longitud y contiene dos claves, cada una de 32 bytes de longitud. SeencKey
usa para descifrar los mensajes binarios que te envía el servidor web de WhatsApp o para encriptar los mensajes binarios que envías al servidor. SemacKey
necesita para validar los mensajes que se le enviaron:encKey
:keysDecrypted[:32]
macKey
:keysDecrypted[32:64]
Validar y descifrar mensajes
Ahora que tiene las dos claves, validar y descifrar los mensajes que el servidor le envió es bastante fácil. Tenga en cuenta que esto solo es necesario para mensajes binarios , todos los JSON que recibe permanecen sin formato. Los mensajes binarios siempre tienen 32 bytes al principio que especifican la suma de comprobación HMAC.
- Validar el mensaje de hash del contenido del mensaje real con el
macKey
(aquímessageContent
es el entero mensaje binario):HmacSha256(macKey, messageContent[32:])
. Si este valor no es igual amessageContent[:32]
, el mensaje enviado por el servidor no es válido y debe descartarse. - Descifrar el contenido del mensaje usando un algoritmo AES y la
encKey
:AESDecrypt(encKey, messageContent[32:])
.
Los datos que obtiene en el paso final tienen un formato binario que se describe a continuación. Aunque es binario, aún puede ver varias cadenas, especialmente el contenido de los mensajes que envía es bastante obvio allí.
Formato de mensaje binario
Decodificación binaria
El script de Python backend/decoder.py
implementa la MessageParser
clase. Es capaz de crear una estructura JSON a partir de datos binarios en los que los datos aún están organizados de una manera bastante desordenada. La sección sobre Manejo de nodo a continuación discutirá cómo los nodos se reorganizan después.
MessageParser
inicialmente solo necesita algunos datos y luego los procesa byte a byte, es decir, como una secuencia. Tiene un par de constantes y muchos métodos que se complementan entre sí.
Constantes
- Etiquetas con sus respectivos valores enteros
- LIST_EMPTY : 0
- STREAM_8 : 2
- DICCIONARIO_0 : 236
- DICCIONARIO_1 : 237
- DICCIONARIO_2 : 238
- DICCIONARIO_3 : 239
- LISTA_8 : 248
- LISTA_16 : 249
- JID_PAIR : 250
- HEX_8 : 251
- BINARY_8 : 252
- BINARY_20 : 253
- BINARY_32 : 254
- NIBBLE_8 : 255
- Los tokens son una larga lista de 151 cadenas en las que importan los índices:
[None,None,None,"200","400","404","500","501","502","action","add", "after","archive","author","available","battery","before","body", "broadcast","chat","clear","code","composing","contacts","count", "create","debug","delete","demote","duplicate","encoding","error", "false","filehash","from","g.us","group","groups_v2","height","id", "image","in","index","invis","item","jid","kind","last","leave", "live","log","media","message","mimetype","missing","modify","name", "notification","notify","out","owner","participant","paused", "picture","played","presence","preview","promote","query","raw", "read","receipt","received","recipient","recording","relay", "remove","response","resume","retry","s.whatsapp.net","seconds", "set","size","status","subject","subscribe","t","text","to","true", "type","unarchive","unavailable","url","user","value","web","width", "mute","read_only","admin","creator","short","update","powersave", "checksum","epoch","block","previous","409","replaced","reason", "spam","modify_tag","message_info","delivery","emoji","title", "description","canonical-url","matched-text","star","unstar", "media_key","filename","identity","unread","page","page_count", "search","media_message","security","call_log","profile","ciphertext", "invite","gif","vcard","frequent","privacy","blacklist","whitelist", "verify","location","document","elapsed","revoke_invite","expiration", "unsubscribe","disable"]
Reformateo numérico
- Desempaquetado de nibbles : devuelve la representación ASCII para números entre 0 y 9. Devuelve
-
para 10,.
para 11 y\0
para 15. - Desempaquetar valores hexadecimales : devuelve la representación ASCII para números entre 0 y 9 o letras entre A y F (es decir, mayúsculas) para números entre 10 y 15.
- Desempaquetar bytes : espera una etiqueta como un parámetro adicional, a saber, NIBBLE_8 o HEX_8 . Desempaqueta un nibble o valor hexadecimal en consecuencia.
Formatos numéricos
- Byte : un simple byte viejo.
- Entero con N bytes : lee N bytes y construye un número a partir de ellos. Puede ser endian pequeño o grande; si no se especifica lo contrario, se usa big endian. Tenga en cuenta que no hay valores negativos posibles.
- Int16 : un entero con dos bytes, leído usando Entero con N bytes .
- Int20 : Consume tres bytes y construye un entero utilizando los últimos cuatro bits del primer byte y el segundo y tercer bytes completos. Por lo tanto, siempre es Big Endian.
- Int32 : un entero con cuatro bytes, leído usando Integer con N bytes .
- Int64 : un entero con ocho bytes, leído usando Entero con N bytes .
- Packed8 : espera una etiqueta como un parámetro adicional, a saber, NIBBLE_8 o HEX_8 . Devuelve una cadena.
- Primero lee un byte
n
y hace lo siguienten&127
muchas veces: lee un bytel
y, para cada nibble, agrega el resultado de su versión desempaquetada al valor de retorno (utilizando los bytes de desempaquetado con la etiqueta dada). El mordisco más significativo primero. - Si
n
se configuró el bit más significativo , elimina el último carácter del valor de retorno.
- Primero lee un byte
Enteros de longitud variable
A diferencia de los formatos numéricos anteriores, leer un entero de longitud variable (VLI) no cambia el puntero de datos actual.
En primer lugar, la longitud l
del VLI se lee leyendo bytes hasta encontrar un byte con el conjunto de bits más significativo, pero a lo sumo 10 bytes.
QUE HACER
Los enteros de longitud variable a distancia esperan un valor mínimo y máximo. Si el entero de longitud variable de lectura es menor que el mínimo o mayor que o igual al máximo, arroje un error.
Métodos de ayuda
- Leer bytes : lee y devuelve la cantidad de bytes especificada.
- Comprobar la etiqueta de la lista : espera una etiqueta como parámetro y devuelve verdadero si la etiqueta es
LIST_EMPTY
,LIST_8
oLIST_16
(es decir, 0, 248 o 249). - Tamaño de lista de lectura : espera una etiqueta de lista como parámetro. Devuelve 0 para
LIST_EMPTY
, devuelve un byte de lecturaLIST_8
o un Int16 de lectura paraLIST_16
. - Leer una cadena de caracteres : espera la longitud de la cadena como parámetro, lee tantos bytes y los devuelve como una cadena.
- Obtener un token : espera un índice de la matriz de tokens y devuelve la cadena respectiva.
- Obtener una doble señal : Espera dos enteros
a
yb
y se pone el token en el índicea*256+b
.
Instrumentos de cuerda
Leer una cadena necesita una etiqueta como parámetro. Dependiendo de esta etiqueta, se leen diferentes datos.
- Si la etiqueta está entre 3 y 235, se obtiene el token (es decir, una cadena) de esta etiqueta. Si el token es
"s.whatsapp.net"
,"c.us"
se devuelve en su lugar, de lo contrario, el token se devuelve tal como está. - Si la etiqueta se encuentra entre DICTIONARY_0 y DICTIONARY_3 , se devuelve un token doble , con el
tag-DICTIONARY_0
primero y un byte de lectura como segundo parámetro. - LIST_EMPTY : no se devuelve nada (por ejemplo
None
). - BINARY_8 : se lee un byte que luego se usa para leer una cadena de caracteres con esta longitud.
- BINARY_20 : se lee un Int20 que luego se usa para leer una cadena de caracteres con esta longitud.
- BINARY_32 : se lee un Int32 que luego se usa para leer una cadena de caracteres con esta longitud.
- JID_PAIR
- Primero, se lee un byte que luego se usa para leer una cadena
i
con esta etiqueta. - En segundo lugar, se lee otro byte que luego se usa para leer una cadena
j
con esta etiqueta. - Por último,
i
yj
se unen entre sí con un@
signo y se devuelve el resultado.
- Primero, se lee un byte que luego se usa para leer una cadena
- NIBBLE_8 o HEX_8 : se devuelve un Packed8 con esta etiqueta.
Listas de atributos
Leer una lista de atributos necesita la cantidad de atributos para leer como parámetro. Una lista de atributos siempre es un objeto JSON. Para cada atributo leído, los siguientes pasos se ejecutan para obtener pares clave-valor (¡exactamente en este orden!):
- Clave : se lee un byte que luego se usa para leer una cadena con esta etiqueta.
- Valor : se lee un byte que luego se usa para leer una cadena con esta etiqueta.
Nodos
Un nodo siempre consta de una matriz JSON con exactamente tres entradas: descripción, atributos y contenido. Los siguientes pasos son necesarios para leer un nodo:
- Un tamaño de lista
a
se lee utilizando un byte de lectura como etiqueta. El tamaño 0 de la lista no es válido. - La etiqueta de descripción se lee como un byte. El valor 2 no es válido para esta etiqueta. La cadena de descripción
descr
se obtiene leyendo una cadena con esta etiqueta. - El objeto de atributos
attrs
se lee leyendo un objeto de atributos con longitud(a-2 + a%2) >> 1
. - Si
a
fue impar, este nodo no tiene ningún contenido,[descr, attrs, None]
es decir, se devuelve. - Para obtener el contenido del nodo, primero un byte, es decir, se lee una etiqueta. Dependiendo de esta etiqueta, surgen diferentes tipos de contenido:
- Si la etiqueta es una etiqueta de lista , se lee una lista con esta etiqueta (consulte las listas a continuación).
- BINARY_8 : se lee un byte que luego se usa como longitud para leer bytes .
- BINARY_20 : se lee un Int20 que luego se usa como longitud para leer bytes .
- BINARY_32 : se lee un Int32 que luego se usa como longitud para leer bytes .
- Si la etiqueta es otra cosa, se lee una cadena con esta etiqueta.
- Eventualmente,
[descr, attrs, content]
es devuelto.
Liza
La lectura de una lista requiere una lista de etiquetas (es decir LIST_EMPTY , LIST_8 o LIST_16 ). La longitud de la lista se obtiene leyendo un tamaño de lista usando esta etiqueta. Para cada entrada de lista, se lee un nodo .
Manejo del nodo
QUE HACER
API web de WhatsApp
WhatsApp Web también tiene una API interesante. Incluso puedes probarlo directamente en tu navegador. Simplemente inicie sesión en la https://web.whatsapp.com/ normal , luego abra la consola de desarrollo del navegador. Ahora ingrese algo como lo siguiente (vea a continuación los detalles sobre la identificación del chat):
window.Store.Wap.profilePicFind("49123456789@c.us").then(res => console.log(res));
window.Store.Wap.lastseenFind("49123456789@c.us").then(res => console.log(res));
window.Store.Wap.statusFind("49123456789@c.us").then(res => console.log(res));
Usando la increíble consola de desarrollo de Chrome, puedes ver que window.Store.Wap
contiene muchas otras funciones muy interesantes. Muchos de ellos devuelven promesas de JavaScript. Cuando hace clic en la pestaña Red y luego en WS (tal vez necesita volver a cargar el sitio primero), puede ver toda la comunicación entre WhatsApp Web y sus servidores.
Identificación de chat
La API web de WhatsApp usa los siguientes formatos para identificar chats con usuarios individuales y grupos de usuarios múltiples.
- Chats :,
[country code][number]@c.us
por ejemplo,49123456789@c.us
cuando eres de Alemania y tu número de teléfono es0123 456789
. - Grupos :
[phone number of group creator]-[timestamp of group creation]@g.us
por ejemplo,49123456789-1509911919@g.us
para el grupo que49123456789@c.us
creó el 5 de noviembre de 2017.
Mensajes de WebSocket
Hay dos tipos de mensajes WebSocket que se intercambian entre el servidor y el cliente. Por un lado, JSON simple que es bastante inequívoco (especialmente para las llamadas a API anteriores), por otro lado, mensajes binarios encriptados.
Lamentablemente, estos binarios no se pueden analizar con las herramientas de desarrollo de Chrome. Además, el back-end de Python, que por supuesto también recibe estos mensajes, necesita descifrarlos, ya que contienen datos encriptados. La sección sobre detalles de encriptación explica cómo se puede descifrar.
Tareas
Backend
- Permitir el envío de mensajes también. Por supuesto, JSON es fácil, pero la escritura del formato de mensaje binario necesita comenzar a ser examinada.
Interfaz web
- Permitir reutilizar la sesión después de iniciar sesión correctamente. Probablemente las cookies normales son las mejores para esto.
- Una interfaz de usuario que no es tan técnica, sino que más bien comienza a emular la interfaz de usuario web de WhatsApp real.
Documentación
- La sección Manejo de nodos . Podría ser muy largo.
- La sección de Descargo de responsabilidad . Debe contener cosas como "sin garantía" y "no hacer nada malo".
- Externalice las diferentes partes de la documentación en sus propios archivos, tal vez en la
gh-pages
sucursal.
Términos y Condiciones
- Usted no utilizar este software con fines de comercialización (correo no deseado, el envío masivo ...). No apoyaremos a nadie con tales intenciones.
- Nos reservamos el derecho de bloquear a cualquier usuario de este repositorio que no cumpla con estas condiciones.
Legal
Este código no está de ninguna manera afiliado, autorizado, mantenido, patrocinado o respaldado por WhatsApp o cualquiera de sus afiliadas o subsidiarias. Este es un software independiente y no oficial. Úselo bajo su propio riesgo.
Aviso de criptografía
Esta distribución incluye software criptográfico. El país en el que reside actualmente puede tener restricciones sobre la importación, posesión, uso y / o reexportación a otro país de software de cifrado. ANTES de utilizar cualquier software de cifrado, compruebe las leyes, normativas y políticas de su país relativas a la importación, posesión o uso, y vuelva a exportar el software de cifrado, para ver si esto está permitido. Ver http://www.wassenaar.org/ para más información.
El Departamento de Comercio del Gobierno de los Estados Unidos, Oficina de Industria y Seguridad (BIS), ha clasificado este software como Número de control de productos básicos de exportación (ECCN) 5D002.C.1, que incluye software de seguridad de la información que utiliza o realiza funciones criptográficas con algoritmos asimétricos. La forma y la forma de esta distribución hacen que sea elegible para exportar bajo la excepción de excepción de licencia del software de tecnología ENC de licencia (TSU) (ver el Reglamento de administración de exportación de BIS, Sección 740.13) para código de objeto y código fuente.
Respuestas