net.java.otr4j.crypto.OtrCryptoEngine.java Source code

Java tutorial

Introduction

Here is the source code for net.java.otr4j.crypto.OtrCryptoEngine.java

Source

/*
 * otr4j, the open source java otr library.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 *
 * SPDX-License-Identifier: LGPL-3.0-only
 */

package net.java.otr4j.crypto;

import net.java.otr4j.io.OtrOutputStream;
import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.SICBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

import javax.annotation.Nonnull;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.DSAPublicKey;

import static net.java.otr4j.util.ByteArrays.allZeroBytes;
import static net.java.otr4j.util.ByteArrays.constantTimeEquals;
import static net.java.otr4j.util.ByteArrays.requireLengthExactly;
import static net.java.otr4j.util.ByteArrays.toHexString;

/**
 * Utility for cryptographic functions.
 *
 * @author George Politis
 * @author Danny van Heumen
 */
public final class OtrCryptoEngine {

    private static final String MD_SHA1 = "SHA-1";
    private static final String MD_SHA256 = "SHA-256";
    private static final String HMAC_SHA1 = "HmacSHA1";
    private static final String HMAC_SHA256 = "HmacSHA256";

    static {
        // Test initialization of all required cryptographic types that need to
        // be created through their respective factories. This test can function
        // as an early indicator in case support for required types is missing.
        try {
            Mac.getInstance(HMAC_SHA256);
            Mac.getInstance(HMAC_SHA1);
            MessageDigest.getInstance(MD_SHA256);
            MessageDigest.getInstance(MD_SHA1);
        } catch (final NoSuchAlgorithmException ex) {
            throw new IllegalStateException(
                    "Failed initialization test of required cryptographic types. otr4j will not function properly.",
                    ex);
        }
    }

    /**
     * The SHA-256 digest length in bytes.
     */
    public static final int SHA256_DIGEST_LENGTH_BYTES = 32;

    /**
     * Length of MAC in bytes.
     */
    private static final int MAC_LENGTH_BYTES = 20;

    /**
     * The AES key length in bytes.
     */
    public static final int AES_KEY_LENGTH_BYTES = 16;
    /**
     * The length of the counter value using for AES-128 encryption/decryption.
     */
    public static final int CTR_LENGTH_BYTES = 16;

    private OtrCryptoEngine() {
        // this class is never instantiated, it only has static methods
    }

    /**
     * SHA-256 HMAC calculation.
     *
     * @param b   The input bytes to calculate checksum of.
     * @param key The salt value for the HMAC calculation.
     * @return Returns the HMAC checksum.
     * @throws OtrCryptoException In case of illegal key value.
     */
    @Nonnull
    public static byte[] sha256Hmac(final byte[] b, final byte[] key) throws OtrCryptoException {
        return sha256Hmac(b, key, 0);
    }

    /**
     * SHA-256 HMAC calculation.
     *
     * @param b      The input bytes to calculate checksum of.
     * @param key    The salt value for the HMAC calculation.
     * @param length The length of the resulting checksum.
     * @return Returns the HMAC checksum.
     * @throws OtrCryptoException In case of illegal key value.
     */
    @Nonnull
    private static byte[] sha256Hmac(final byte[] b, final byte[] key, final int length) throws OtrCryptoException {
        assert !allZeroBytes(
                b) : "Expected non-zero bytes for b. This may indicate that a critical bug is present, or it may be a false warning.";
        assert !allZeroBytes(
                key) : "Expected non-zero bytes for key. This may indicate that a critical bug is present, or it may be a false warning.";
        final SecretKeySpec keyspec = new SecretKeySpec(key, HMAC_SHA256);
        final Mac mac;
        try {
            mac = Mac.getInstance(HMAC_SHA256);
            mac.init(keyspec);
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalStateException("Unable to initialize MAC based on SHA-256.", e);
        } catch (final InvalidKeyException e) {
            throw new OtrCryptoException("Invalid key, results in invalid keyspec.", e);
        }

        final byte[] macBytes = mac.doFinal(b);

        if (length > 0) {
            final byte[] bytes = new byte[length];
            final ByteBuffer buff = ByteBuffer.wrap(macBytes);
            buff.get(bytes);
            return bytes;
        } else {
            return macBytes;
        }
    }

    /**
     * The SHA-1 HMAC.
     *
     * @param b   the input bytes
     * @param key the salt
     * @return Returns the checksum.
     */
    @Nonnull
    public static byte[] sha1Hmac(final byte[] b, final byte[] key) {
        assert !allZeroBytes(
                b) : "Expected non-zero bytes for b. This may indicate that a critical bug is present, or it may be a false warning.";
        assert !allZeroBytes(
                key) : "Expected non-zero bytes for key. This may indicate that a critical bug is present, or it may be a false warning.";
        final byte[] macBytes;
        try {
            final Mac mac = Mac.getInstance(HMAC_SHA1);
            mac.init(new SecretKeySpec(key, HMAC_SHA1));
            macBytes = mac.doFinal(b);
        } catch (final NoSuchAlgorithmException ex) {
            throw new IllegalStateException("Unsupported HMAC function specified.", ex);
        } catch (final InvalidKeyException ex) {
            throw new IllegalStateException("Invalid key, results in invalid keyspec.", ex);
        }
        final byte[] bytes = new byte[MAC_LENGTH_BYTES];
        ByteBuffer.wrap(macBytes).get(bytes);
        return bytes;
    }

    /**
     * SHA-256 HMAC, take first 160 bits (20 bytes).
     *
     * @param b   the bytes
     * @param key the salt
     * @return Returns the checksum result.
     * @throws OtrCryptoException In case of illegal key value.
     */
    @Nonnull
    public static byte[] sha256Hmac160(final byte[] b, final byte[] key) throws OtrCryptoException {
        return sha256Hmac(b, key, MAC_LENGTH_BYTES);
    }

    /**
     * SHA-256 hash function.
     *
     * @param first the first bytes
     * @param next  any possible next byte-arrays of additional data
     * @return Returns the checksum result.
     */
    @Nonnull
    public static byte[] sha256Hash(final byte[] first, final byte[]... next) {
        final MessageDigest sha256;
        try {
            sha256 = MessageDigest.getInstance(MD_SHA256);
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalStateException("Failed to acquire SHA-256 message digest.", e);
        }
        sha256.update(first);
        for (final byte[] b : next) {
            sha256.update(b, 0, b.length);
        }
        return sha256.digest();
    }

    /**
     * SHA-1 hash function.
     *
     * @param first the first bytes
     * @param next  any possible next byte-arrays of additional data
     * @return Returns the checksum result.
     */
    @Nonnull
    public static byte[] sha1Hash(final byte[] first, final byte[]... next) {
        final MessageDigest sha1;
        try {
            sha1 = MessageDigest.getInstance(MD_SHA1);
        } catch (final NoSuchAlgorithmException e) {
            throw new IllegalStateException("Failed to acquire SHA1 message digest.", e);
        }
        sha1.update(first, 0, first.length);
        for (final byte[] b : next) {
            sha1.update(b, 0, b.length);
        }
        return sha1.digest();
    }

    /**
     * Decrypt AES-encrypted payload.
     *
     * @param key the decryption key
     * @param ctr the counter value used in encryption
     * @param b   the ciphertext
     * @return Returns the decrypted content.
     * @throws OtrCryptoException In case of illegal ciphertext.
     */
    @Nonnull
    public static byte[] aesDecrypt(final byte[] key, final byte[] ctr, final byte[] b) throws OtrCryptoException {
        requireLengthExactly(CTR_LENGTH_BYTES, ctr);
        assert !allZeroBytes(
                key) : "Expected non-zero bytes for key. This may indicate that a critical bug is present, or it may be a false warning.";
        assert !allZeroBytes(
                b) : "Expected non-zero bytes for b. This may indicate that a critical bug is present, or it may be a false warning.";
        final AESEngine aesDec = new AESEngine();
        final SICBlockCipher sicAesDec = new SICBlockCipher(aesDec);
        final BufferedBlockCipher bufSicAesDec = new BufferedBlockCipher(sicAesDec);
        bufSicAesDec.init(false, new ParametersWithIV(new KeyParameter(key), ctr));
        final byte[] aesOutLwDec = new byte[b.length];
        final int done = bufSicAesDec.processBytes(b, 0, b.length, aesOutLwDec, 0);
        try {
            bufSicAesDec.doFinal(aesOutLwDec, done);
        } catch (final InvalidCipherTextException ex) {
            throw new OtrCryptoException("Encrypted message contents is bad.", ex);
        }

        return aesOutLwDec;
    }

    /**
     * Encrypt payload using AES.
     *
     * @param key the encryption key
     * @param ctr the initial counter value to use
     * @param b   the plaintext content in bytes
     * @return Returns the encrypted content.
     */
    @Nonnull
    public static byte[] aesEncrypt(final byte[] key, final byte[] ctr, final byte[] b) {
        requireLengthExactly(CTR_LENGTH_BYTES, ctr);
        assert !allZeroBytes(
                key) : "Expected non-zero bytes for key. This may indicate that a critical bug is present, or it may be a false warning.";
        assert !allZeroBytes(
                b) : "Expected non-zero bytes for b. This may indicate that a critical bug is present, or it may be a false warning.";
        final AESEngine aesEnc = new AESEngine();
        final SICBlockCipher sicAesEnc = new SICBlockCipher(aesEnc);
        final BufferedBlockCipher bufSicAesEnc = new BufferedBlockCipher(sicAesEnc);
        bufSicAesEnc.init(true, new ParametersWithIV(new KeyParameter(key), ctr));
        final byte[] aesOutLwEnc = new byte[b.length];
        final int done = bufSicAesEnc.processBytes(b, 0, b.length, aesOutLwEnc, 0);
        try {
            bufSicAesEnc.doFinal(aesOutLwEnc, done);
        } catch (final InvalidCipherTextException ex) {
            throw new IllegalStateException("Failed to encrypt content.", ex);
        }
        return aesOutLwEnc;
    }

    /**
     * Get the fingerprint for provided DSA public key.
     *
     * @param pubKey the DSA public key
     * @return Returns fingerprint in hexadecimal string-representation
     */
    @Nonnull
    public static String getFingerprint(final DSAPublicKey pubKey) {
        final byte[] b = getFingerprintRaw(pubKey);
        return toHexString(b);
    }

    /**
     * Get the fingerprint for provided DSA public key as "raw" byte-array.
     *
     * @param pubKey the DSA public key
     * @return Returns the fingerprint as byte-array.
     */
    @Nonnull
    public static byte[] getFingerprintRaw(final DSAPublicKey pubKey) {
        final byte[] bRemotePubKey = new OtrOutputStream().writePublicKey(pubKey).toByteArray();
        final byte[] trimmed = new byte[bRemotePubKey.length - 2];
        System.arraycopy(bRemotePubKey, 2, trimmed, 0, trimmed.length);
        return sha1Hash(trimmed);
    }

    /**
     * Equality check for byte arrays that throws an exception in case of
     * failure. This version enables us to put equality checks in line with
     * other code and be sure that we immediately "exit" upon failing a check,
     * hence we cannot forget to handle the verification result.
     *
     * @param a byte[] a
     * @param b byte[] b
     * @param message The exception message in case of arrays are not equal.
     * @throws OtrCryptoException Throws exception in case of inequality.
     */
    public static void checkEquals(final byte[] a, final byte[] b, final String message) throws OtrCryptoException {
        assert !allZeroBytes(
                a) : "Expected non-zero bytes for a. This may indicate that a critical bug is present, or it may be a false warning.";
        assert !allZeroBytes(
                b) : "Expected non-zero bytes for b. This may indicate that a critical bug is present, or it may be a false warning.";
        if (!constantTimeEquals(a, b)) {
            throw new OtrCryptoException(message);
        }
    }
}