ch.threema.apitool.CryptTool.java Source code

Java tutorial

Introduction

Here is the source code for ch.threema.apitool.CryptTool.java

Source

/*
 * $Id$
 *
 * The MIT License (MIT)
 * Copyright (c) 2015 Threema GmbH
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE
 */

package ch.threema.apitool;

import ch.threema.apitool.exceptions.BadMessageException;
import ch.threema.apitool.exceptions.DecryptionFailedException;
import ch.threema.apitool.exceptions.MessageParseException;
import ch.threema.apitool.exceptions.UnsupportedMessageTypeException;
import ch.threema.apitool.messages.*;
import ch.threema.apitool.results.EncryptResult;
import ch.threema.apitool.results.UploadResult;
import com.neilalexander.jnacl.NaCl;
import org.apache.commons.io.EndianUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;

/**
 * Contains static methods to do various Threema cryptography related tasks.
 */
public class CryptTool {

    /* HMAC-SHA256 keys for email/mobile phone hashing */
    private static final byte[] EMAIL_HMAC_KEY = new byte[] { (byte) 0x30, (byte) 0xa5, (byte) 0x50, (byte) 0x0f,
            (byte) 0xed, (byte) 0x97, (byte) 0x01, (byte) 0xfa, (byte) 0x6d, (byte) 0xef, (byte) 0xdb, (byte) 0x61,
            (byte) 0x08, (byte) 0x41, (byte) 0x90, (byte) 0x0f, (byte) 0xeb, (byte) 0xb8, (byte) 0xe4, (byte) 0x30,
            (byte) 0x88, (byte) 0x1f, (byte) 0x7a, (byte) 0xd8, (byte) 0x16, (byte) 0x82, (byte) 0x62, (byte) 0x64,
            (byte) 0xec, (byte) 0x09, (byte) 0xba, (byte) 0xd7 };
    private static final byte[] PHONENO_HMAC_KEY = new byte[] { (byte) 0x85, (byte) 0xad, (byte) 0xf8, (byte) 0x22,
            (byte) 0x69, (byte) 0x53, (byte) 0xf3, (byte) 0xd9, (byte) 0x6c, (byte) 0xfd, (byte) 0x5d, (byte) 0x09,
            (byte) 0xbf, (byte) 0x29, (byte) 0x55, (byte) 0x5e, (byte) 0xb9, (byte) 0x55, (byte) 0xfc, (byte) 0xd8,
            (byte) 0xaa, (byte) 0x5e, (byte) 0xc4, (byte) 0xf9, (byte) 0xfc, (byte) 0xd8, (byte) 0x69, (byte) 0xe2,
            (byte) 0x58, (byte) 0x37, (byte) 0x07, (byte) 0x23 };

    private static final byte[] FILE_NONCE = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 };
    private static final byte[] FILE_THUMBNAIL_NONCE = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02 };

    private static final SecureRandom random = new SecureRandom();

    /**
     * Encrypt a text message.
     *
     * @param text the text to be encrypted (max. 3500 bytes)
     * @param senderPrivateKey the private key of the sending ID
     * @param recipientPublicKey the public key of the receiving ID
     * @return encrypted result
     */
    public static EncryptResult encryptTextMessage(String text, byte[] senderPrivateKey,
            byte[] recipientPublicKey) {
        return encryptMessage(new TextMessage(text), senderPrivateKey, recipientPublicKey);
    }

    /**
     * Encrypt an image message.
     *
     * @param encryptResult result of the image encryption
     * @param uploadResult result of the upload
     * @param senderPrivateKey the private key of the sending ID
     * @param recipientPublicKey the public key of the receiving ID
     * @return encrypted result
     */
    public static EncryptResult encryptImageMessage(EncryptResult encryptResult, UploadResult uploadResult,
            byte[] senderPrivateKey, byte[] recipientPublicKey) {
        return encryptMessage(
                new ImageMessage(uploadResult.getBlobId(), encryptResult.getSize(), encryptResult.getNonce()),
                senderPrivateKey, recipientPublicKey);
    }

    /**
     * Encrypt a file message.
     *
     * @param encryptResult result of the file data encryption
     * @param uploadResult result of the upload
     * @param mimeType MIME type of the file
     * @param fileName File name
     * @param fileSize Size of the file, in bytes
     * @param uploadResultThumbnail result of thumbnail upload
     * @param senderPrivateKey Private key of sender
     * @param recipientPublicKey Public key of recipient
     * @return Result of the file message encryption (not the same as the file data encryption!)
     */
    public static EncryptResult encryptFileMessage(EncryptResult encryptResult, UploadResult uploadResult,
            String mimeType, String fileName, int fileSize, UploadResult uploadResultThumbnail,
            byte[] senderPrivateKey, byte[] recipientPublicKey) {
        return encryptMessage(
                new FileMessage(uploadResult.getBlobId(), encryptResult.getSecret(), mimeType, fileName, fileSize,
                        uploadResultThumbnail != null ? uploadResultThumbnail.getBlobId() : null),
                senderPrivateKey, recipientPublicKey);
    }

    private static EncryptResult encryptMessage(ThreemaMessage threemaMessage, byte[] privateKey,
            byte[] publicKey) {
        /* determine random amount of PKCS7 padding */
        int padbytes = random.nextInt(254) + 1;

        byte[] messageBytes;
        try {
            messageBytes = threemaMessage.getData();
        } catch (BadMessageException e) {
            return null;
        }

        /* prepend type byte (0x02) to message data */
        byte[] data = new byte[1 + messageBytes.length + padbytes];
        data[0] = (byte) threemaMessage.getTypeCode();

        System.arraycopy(messageBytes, 0, data, 1, messageBytes.length);

        /* append padding */
        for (int i = 0; i < padbytes; i++) {
            data[i + 1 + messageBytes.length] = (byte) padbytes;
        }

        return encrypt(data, privateKey, publicKey);
    }

    /**
     * Decrypt an NaCl box using the recipient's private key and the sender's public key.
     *
     * @param box The box to be decrypted
     * @param privateKey The private key of the recipient
     * @param publicKey The public key of the sender
     * @param nonce The nonce that was used for encryption
     * @return The decrypted data, or null if decryption failed
     */
    public static byte[] decrypt(byte[] box, byte[] privateKey, byte[] publicKey, byte[] nonce) {
        return new NaCl(privateKey, publicKey).decrypt(box, nonce);
    }

    /**
     * Decrypt symmetrically encrypted file data.
     *
     * @param fileData The encrypted file data
     * @param secret The symmetric key that was used for encryption
     * @return The decrypted file data, or null if decryption failed
     */
    public static byte[] decryptFileData(byte[] fileData, byte[] secret) {
        return NaCl.symmetricDecryptData(fileData, secret, FILE_NONCE);
    }

    /**
     * Decrypt symmetrically encrypted file thumbnail data.
     *
     * @param fileData The encrypted thumbnail data
     * @param secret The symmetric key that was used for encryption
     * @return The decrypted thumbnail data, or null if decryption failed
     */
    public static byte[] decryptFileThumbnailData(byte[] fileData, byte[] secret) {
        return NaCl.symmetricDecryptData(fileData, secret, FILE_THUMBNAIL_NONCE);
    }

    /**
     * Decrypt a message.
     *
     * @param box the box to be decrypted
     * @param recipientPrivateKey the private key of the receiving ID
     * @param senderPublicKey the public key of the sending ID
     * @param nonce the nonce that was used for the encryption
     * @return decrypted message (text or delivery receipt)
     */
    public static ThreemaMessage decryptMessage(byte[] box, byte[] recipientPrivateKey, byte[] senderPublicKey,
            byte[] nonce) throws MessageParseException {

        byte[] data = decrypt(box, recipientPrivateKey, senderPublicKey, nonce);
        if (data == null)
            throw new DecryptionFailedException();

        /* remove padding */
        int padbytes = data[data.length - 1] & 0xFF;
        int realDataLength = data.length - padbytes;
        if (realDataLength < 1)
            throw new BadMessageException(); /* Bad message padding */

        /* first byte of data is type */
        int type = data[0] & 0xFF;

        switch (type) {
        case TextMessage.TYPE_CODE:
            /* Text message */
            if (realDataLength < 2)
                throw new BadMessageException();

            try {
                return new TextMessage(new String(data, 1, realDataLength - 1, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                /* should never happen, UTF-8 is always supported */
                throw new RuntimeException(e);
            }

        case DeliveryReceipt.TYPE_CODE:
            /* Delivery receipt */
            if (realDataLength < MessageId.MESSAGE_ID_LEN + 2
                    || ((realDataLength - 2) % MessageId.MESSAGE_ID_LEN) != 0)
                throw new BadMessageException();

            DeliveryReceipt.Type receiptType = DeliveryReceipt.Type.get(data[1] & 0xFF);
            if (receiptType == null)
                throw new BadMessageException();

            List<MessageId> messageIds = new LinkedList<MessageId>();

            int numMsgIds = ((realDataLength - 2) / MessageId.MESSAGE_ID_LEN);
            for (int i = 0; i < numMsgIds; i++) {
                messageIds.add(new MessageId(data, 2 + i * MessageId.MESSAGE_ID_LEN));
            }

            return new DeliveryReceipt(receiptType, messageIds);

        case ImageMessage.TYPE_CODE:
            if (realDataLength != (1 + ThreemaMessage.BLOB_ID_LEN + 4 + NaCl.NONCEBYTES)) {
                System.out.println(String.valueOf(realDataLength));
                System.out.println(String.valueOf(1 + ThreemaMessage.BLOB_ID_LEN + 4 + NaCl.NONCEBYTES));
                throw new BadMessageException();
            }
            byte[] blobId = new byte[ThreemaMessage.BLOB_ID_LEN];
            System.arraycopy(data, 1, blobId, 0, ThreemaMessage.BLOB_ID_LEN);
            int size = EndianUtils.readSwappedInteger(data, 1 + ThreemaMessage.BLOB_ID_LEN);
            byte[] fileNonce = new byte[NaCl.NONCEBYTES];
            System.arraycopy(data, 1 + 4 + ThreemaMessage.BLOB_ID_LEN, fileNonce, 0, nonce.length);

            return new ImageMessage(blobId, size, fileNonce);

        case FileMessage.TYPE_CODE:
            try {
                return FileMessage.fromString(new String(data, 1, realDataLength - 1, "UTF-8"));
            } catch (UnsupportedEncodingException e) {
                throw new BadMessageException();
            }

        default:
            throw new UnsupportedMessageTypeException();
        }
    }

    /**
     * Generate a new key pair.
     *
     * @param privateKey is used to return the generated private key (length must be NaCl.PRIVATEKEYBYTES)
     * @param publicKey is used to return the generated public key (length must be NaCl.PUBLICKEYBYTES)
     */
    public static void generateKeyPair(byte[] privateKey, byte[] publicKey) {
        if (publicKey.length != NaCl.PUBLICKEYBYTES || privateKey.length != NaCl.SECRETKEYBYTES) {
            throw new IllegalArgumentException("Wrong key length");
        }

        NaCl.genkeypair(publicKey, privateKey);
    }

    /**
     * Encrypt data using NaCl asymmetric ("box") encryption.
     *
     * @param data the data to be encrypted
     * @param privateKey is used to return the generated private key (length must be NaCl.PRIVATEKEYBYTES)
     * @param publicKey is used to return the generated public key (length must be NaCl.PUBLICKEYBYTES)
     */
    public static EncryptResult encrypt(byte[] data, byte[] privateKey, byte[] publicKey) {
        if (publicKey.length != NaCl.PUBLICKEYBYTES || privateKey.length != NaCl.SECRETKEYBYTES) {
            throw new IllegalArgumentException("Wrong key length");
        }

        byte[] nonce = randomNonce();
        NaCl naCl = new NaCl(privateKey, publicKey);
        return new EncryptResult(naCl.encrypt(data, nonce), null, nonce);
    }

    /**
     * Encrypt file data using NaCl symmetric encryption with a random key.
     *
     * @param data the file contents to be encrypted
     * @return the encryption result including the random key
     */
    public static EncryptResult encryptFileData(byte[] data) {
        //create random key
        SecureRandom rnd = new SecureRandom();
        byte[] encryptionKey = new byte[NaCl.SYMMKEYBYTES];
        rnd.nextBytes(encryptionKey);

        //encrypt file data in-place
        NaCl.symmetricEncryptDataInplace(data, encryptionKey, FILE_NONCE);

        return new EncryptResult(data, encryptionKey, FILE_NONCE);
    }

    /**
     * Encrypt file thumbnail data using NaCl symmetric encryption with a random key.
     *
     * @param data the file contents to be encrypted
     * @return the encryption result including the random key
     */
    public static EncryptResult encryptFileThumbnailData(byte[] data, byte[] encryptionKey) {
        // encrypt file data in-place
        NaCl.symmetricEncryptDataInplace(data, encryptionKey, FILE_THUMBNAIL_NONCE);

        return new EncryptResult(data, encryptionKey, FILE_THUMBNAIL_NONCE);
    }

    /**
     * Hashes an email address for identity lookup.
     *
     * @param email the email address
     * @return the raw hash
     */
    public static byte[] hashEmail(String email) {
        try {
            Mac emailMac = Mac.getInstance("HmacSHA256");
            emailMac.init(new SecretKeySpec(EMAIL_HMAC_KEY, "HmacSHA256"));
            String normalizedEmail = email.toLowerCase().trim();
            return emailMac.doFinal(normalizedEmail.getBytes("US-ASCII"));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Hashes a phone number for identity lookup.
     *
     * @param phoneNo the phone number
     * @return the raw hash
     */
    public static byte[] hashPhoneNo(String phoneNo) {
        try {
            Mac phoneMac = Mac.getInstance("HmacSHA256");
            phoneMac.init(new SecretKeySpec(PHONENO_HMAC_KEY, "HmacSHA256"));
            String normalizedPhoneNo = phoneNo.replaceAll("[^0-9]", "");
            return phoneMac.doFinal(normalizedPhoneNo.getBytes("US-ASCII"));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Generate a random nonce.
     *
     * @return random nonce
     */
    public static byte[] randomNonce() {
        byte[] nonce = new byte[NaCl.NONCEBYTES];
        random.nextBytes(nonce);
        return nonce;
    }

    /**
     * Return the public key that corresponds with a given private key.
     *
     * @param privateKey The private key whose public key should be derived
     * @return The corresponding public key.
     */
    public static byte[] derivePublicKey(byte[] privateKey) {
        return NaCl.derivePublicKey(privateKey);
    }
}