grantedbyme.CryptoUtil.java Source code

Java tutorial

Introduction

Here is the source code for grantedbyme.CryptoUtil.java

Source

/*
 * =BEGIN MIT LICENSE
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 GrantedByMe
 *
 * 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.
 *
 * =END MIT LICENSE
 */
package grantedbyme;

import org.bouncycastle.openssl.PEMReader;
import org.bouncycastle.util.encoders.Base64;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemWriter;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.StringReader;
import java.io.StringWriter;
import java.security.*;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.Checksum;

/**
 * CryptoUtil class
 *
 * @author GrantedByMe <info@grantedby.me>
 */
public final class CryptoUtil {

    private static final String PROVIDER = "BC";

    /**
     * TBD
     *
     * @param source
     * @return
     * @throws Exception
     */
    public static KeyPair loadPrivate(String source) throws Exception {
        PEMReader pemReader = new PEMReader(new StringReader(source));
        return (KeyPair) pemReader.readObject();
    }

    /**
     * TBD
     *
     * @param source
     * @return
     * @throws Exception
     */
    public static PublicKey loadPublic(String source) throws Exception {
        PEMReader pemReader = new PEMReader(new StringReader(source));
        return (PublicKey) pemReader.readObject();
    }

    /**
     * TBD
     *
     * @param source
     * @return
     * @throws Exception
     */
    public static String savePublic(byte[] source) throws Exception {
        return saveKeyPair(source, "PUBLIC KEY");
    }

    /**
     * TBD
     *
     * @param source
     * @return
     * @throws Exception
     */
    public static String savePrivate(byte[] source) throws Exception {
        return saveKeyPair(source, "PRIVATE KEY");
    }

    /**
     * TBD
     *
     * @param source
     * @param type
     * @return
     * @throws Exception
     */
    public static String saveKeyPair(byte[] source, String type) throws Exception {
        final StringWriter stringWriter = new StringWriter();
        PemWriter pemWriter = new PemWriter(stringWriter);
        pemWriter.writeObject(new PemObject(type, source));
        pemWriter.flush();
        pemWriter.close();
        return stringWriter.toString();
    }

    /**
     * Generates a new RSA keypair with fixed 2048bit size
     *
     * @return
     * @throws Exception
     */
    public static KeyPair generateKeyPair() throws Exception {
        final KeyPairGenerator factory = KeyPairGenerator.getInstance("RSA", PROVIDER);
        factory.initialize(2048);
        return factory.generateKeyPair();
    }

    /**
     * TBD
     *
     * @param requestBody
     * @param serverPublicKey
     * @param privateKey
     * @return
     */
    public static JSONObject encryptAndSign(JSONObject requestBody, String serverPublicKey, String privateKey) {
        try {
            return CryptoUtil.encryptAndSign(requestBody, CryptoUtil.loadPublic(serverPublicKey),
                    CryptoUtil.loadPrivate(privateKey).getPrivate(), CryptoUtil.sha512(serverPublicKey));
        } catch (Exception e) {

        }
        return null;
    }

    /**
     * JSON object encryptor helper
     *
     * @param requestBody
     * @param serverPublicKey
     * @param privateKey
     * @param publicHash
     * @return
     */
    public static JSONObject encryptAndSign(JSONObject requestBody, PublicKey serverPublicKey,
            PrivateKey privateKey, String publicHash) {
        //if (BuildConfig.DEBUG) Log.d(TAG, "encryptAndSign: " + requestBody);
        // Convert JSON String to Bytes
        byte[] plainBytes = requestBody.toString().getBytes();
        // AES cipher
        byte[] cipherKey = randomBytes(16);
        byte[] cipherIv = randomBytes(16);
        byte[] cipherBytes;
        byte[] cipherSignature;
        String cipherText;
        JSONObject cipherJSON;
        // RSA payload
        byte[] payloadBytes;
        String payloadText;
        // RSA signature
        byte[] signatureBytes;
        String signatureText;
        // AES encrypt -> RSA encrypt -> RSA sign
        try {
            if (plainBytes.length < 215) {
                cipherText = null;
                final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", PROVIDER);
                cipher.init(Cipher.ENCRYPT_MODE, serverPublicKey);
                payloadBytes = cipher.doFinal(plainBytes);
                payloadText = new String(Base64.encode(payloadBytes), "UTF-8");
                // Generate RSA signature
                final Signature s = Signature.getInstance("SHA512WITHRSAANDMGF1", PROVIDER);
                //s.setParameter(new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 64, 1));
                s.initSign(privateKey);
                s.update(plainBytes);
                signatureBytes = s.sign();
                signatureText = new String(Base64.encode(signatureBytes), "UTF-8");
            } else {
                cipherBytes = crypt(plainBytes, cipherKey, cipherIv, Cipher.ENCRYPT_MODE);
                cipherText = new String(Base64.encode(cipherBytes), "UTF-8");
                // Sign using HMAC
                final Mac hmac = Mac.getInstance("HmacSHA256", PROVIDER);
                final SecretKeySpec keySpec = new SecretKeySpec(cipherKey, "HmacSHA256");
                hmac.init(keySpec);
                cipherSignature = hmac.doFinal(plainBytes);
                // Wrap AES cipher data into JSON
                Map<String, Object> cipherParams = new HashMap<>();
                cipherParams.put("cipher_key", new String(Base64.encode(cipherKey), "UTF-8"));
                cipherParams.put("cipher_iv", new String(Base64.encode(cipherIv), "UTF-8"));
                cipherParams.put("signature", new String(Base64.encode(cipherSignature), "UTF-8"));
                Long timestamp = System.currentTimeMillis() / 1000L;
                cipherParams.put("timestamp", timestamp);
                cipherJSON = new JSONObject(cipherParams);
                // System.out.println(cipherJSON.toJSONString());
                final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", PROVIDER);
                cipher.init(Cipher.ENCRYPT_MODE, serverPublicKey);
                payloadBytes = cipher.doFinal(cipherJSON.toString().getBytes());
                payloadText = new String(Base64.encode(payloadBytes), "UTF-8");
                // Generate RSA signature
                final Signature s = Signature.getInstance("SHA512WITHRSAANDMGF1", PROVIDER);
                //s.setParameter(new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 64, 1));
                s.initSign(privateKey);
                s.update(cipherJSON.toString().getBytes());
                signatureBytes = s.sign();
                signatureText = new String(Base64.encode(signatureBytes), "UTF-8");
            }
        } catch (Exception e) {
            throw new RuntimeException("encryptAndSign failed", e);
        }
        // collect
        Map<String, Object> params = new HashMap<>();
        params.put("payload", payloadText);
        params.put("signature", signatureText);
        if (cipherText != null) {
            params.put("message", cipherText);
        }
        params.put("public_hash", publicHash);
        // return
        return new JSONObject(params);
    }

    /**
     * TBD
     *
     * @param responseBody
     * @param serverPublicKey
     * @param privateKey
     * @return
     */
    public static JSONObject decryptAndVerify(JSONObject responseBody, String serverPublicKey, String privateKey) {
        try {
            return CryptoUtil.decryptAndVerify(responseBody, CryptoUtil.loadPublic(serverPublicKey),
                    CryptoUtil.loadPrivate(privateKey).getPrivate());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * JSON object decryptor helper
     *
     * @param responseBody
     * @param serverPublicKey
     * @param privateKey
     * @return
     */
    public static JSONObject decryptAndVerify(JSONObject responseBody, PublicKey serverPublicKey,
            PrivateKey privateKey) {
        if (responseBody == null || !responseBody.containsKey("payload")
                || !responseBody.containsKey("signature")) {
            throw new RuntimeException("decryptAndVerify failed with invalid message");
        }
        JSONObject result;
        byte[] payload;
        try {
            // decrypt
            payload = Base64.decode((String) responseBody.get("payload"));
            final Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", PROVIDER);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            payload = cipher.doFinal(payload);
            // verify
            byte[] signature = Base64.decode((String) responseBody.get("signature"));
            final Signature s = Signature.getInstance("SHA512WITHRSAANDMGF1", PROVIDER);
            //s.setParameter(new PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 64, 1));
            s.initVerify(serverPublicKey);
            s.update(payload);
            Boolean isValid = s.verify(signature);
            if (!isValid) {
                throw new RuntimeException("decryptAndVerify failed, signature error");
            }
            String payloadJson = new String(payload, "UTF-8"); // bytes to string
            result = (JSONObject) new JSONParser().parse(payloadJson); // string to object
            // If server sent payload and signature only, and decrypted payload does not contain secret keys
            // assume that the message is non-compound RSA encrypted and signed message.
            if (!result.containsKey("signature") && !result.containsKey("cipher_key")
                    && !result.containsKey("cipher_iv")) {
                if (!responseBody.containsKey("message") || responseBody.get("message") == null) {
                    return result;
                }
            }
            // Use AES encryption for messages longer than the available RSA key space
            byte[] message = Base64.decode((String) responseBody.get("message"));
            byte[] cipherKey = Base64.decode((String) result.get("cipher_key"));
            byte[] cipherIv = Base64.decode((String) result.get("cipher_iv"));
            byte[] cipherResult = crypt(message, cipherKey, cipherIv, Cipher.DECRYPT_MODE);
            String cipherJson = new String(cipherResult, "UTF-8");
            result = (JSONObject) new JSONParser().parse(cipherJson);
        } catch (Exception e) {
            throw new RuntimeException("decryptAndVerify failed", e);
        }
        //if (BuildConfig.DEBUG) Log.d(TAG, "decryptAndVerify: " + result);
        return result;
    }

    /**
     * AES crypto helper (two-way)
     *
     * @param source
     * @param key
     * @param iv
     * @param mode
     * @return
     * @throws Exception
     */
    public static byte[] crypt(byte[] source, byte[] key, byte[] iv, int mode) throws Exception {
        final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", PROVIDER);
        cipher.init(mode, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
        return cipher.doFinal(source);
    }

    /**
     * Generates a message digest using given algorithm
     *
     * @param source
     * @param salt
     * @param algorithm
     * @return
     * @throws Exception
     */
    public static byte[] hash(byte[] source, byte[] salt, String algorithm) throws Exception {
        if (algorithm == null) {
            algorithm = "SHA-512";
        }
        final MessageDigest digest = MessageDigest.getInstance(algorithm, PROVIDER);
        byte[] output = digest.digest(source);
        if (salt != null) {
            digest.update(salt);
            digest.update(output);
            //return new BigInteger(1, digest.digest());
            return digest.digest();
        }
        return output;
    }

    /**
     * Generate a SHA-512 digest from a string input
     *
     * @param data
     * @return
     */
    public static String sha512(String data) {
        data = data.replace("\r\n", "\n");
        data = data.replace("\r", "\n");
        try {
            return CryptoUtil.hexFromBytes(hash(data.getBytes(), null, "SHA-512"));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Generates a CRC-32 checksum from byte input
     *
     * @param bytes
     * @return
     * @throws Exception
     */
    public static String checksum(byte[] bytes) throws Exception {
        final Checksum checksumEngine = new CRC32();
        checksumEngine.update(bytes, 0, bytes.length);
        String hex = Long.toHexString(checksumEngine.getValue());
        return "00000000".substring(0, 8 - hex.length()) + hex.toUpperCase();
    }

    /**
     * Generates secure random bytes with given length
     *
     * @param length
     * @return
     */
    public static byte[] randomBytes(int length) {
        SecureRandom random = new SecureRandom();
        byte result[] = new byte[length];
        random.nextBytes(result);
        return result;
    }

    final private static char[] HEX_ARRAY = "0123456789abcdef".toCharArray();

    /**
     * Byte to Hex conversion helper
     *
     * @param bytes
     * @return
     */
    public static String hexFromBytes(byte[] bytes) {
        final char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }

}