Hi Facebook community, I'm currently working on WhatsApp flows that involve a DocumentPicker and PhotoPicker. I've implemented the decryption and validation of media based on the Facebook guidelines but I'm facing challenges with the media decryption process.
Could anyone share a wrapper code block in Java for encrypting and decrypting files selected via the DocumentPicker and PhotoPicker?
The below code is a sample response I'm getting in the API
documents = [
{
file_name=abcd.pdf,
media_id=1234,
cdn_url=https://mmg.whatsapp.net....,
encryption_metadata=
{
encryption_key=xxxxxx,
hmac_key=xxxxxx,
iv=xxxxxx,
plaintext_hash=xxxxxx,
encrypted_hash=xxxxxx
}
}
]
I did as per the above links 5 steps, Perform the following steps to decrypt the media: 1. Download cdn_file file from cdn_url 2. Make sure SHA256(cdn_file) == enc_hash 3. Validate HMAC-SHA256 3.1 Calculate HMAC with hmac_key, initialization vector (encryption_metadata.iv) and ciphertex 3.2 Make sure first 10 bytes == hmac10 4. Decrypt media content 4.1. Run AES with CBC mode and initialization vector (encryption_metadata.iv) on ciphertex 4.2. Remove padding (AES256 uses blocks of 16 bytes, padding algorithm is pkcs7). We’ll call this decrypted_media 5. Validate the decrypted media 5.1. Make sure SHA256(decrypted_media) = plaintext_hash
In the decryptMedia method, I'm getting an invalid encryption key size.
try {
String encryptionKey = documentList.get(0).getEncryptionMetadata().getEncryptionKey();
String iv = documentList.get(0).getEncryptionMetadata().getIv();
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<byte[]> response = restTemplate.getForEntity(documentList.get(0).getCdnUrl(), byte[].class);
byte[] cdnFile = response.getBody();
String hmacKey = documentList.get(0).getEncryptionMetadata().getHmacKey();
String encHash = documentList.get(0).getEncryptionMetadata().getEncryptedHash();
String plaintextHash = documentList.get(0).getEncryptionMetadata().getPlaintextHash();
// Validate SHA-256 of the CDN file
String computedEncHash = computeSha256(cdnFile);
if (!computedEncHash.equals(encHash)) {
throw new IllegalArgumentException("SHA256(cdn_file) does not match enc_hash.");
}
// Split CDN file into ciphertext and HMAC10
byte[] ciphertext = Arrays.copyOf(cdnFile, cdnFile.length - 10);
byte[] hmac10 = Arrays.copyOfRange(cdnFile, cdnFile.length - 10, cdnFile.length);
// Validate HMAC-SHA256
byte[] ivBytes = Base64.getDecoder().decode(iv);
String computedHmac = computeHmacSha256(hmacKey, ivBytes, ciphertext);
if (!validateHmac(Base64.getDecoder().decode(computedHmac), hmac10)) {
throw new IllegalArgumentException("HMAC validation failed.");
}
// Decrypt media content
byte[] decryptedMedia = decryptMedia(encryptionKey, iv, ciphertext);
// Remove PKCS7 padding
byte[] unpaddedMedia = removePadding(decryptedMedia);
// Validate decrypted media SHA-256 hash
String computedPlaintextHash = computeSha256(unpaddedMedia);
if (!computedPlaintextHash.equals(plaintextHash)) {
throw new IllegalArgumentException("SHA256(decrypted_media) does not match plaintext_hash.");
}
System.out.println("Decryption and validation successful!");
} catch (Exception e) {
LOGGER.error("Error in decryptedMedia ", e);
}
// Decryption code start
public static byte[] decryptMedia(String encryptionKey, String iv, byte[] ciphertext) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(encryptionKey);
byte[] ivBytes = Base64.getDecoder().decode(iv);
// Initialize cipher for AES CBC decryption
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
return cipher.doFinal(ciphertext);
}
public static String computeHmacSha256(String hmacKey, byte[] iv, byte[] ciphertext) throws Exception {
byte[] keyBytes = Base64.getDecoder().decode(hmacKey);
// Concatenate IV and ciphertext for HMAC calculation
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
outputStream.write(iv);
outputStream.write(ciphertext);
byte[] dataToHmac = outputStream.toByteArray();
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "HmacSHA256");
mac.init(secretKey);
return Base64.getEncoder().encodeToString(mac.doFinal(dataToHmac));
}
public static boolean validateHmac(byte[] computedHmac, byte[] hmac10) {
// Compare the first 10 bytes of the computed HMAC with the provided HMAC10
return Arrays.equals(Arrays.copyOf(computedHmac, 10), hmac10);
}
public static byte[] removePadding(byte[] decryptedMedia) {
int paddingLength = decryptedMedia[decryptedMedia.length - 1];
return Arrays.copyOfRange(decryptedMedia, 0, decryptedMedia.length - paddingLength);
}
public static String computeSha256(byte[] data) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(data);
return Base64.getEncoder().encodeToString(hash);
}
// Decryption code end
It seems that the error is occurring at the line-
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
Please let me know if its thrown at a different line.
The error message suggests that the encryption key length is incorrect. The PhotoPicker and DocumentPicker use the AES256-CBC+HMAC-SHA256+PKSC7 algorithm to encrypt media, which requires an encryption key length of 32 bytes for AES256.
To resolve this issue, please check the following:
1. Verify that the encryptionKey variable is correctly retrieving the encryption_key shared in the data exchange.
2. Verify that the length of the keyBytes array computed at line
byte[] keyBytes = Base64.getDecoder().decode(encryptionKey);
, is 32 bytes
If the above conditions are met, the error should be resolved. If not, please let us know for further assistance.
I think you may have specified the incorrect padding in the decryptMedia method. This line:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
should be:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
P.S. I was also struggling to implement WhatsApp flows that involve a DocumentPicker and I used your code as a guide (converted to C#), and it is working, so thank you very much!
PKCS7Padding: Java's standard JCA provider does not support this padding directly. However if you could share your C# code it will be helpful for us to analyse. Thanks for your response
I am using an external library called Bouncy Castle because I could not get the WhatsApp message decryption working using the native .Net System.Security.Cryptography, sorry I should have mentioned that.
I think they have a Java implementation Bouncy Castle for Java
Here is a code sample:
using Org.BouncyCastle.Crypto.Digests;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
public class Program
{
private static void Main()
{
try
{
string decrypted_Data = @"{""data"":{""media"":[{""file_name"":""test1.pdf"",""media_id"":""xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"",""cdn_url"":""https://mmg.whatsapp.net/v/xxx.xxxxx-xx/xxxxxxxx..."",""encryption_metadata"":{""encryption_key"":""xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="",""hmac_key"":""xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="",""iv"":""xxxxxxxxxxxxxxxxxxxxxx=="",""plaintext_hash"":""xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx="",""encrypted_hash"":""xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=""}}]},""flow_token"":""test1"",""screen"":""UPLOAD"",""action"":""data_exchange"",""version"":""3.0""}";
WhatsAppFlowRequestBodyModel whatsAppFlowRequestBody = JsonConvert.DeserializeObject<WhatsAppFlowRequestBodyModel>(decrypted_Data,
new JsonSerializerSettings() { NullValueHandling = NullValueHandling.Include });
WhatsAppFlowRequestDataMediaModel whatsAppFlowRequestDataMedia = whatsAppFlowRequestBody.Data.Media[0];
// 1. Download cdn_file file from cdn_url
byte[] mediaFileBytes = null;
HttpClient httpClient = new HttpClient();
HttpResponseMessage httpResponseMessage = httpClient.GetAsync(whatsAppFlowRequestDataMedia.CndUrl).Result;
mediaFileBytes = httpResponseMessage.Content.ReadAsByteArrayAsync().Result;
//2. Make sure SHA256(cdn_file) == enc_hash
Sha256Digest sha256Digest = new Sha256Digest();
sha256Digest.BlockUpdate(mediaFileBytes, 0, mediaFileBytes.Length);
byte[] computedHashBytes = new byte[sha256Digest.GetDigestSize()];
sha256Digest.DoFinal(computedHashBytes, 0);
string computedHash = Convert.ToBase64String(computedHashBytes);
int check1 = string.Compare(computedHash, whatsAppFlowRequestDataMedia.EncryptionMetadata.EncryptedHash);
//3. Validate HMAC-SHA256
// 1. Calculate HMAC with hmac_key, initialization vector(encryption_metadata.iv) and ciphertex
// 2. Make sure first 10 bytes == hmac10
byte[] hmac10Bytes = mediaFileBytes.Skip(mediaFileBytes.Length - 10).ToArray();
byte[] ciphertextBytes = mediaFileBytes.Take(mediaFileBytes.Length - 10).ToArray();
byte[] ivBytes = Convert.FromBase64String(whatsAppFlowRequestDataMedia.EncryptionMetadata.IV);
byte[] hmacKeyBytes = Convert.FromBase64String(whatsAppFlowRequestDataMedia.EncryptionMetadata.HmacKey);
byte[] dataToHmacBytes = new byte[ivBytes.Length + ciphertextBytes.Length];
Buffer.BlockCopy(ivBytes, 0, dataToHmacBytes, 0, ivBytes.Length);
Buffer.BlockCopy(ciphertextBytes, 0, dataToHmacBytes, ivBytes.Length, ciphertextBytes.Length);
HMac hmac = new HMac(new Sha256Digest());
hmac.Init(new KeyParameter(hmacKeyBytes));
byte[] computedHmacBytes = new byte[hmac.GetMacSize()];
hmac.BlockUpdate(dataToHmacBytes, 0, dataToHmacBytes.Length);
hmac.DoFinal(computedHmacBytes, 0);
bool check2 = hmac10Bytes.SequenceEqual(computedHmacBytes.Take(10));
// 4. Decrypt media content
// 1. Run AES with CBC mode and initialization vector (encryption_metadata.iv) on ciphertex
// 2. Remove padding (AES256 uses blocks of 16 bytes, padding algorithm is pkcs7). We'll call this decrypted_media
byte[] encryptionKeyBytes = Convert.FromBase64String(whatsAppFlowRequestDataMedia.EncryptionMetadata.EncryptionKey);
// *** using CipherUtilities.GetCipher
var cipher = CipherUtilities.GetCipher("AES/CBC/PKCS7PADDING");
cipher.Init(false, new ParametersWithIV(ParameterUtilities.CreateKeyParameter("AES", encryptionKeyBytes), ivBytes));
byte[] decrypted_DataBytes = cipher.DoFinal(ciphertextBytes);
// *** Or explicit cipher instantiation
//IBlockCipherPadding padding = new Pkcs7Padding();
//PaddedBufferedBlockCipher paddedBufferedBlockCipher = new PaddedBufferedBlockCipher(new CbcBlockCipher(new AesEngine()), padding);
//paddedBufferedBlockCipher.Init(false, new ParametersWithIV(ParameterUtilities.CreateKeyParameter("AES", encryptionKeyBytes), ivBytes));
//byte[] decrypted_DataBytes = paddedBufferedBlockCipher.DoFinal(ciphertextBytes);
// ***
// 5. Validate the decrypted media, Make sure SHA256(decrypted_media) = plaintext_hash
sha256Digest = new Sha256Digest();
sha256Digest.BlockUpdate(decrypted_DataBytes, 0, decrypted_DataBytes.Length);
computedHashBytes = new byte[sha256Digest.GetDigestSize()];
sha256Digest.DoFinal(computedHashBytes, 0);
computedHash = Convert.ToBase64String(computedHashBytes);
int check3 = string.Compare(computedHash, whatsAppFlowRequestDataMedia.EncryptionMetadata.PlaintextHash);
}
catch (Exception ex)
{
Console.WriteLine(@$"{ex.Message}");
}
}
}
public class WhatsAppFlowRequestBodyModel
{
[JsonProperty("screen", NullValueHandling = NullValueHandling.Include)]
public string Screen { get; set; }
[JsonProperty("data", NullValueHandling = NullValueHandling.Include)]
public WhatsAppFlowRequestDataModel Data { get; set; }
[JsonProperty("version", NullValueHandling = NullValueHandling.Include)]
public string Version { get; set; }
[JsonProperty("action", NullValueHandling = NullValueHandling.Include)]
public string Action { get; set; }
[JsonProperty("flow_token", NullValueHandling = NullValueHandling.Include)]
public string FlowToken { get; set; }
}
public class WhatsAppFlowRequestDataModel
{
[JsonProperty("error", NullValueHandling = NullValueHandling.Include)]
public string Error { get; set; }
[JsonProperty("requestid", NullValueHandling = NullValueHandling.Include)]
public Guid RequestId { get; set; }
[JsonProperty("media", NullValueHandling = NullValueHandling.Include)]
public WhatsAppFlowRequestDataMediaModel[] Media { get; set; }
}
public class WhatsAppFlowRequestDataMediaModel
{
[JsonProperty("file_name", NullValueHandling = NullValueHandling.Include)]
public string FileName { get; set; }
[JsonProperty("media_id", NullValueHandling = NullValueHandling.Include)]
public string MediaId { get; set; }
[JsonProperty("cdn_url", NullValueHandling = NullValueHandling.Include)]
public string CndUrl { get; set; }
[JsonProperty("encryption_metadata", NullValueHandling = NullValueHandling.Include)]
public WhatsAppFlowRequestDataMediaEncryptionMetadataModel EncryptionMetadata { get; set; }
}
public class WhatsAppFlowRequestDataMediaEncryptionMetadataModel
{
[JsonProperty("encryption_key", NullValueHandling = NullValueHandling.Include)]
public string EncryptionKey { get; set; }
[JsonProperty("hmac_key", NullValueHandling = NullValueHandling.Include)]
public string HmacKey { get; set; }
[JsonProperty("iv", NullValueHandling = NullValueHandling.Include)]
public string IV { get; set; }
[JsonProperty("plaintext_hash", NullValueHandling = NullValueHandling.Include)]
public string PlaintextHash { get; set; }
[JsonProperty("encrypted_hash", NullValueHandling = NullValueHandling.Include)]
public string EncryptedHash { get; set; }
}
@Anoud did you get this working?
Here is the python version which is written by claude and works perfectly fine:
import requests
import hashlib
import hmac
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import base64
def decrypt_media(cdn_url, encryption_metadata):
# Step 1: Download the file
response = requests.get(cdn_url)
cdn_file = response.content
# Step 2: Verify SHA256 hash
enc_hash = base64.b64decode(encryption_metadata['encrypted_hash'])
if hashlib.sha256(cdn_file).digest() != enc_hash:
raise ValueError("SHA256 hash verification failed")
# Step 3: Validate HMAC-SHA256
hmac_key = base64.b64decode(encryption_metadata['hmac_key'])
iv = base64.b64decode(encryption_metadata['iv'])
ciphertext = cdn_file[:-10] # Remove last 10 bytes (HMAC)
hmac10 = cdn_file[-10:]
h = hmac.new(hmac_key, iv + ciphertext, hashlib.sha256)
if h.digest()[:10] != hmac10:
raise ValueError("HMAC validation failed")
# Step 4: Decrypt media content
key = base64.b64decode(encryption_metadata['encryption_key'])
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_padded = cipher.decrypt(ciphertext)
decrypted_media = unpad(decrypted_padded, AES.block_size)
# Step 5: Validate decrypted media
plaintext_hash = base64.b64decode(encryption_metadata['plaintext_hash'])
if hashlib.sha256(decrypted_media).digest() != plaintext_hash:
raise ValueError("Decrypted media validation failed")
return decrypted_media
Working Nodejs to Decrypt and upload the file to s3
const crypto = require('crypto');
const axios = require('axios');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
// Initialize S3 client
let s3Client = new S3Client({
credentials:{
accessKeyId: "AWSACCESSKEY",
secretAccessKey: "AWSSECRETKEY"
},
region : "eu-central-1"
});
// Convert Buffer to base64 string
const bufferToBase64 = (buffer) => buffer.toString('base64');
// Convert base64 string to Buffer
const base64ToBuffer = (base64) => Buffer.from(base64, 'base64');
exports.decryptAndUploadMedia = async(file, bucketName, s3KeyPrefix = '') =>{
try {
const {
media_id,
cdn_url,
file_name,
encryption_metadata: {
encrypted_hash,
iv: ivBase64,
encryption_key: encKeyBase64,
hmac_key: hmacKeyBase64,
plaintext_hash
}
} = file;
// 1. Download file from CDN
console.log('Downloading file from CDN...');
const response = await axios.get(cdn_url, { responseType: 'arraybuffer' });
const cdnFile = Buffer.from(response.data);
// 2. Validate encrypted file hash
console.log('Validating encrypted file hash...');
const cdnFileHash = crypto
.createHash('sha256')
.update(cdnFile)
.digest('base64');
if (cdnFileHash !== encrypted_hash) {
throw new Error('Encrypted file hash mismatch');
}
// Split cdn_file into ciphertext and hmac10
const hmac10 = cdnFile.slice(-10);
const ciphertext = cdnFile.slice(0, -10);
// 3. Validate HMAC
console.log('Validating HMAC...');
const hmacKey = base64ToBuffer(hmacKeyBase64);
const iv = base64ToBuffer(ivBase64);
const hmac = crypto
.createHmac('sha256', hmacKey)
.update(iv)
.update(ciphertext)
.digest();
const calculatedHmac10 = hmac.slice(0, 10);
if (!calculatedHmac10.equals(hmac10)) {
throw new Error('HMAC validation failed');
}
// 4. Decrypt media content
console.log('Decrypting media content...');
const encKey = base64ToBuffer(encKeyBase64);
const decipher = crypto.createDecipheriv('aes-256-cbc', encKey, iv);
const decryptedMedia = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
// 5. Validate decrypted media
console.log('Validating decrypted media...');
const decryptedHash = crypto
.createHash('sha256')
.update(decryptedMedia)
.digest('base64');
if (decryptedHash !== plaintext_hash) {
throw new Error('Decrypted media hash mismatch');
}
// 6. Upload to S3
console.log('Uploading to S3...');
const s3Key = `${s3KeyPrefix}/${file_name}`;
const uploadParams = {
Bucket: bucketName,
Key: s3Key,
Body: decryptedMedia,
ContentType: getContentType(file_name),
Metadata: {
'original-filename': file_name,
'media-id': media_id
}
};
const uploadCommand = new PutObjectCommand(uploadParams);
await s3Client.send(uploadCommand);
console.log('Successfully decrypted and uploaded file to S3:', s3Key);
return {
success: true,
s3Key,
bucket: bucketName
};
} catch (error) {
console.error('Error processing media:', error.message);
throw error;
}
}
// Helper function to determine content type based on file extension
function getContentType(fileName) {
const extension = fileName.toLowerCase().split('.').pop();
const contentTypes = {
'gz': 'application/gzip',
'doc': 'application/msword',
'pdf': 'application/pdf',
'xls': 'application/vnd.ms-excel',
'ppt': 'application/vnd.ms-powerpoint',
'odp': 'application/vnd.oasis.opendocument.presentation',
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
'odt': 'application/vnd.oasis.opendocument.text',
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'7z': 'application/x-7z-compressed',
'zip': 'application/zip',
'avif': 'image/avif',
'gif': 'image/gif',
'heic': 'image/heic',
'heif': 'image/heif',
'jpg': 'image/jpeg',
'png': 'image/png',
'tiff': 'image/tiff',
'webp': 'image/webp',
'txt': 'text/plain',
'mp4': 'video/mp4',
'mpeg': 'video/mpeg'
};
return contentTypes[extension] || 'application/octet-stream';
}
Hi Anoud,
Currently, the sample code you requested isn't available in our public documentation, and it's undergoing internal review before we can share it publicly.
In the meantime, I'd be more than happy to assist you directly. If you could share the code you're working with and the specific issue you're encountering, I can provide guidance and help troubleshoot any problems.
Thanks!
Hi Sonika,
Thanks for your previous response! I'm checking in to see if there's been any update regarding the availability of the wrapper code for encrypting and decrypting files selected via the DocumentPicker and PhotoPicker. Is it now available in the public domain, or has the internal review been completed?
I'm Looking forward to any updates or documentation you might have on this.
Thanks again for your assistance!