Skip to content

Commit

Permalink
Implement e2e media decryption
Browse files Browse the repository at this point in the history
Add MediaDecryption class to decrypt images, videos, audio and documents
  • Loading branch information
Louuke committed Aug 8, 2020
1 parent 6e47d8c commit 45fd84d
Show file tree
Hide file tree
Showing 33 changed files with 158 additions and 40 deletions.
Binary file modified bin/main/icu/jnet/whatsjava/Main$1.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/Main.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/encryption/AES.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/encryption/BinaryDecoder.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/encryption/BinaryEncryption.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/encryption/EncryptionKeys.class
Binary file not shown.
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/helper/Utils.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/web/WebConversationMessage.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/web/WebImageMessage.class
Binary file not shown.
Binary file modified bin/main/icu/jnet/whatsjava/web/WebVideoMessage.class
Binary file not shown.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ dependencies {
// https://mvnrepository.com/artifact/commons-codec/commons-codec
compile group: 'commons-codec', name: 'commons-codec', version: '1.14'

// https://mvnrepository.com/artifact/commons-io/commons-io
compile group: 'commons-io', name: 'commons-io', version: '2.7'

// https://mvnrepository.com/artifact/org.whispersystems/curve25519-java
compile group: 'org.whispersystems', name: 'curve25519-java', version: '0.5.0'

Expand Down
Binary file modified build/classes/java/main/icu/jnet/whatsjava/Main$1.class
Binary file not shown.
Binary file modified build/classes/java/main/icu/jnet/whatsjava/Main.class
Binary file not shown.
Binary file modified build/classes/java/main/icu/jnet/whatsjava/encryption/AES.class
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified build/classes/java/main/icu/jnet/whatsjava/helper/Utils.class
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified build/libs/WhatsJava-all.jar
Binary file not shown.
5 changes: 0 additions & 5 deletions src/main/java/icu/jnet/whatsjava/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Base64;

import javax.imageio.ImageIO;

import icu.jnet.whatsjava.helper.Utils;
import icu.jnet.whatsjava.web.WebChat;
import icu.jnet.whatsjava.web.WebConversationMessage;
import icu.jnet.whatsjava.web.WebImageMessage;
import icu.jnet.whatsjava.web.WebVideoMessage;

public class Main {

Expand Down
20 changes: 7 additions & 13 deletions src/main/java/icu/jnet/whatsjava/constants/ExpectedResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,11 @@ public class ExpectedResponse {

// Excepted message type for the next message of the backend

public static final byte LOGIN = 0;

public static final byte NEW_SERVER_ID = 1;

public static final byte SCAN_QR_CODE = 2;

public static final byte RESTORE_SESSION = 3;

public static final byte RESOLVE_CHALLENGE = 4;

public static final byte LOGGING_OUT = 5;

public static final byte MESSAGE_GENERIC = 6;
public static final byte LOGIN = 0,
NEW_SERVER_ID = 1,
SCAN_QR_CODE = 2,
RESTORE_SESSION = 3,
RESOLVE_CHALLENGE = 4,
LOGGING_OUT = 5,
MESSAGE_GENERIC = 6;
}
17 changes: 16 additions & 1 deletion src/main/java/icu/jnet/whatsjava/encryption/AES.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,22 @@ static byte[] decrypt(byte[] encrypted, byte[] secretKey) {
return null;
}

// Encrypt it using AES and encKey
// Used for E2E media decryption in the MediaEncryption class
static byte[] decrypt(byte[] encrypted, byte[] secretKey, byte[] iv) {
try
{
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING"); // PKCS5PADDING required
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv));

return cipher.doFinal(encrypted);
} catch (Exception e) {
System.out.println("Error while decrypting: " + e.toString());
}
return null;
}

// Encrypt using AES and encKey
static byte[] encrypt(byte[] decrypted, byte[] encKey) {
try
{
Expand Down
10 changes: 5 additions & 5 deletions src/main/java/icu/jnet/whatsjava/encryption/BinaryDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -279,26 +279,26 @@ private String readNode() {

try {
switch(tag) {
// "message" message
// Conversation messages
case BinaryConstants.Tags.BINARY_8:
byte[] bin8 = readBytes(readByte() & 0xff);
base64Decoded = Base64.getEncoder().encodeToString(
WebMessageInfo.parseFrom(bin8).toByteArray());

break;
// video & image message
// Image, video, extended and rarely conversation messages
case BinaryConstants.Tags.BINARY_20:
byte[] bin20 = readBytes(readInt20());

base64Decoded = Base64.getEncoder().encodeToString(
WebMessageInfo.parseFrom(bin20).toByteArray());

break;
// ?
case BinaryConstants.Tags.BINARY_32:
byte[] bin32 = readBytes(readInt(4, false));

base64Decoded = Base64.getEncoder().encodeToString(
WebMessageInfo.parseFrom(bin32).toByteArray());

break;
default:
base64Decoded = readString(tag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

public class BinaryEncryption {

/*
* Encrypt and decrypt binary messages, but no E2E media
*
*/

public static byte[] decrypt(byte[] message, EncryptionKeyPair keyPair) throws DecoderException {
// Encode byte array to hex char
String hexMessage = Hex.encodeHexString(message, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import org.whispersystems.curve25519.Curve25519;

import at.favre.lib.crypto.HKDF;
import icu.jnet.whatsjava.helper.Utils;

public class EncryptionKeys {
Expand All @@ -25,7 +24,7 @@ public static EncryptionKeyPair generate(String base64Secret, byte[] privateKey)
byte[] sharedSecret = Curve25519.getInstance(Curve25519.BEST)
.calculateAgreement(publicKey, privateKey);
// Expand the shared key to 80 bytes using HKDF
byte[] sharedSecretExpanded = expandUsingHKDF(sharedSecret, 80);
byte[] sharedSecretExpanded = Utils.expandUsingHKDF(sharedSecret, 80, null);

// Validate data by HMAC
boolean valid = hmacValidate(sharedSecretExpanded, secret);
Expand Down Expand Up @@ -61,11 +60,6 @@ public static EncryptionKeyPair generate(String base64Secret, byte[] privateKey)
return null;
}

private static byte[] expandUsingHKDF(byte[] key, int length) {
byte[] pseudoRandomKey = HKDF.fromHmacSha256().extract(null, key);
return HKDF.fromHmacSha256().expand(pseudoRandomKey, null, length);
}

private static boolean hmacValidate(byte[] sharedSecretExpanded, byte[] secret) {
byte[] hmacValidationKey = Arrays.copyOfRange(sharedSecretExpanded, 32, 64);
byte[] hmacSecretA = Arrays.copyOfRange(secret, 0, 32);
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/icu/jnet/whatsjava/encryption/MediaEncryption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package icu.jnet.whatsjava.encryption;

import java.nio.ByteBuffer;
import java.util.Arrays;

import icu.jnet.whatsjava.helper.Utils;

public class MediaEncryption {

/*
* Decrypt E2E media
*
*/

// Depending on the media type a different info parameter is used for the HKDF function
public static String MEDIA_TYPE_IMAGE = "WhatsApp Image Keys",
MEDIA_TYPE_VIDEO = "WhatsApp Video Keys",
MEDIA_TYPE_AUDIO = "WhatsApp Audio Keys",
MEDIA_TYPE_DOCUMENT = "WhatsApp Document Keys";


public static byte[] decrypt(byte[] mediaKey, String url, String mediaType) {
// Expand mediaKey to 112 bytes and add mediaInfo
byte[] mediaKeyExpanded = Utils.expandUsingHKDF(mediaKey, 112, mediaType.getBytes());

byte[] iv = Arrays.copyOfRange(mediaKeyExpanded, 0, 16);
byte[] cipherKey = Arrays.copyOfRange(mediaKeyExpanded, 16, 48);
byte[] macKey = Arrays.copyOfRange(mediaKeyExpanded, 48, 80);
// refKey mediaKeyExpanded[80:112] not used

// Download encrypted media
byte[] encryptedMedia = Utils.urlToEncMedia(url);

if(encryptedMedia != null) {
byte[] file = Arrays.copyOfRange(encryptedMedia, 0, encryptedMedia.length - 10);
byte[] mac = Arrays.copyOfRange(encryptedMedia, encryptedMedia.length - 10, encryptedMedia.length);

// Hmac sign message
byte[] message = ByteBuffer.allocate(iv.length + file.length).put(iv).put(file).array();

// Validate macKey of mediaKeyExpanded with mac key of the encrypted media
byte[] hmacSign = Utils.signHMAC(macKey, message);

// Media validated
if(Arrays.equals(mac, Arrays.copyOfRange(hmacSign, 0, 10))) {
return AES.decrypt(file, cipherKey, iv);
}
}

return null;
}
}
55 changes: 47 additions & 8 deletions src/main/java/icu/jnet/whatsjava/helper/Utils.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package icu.jnet.whatsjava.helper;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
Expand All @@ -9,9 +15,12 @@
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.io.FileUtils;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import at.favre.lib.crypto.HKDF;
import icu.jnet.whatsjava.constants.RequestType;
import icu.jnet.whatsjava.encryption.BinaryEncoder;
import icu.jnet.whatsjava.encryption.BinaryEncryption;
Expand All @@ -34,7 +43,7 @@ public static void waitMillis(long millis) {
}
}

/* Generate random byte array of specified length */
// Generate random byte array of specified length
public static byte[] randomBytes(int length) {
Random rand = new Random();
byte[] clientId = new byte[length];
Expand All @@ -43,26 +52,26 @@ public static byte[] randomBytes(int length) {
return clientId;
}

/* WhatsApp adds a tag to most of the json messages. That's why we need to remove it */
// WhatsApp adds a tag to most of the json messages. That's why we need to remove it
public static JsonObject encodeValidJson(String message, String splitStart) {
String rawSplittedMessage = message.replaceFirst(splitStart, "##").split("##")[1];
String rawMessage = rawSplittedMessage.substring(0, rawSplittedMessage.length() - 1);
return JsonParser.parseString(rawMessage).getAsJsonObject();
}

/* Default split char [,] */
// Default split char [,]
public static JsonObject encodeValidJson(String message) {
String raw = message.replaceFirst("[,]", "##").split("##")[1];
return JsonParser.parseString(raw).getAsJsonObject();
}

/* WhatsApp needs a message tag at the start of every Websocket request */
// WhatsApp needs a message tag at the start of every Websocket request
private static String getMessageTag() {
String messageTag = Instant.now().getEpochSecond() + ".--" + wsRequestCount++;
return messageTag;
}

/* WhatsApp binary message tags look different */
// WhatsApp binary message tags look different
private static String getBinaryMessageTag() {
if(binaryMessageTag.equals("")) {
binaryMessageTag = (new Random().nextInt(900) + 100) + "";
Expand All @@ -76,7 +85,7 @@ public static int getMessageCount() {
return wsRequestCount;
}

/* Create a new websocket json request string */
// Create a new websocket json request string
public static String buildWebsocketJsonRequest(int requestType, String... content) {
String messageTag = getMessageTag();

Expand Down Expand Up @@ -108,7 +117,7 @@ public static String buildWebsocketJsonRequest(int requestType, String... conten
return request;
}

/* Create a new websocket binary request array */
// Create a new websocket binary request array
public static byte[] buildWebsocketBinaryRequest(EncryptionKeyPair keyPair, String json, byte... waTags) {
String tag = null;

Expand All @@ -130,7 +139,13 @@ public static byte[] buildWebsocketBinaryRequest(EncryptionKeyPair keyPair, Stri
.put(messageTag).put(waTags).put(hmacSign).put(encrypted).array();
}

/* Implementation: https://github.com/danharper/hmac-examples */
// HLDF key expansion
public static byte[] expandUsingHKDF(byte[] key, int expandedLength, byte[] info) {
byte[] pseudoRandomKey = HKDF.fromHmacSha256().extract(null, key);
return HKDF.fromHmacSha256().expand(pseudoRandomKey, info, expandedLength);
}

// Implementation: https://github.com/danharper/hmac-examples
public static byte[] signHMAC(byte[] hmacValidationKey, byte[] hmacValidationMessage) {
try {
Mac hasher = Mac.getInstance("HmacSHA256");
Expand All @@ -143,4 +158,28 @@ public static byte[] signHMAC(byte[] hmacValidationKey, byte[] hmacValidationMes
}
return null;
}

// Download encrypted media files
public static byte[] urlToEncMedia(String url) {
try {
// Create random temporary file
Path path = Files.createTempFile(null, ".enc");
File tmpFile = path.toFile();

byte[] encryptedMedia = null;
// Download encrypted file
try {
FileUtils.copyURLToFile(new URL(url), tmpFile);

// Convert file to byte array
encryptedMedia = Files.readAllBytes(path);
} catch(FileNotFoundException ex) {}

tmpFile.delete();
return encryptedMedia;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
11 changes: 11 additions & 0 deletions src/main/java/icu/jnet/whatsjava/web/WebImageMessage.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package icu.jnet.whatsjava.web;

import icu.jnet.whatsjava.encryption.MediaEncryption;
import icu.jnet.whatsjava.encryption.proto.ProtoBuf.ImageMessage;
import icu.jnet.whatsjava.encryption.proto.ProtoBuf.WebMessageInfo;

public class WebImageMessage extends WebMessage {

/*
* E2E media image message
*
*/


private String mimetype, url, caption;
private byte[] fileSha256, mediaKey, jpegThumbnail;
private long fileLength;
Expand Down Expand Up @@ -51,6 +58,10 @@ public byte[] getJpegThumbnail() {
return jpegThumbnail;
}

public byte[] getJpegFullResolution() {
return MediaEncryption.decrypt(mediaKey, url, MediaEncryption.MEDIA_TYPE_IMAGE);
}

public long getFileLength() {
return fileLength;
}
Expand Down
12 changes: 11 additions & 1 deletion src/main/java/icu/jnet/whatsjava/web/WebVideoMessage.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package icu.jnet.whatsjava.web;

import icu.jnet.whatsjava.encryption.MediaEncryption;
import icu.jnet.whatsjava.encryption.proto.ProtoBuf.VideoMessage;
import icu.jnet.whatsjava.encryption.proto.ProtoBuf.WebMessageInfo;
import icu.jnet.whatsjava.encryption.proto.ProtoBuf.VideoMessage.VIDEO_MESSAGE_ATTRIBUTION;

public class WebVideoMessage extends WebMessage {

/*
* E2E media video message
*
*/

private String mimetype, url;
private byte[] fileSha256, mediaKey, jpegThumbnail;
private long fileLength;
Expand Down Expand Up @@ -46,10 +52,14 @@ public byte[] getMediaKey() {
return mediaKey;
}

public byte[] getJpegThumbnail() {
public byte[] getMp4Thumbnail() {
return jpegThumbnail;
}

public byte[] getMp4FullResolution() {
return MediaEncryption.decrypt(mediaKey, url, MediaEncryption.MEDIA_TYPE_VIDEO);
}

public long getFileLength() {
return fileLength;
}
Expand Down

0 comments on commit 45fd84d

Please sign in to comment.