Can anyone share the wrapper code block to decrypt media in flows?
1

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
   }
  }
 ]
Anoud
Вопрос задан около 4 мес. назад
Выбранный ответ
1

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
16 августа в 00:12
Anoud
Sonika

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.

19 августа в 05:02
1

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!

17 августа в 05:45
Clint
Anoud

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

17 августа в 08:48
Clint

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; }
    }
19 августа в 09:12
1

@Anoud did you get this working?

7 сентября в 01:51
Clint
1

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
1 октября в 06:08
Abdulhamit Kera
Aaditya

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';
}
22 октября в 13:49
2

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!

14 августа в 18:46
Sonika
Anoud

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!

22 октября в 03:50