frost.crypt.FrostCrypt.java Source code

Java tutorial

Introduction

Here is the source code for frost.crypt.FrostCrypt.java

Source

/*
  FrostCrypt.java / Frost
  Copyright (C) 2003  Frost Project <jtcfrost.sourceforge.net>
    
  This program is free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License as
  published by the Free Software Foundation; either version 2 of
  the License, or (at your option) any later version.
    
  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  General Public License for more details.
    
  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package frost.crypt;

import java.io.*;
import java.math.*;
import java.nio.*;
import java.nio.channels.*;
import java.security.*;
import java.security.spec.*;
import java.util.*;

import javax.crypto.*;
import javax.crypto.spec.*;

import thaw.core.Logger;

import org.bouncycastle.crypto.*;
import org.bouncycastle.crypto.digests.*;
import org.bouncycastle.crypto.engines.*;
import org.bouncycastle.crypto.generators.*;
import org.bouncycastle.crypto.params.*;
import org.bouncycastle.crypto.signers.*;
import org.bouncycastle.jce.provider.*;
import org.bouncycastle.util.encoders.*;

/**
 * Implementation of the crypto layer.
 */
public final class FrostCrypt {

    private PSSSigner signer;
    private SecureRandom secureRandom = null;

    private KeyGenerator keyGeneratorAES = null;

    public FrostCrypt() {
        Security.addProvider(new BouncyCastleProvider());

        signer = new PSSSigner(new RSAEngine(), new SHA1Digest(), 16);
        try {
            secureRandom = SecureRandom.getInstance("SHA1PRNG");
        } catch (NoSuchAlgorithmException e) {
            secureRandom = new SecureRandom();
        }
    }

    /**
     * Generate a new RSA 1024 bit key pair.
     * @returns String[0] is private key; String[1] is public key
     */
    public synchronized String[] generateKeys() {

        RSAKeyPairGenerator keygen = new RSAKeyPairGenerator();
        keygen.init(new RSAKeyGenerationParameters(
                new BigInteger("3490529510847650949147849619903898133417764638493387843990820577"),
                getSecureRandom(), 1024, 80));
        //this big integer is the winner of some competition as far as I remember

        AsymmetricCipherKeyPair keys = keygen.generateKeyPair();

        //extract the keys
        RSAKeyParameters pubKey = (RSAKeyParameters) keys.getPublic();
        RSAPrivateCrtKeyParameters privKey = (RSAPrivateCrtKeyParameters) keys.getPrivate();

        //the return value
        String[] result = new String[2];
        StringBuffer temp = new StringBuffer();

        //create the keys
        temp.append(new String(Base64.encode(pubKey.getExponent().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(pubKey.getModulus().toByteArray())));
        result[1] = temp.toString(); // public key

        //rince and repeat, this time exactly the way its done in the constructor
        temp = new StringBuffer();
        temp.append(new String(Base64.encode(privKey.getModulus().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getPublicExponent().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getExponent().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getP().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getQ().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getDP().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getDQ().toByteArray())));
        temp.append(":");
        temp.append(new String(Base64.encode(privKey.getQInv().toByteArray())));
        result[0] = temp.toString(); // private key

        return result;
    }

    /**
     * Computes the SHA-1 checksum of given message.
     */
    public synchronized String digest(String message) {
        try {
            SHA1Digest stomach = new SHA1Digest();
            stomach.reset();
            byte[] food = message.getBytes("UTF-8");
            stomach.update(food, 0, food.length);
            byte[] poop = new byte[64];
            stomach.doFinal(poop, 0);
            return (new String(Base64.encode(poop))).substring(0, 27);
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 encoding is not supported : " + ex.toString());
        }
        return null;
    }

    /**
     * Computes the SHA-1 checksum of given file.
     */
    public synchronized String digest(File file) {
        SHA1Digest stomach = new SHA1Digest();
        byte[] poop = new byte[64];
        FileChannel chan = null;
        try {
            chan = (new FileInputStream(file)).getChannel();
        } catch (IOException e) {
            Logger.error(this, "Exception thrown in digest(File file): " + e.toString());
        }
        byte[] temp = new byte[4 * 1024];
        ByteBuffer _temp = ByteBuffer.wrap(temp);
        try {
            while (true) {
                //if (y >= file.length()) break;
                //if (y > file.length()) y = file.length();
                int pos = _temp.position();
                int read = chan.read(_temp);
                if (read == -1)
                    break;
                stomach.update(temp, pos, read);
                if (_temp.remaining() == 0)
                    _temp.position(0);
            }
            chan.close();
        } catch (IOException e) {
            Logger.error(this, "Exception thrown in digest(File file): " + e.toString());
        }
        stomach.doFinal(poop, 0);
        return (new String(Base64.encode(poop))).substring(0, 27);
    }

    public synchronized String encrypt(String what, String publicKey) {
        try {
            byte[] whatBytes = what.getBytes("UTF-8");
            byte[] encryptedBytes = encrypt(whatBytes, publicKey);
            String result = new String(Base64.encode(encryptedBytes), "ISO-8859-1");
            return result;
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 encoding is not supported: " + ex.toString());
        }
        return null;
    }

    /**
     * Encryption of a byte array.
     *
     * We generate a new 128bit AES key and encrypt the content with this key.
     * Then we RSA encrypt the key with publicKey. RSA ecpects 117 bytes of input
     * and generates 128byte of output. So we prepare a byte array of length 117 with
     * random data and copy the AES key into the front of it. Then we RSA encrypt this
     * array and put the result array of 128bytes length into the front of the AES encrypted
     * data.
     *
     *  @returns null if anything failed.
     */
    public synchronized byte[] encrypt(byte[] what, String publicKey) {

        byte[] aesKeyBytes = null;
        Cipher cipherAES = null;
        Cipher cipherRSA = null;

        int cipherRSAinSize = 0;
        int cipherRSAoutSize = 0;

        // prepare AES, we need cipherAES and keyBytes
        aesKeyBytes = generateAESSessionKey(); // 16 bytes
        if (aesKeyBytes == null) {
            return null;
        }
        cipherAES = buildCipherAES(Cipher.ENCRYPT_MODE, aesKeyBytes);
        ;
        if (cipherAES == null) {
            return null;
        }

        // prepare RSA, we only need chiperRSA
        try {
            StringTokenizer keycutter = new StringTokenizer(publicKey, ":");
            BigInteger Exponent = new BigInteger(Base64.decode(keycutter.nextToken()));
            BigInteger Modulus = new BigInteger(Base64.decode(keycutter.nextToken()));
            RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(Modulus, Exponent);

            KeyFactory fact = KeyFactory.getInstance("RSA", "BC");
            PublicKey pubKey = fact.generatePublic(pubKeySpec);
            cipherRSA = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
            cipherRSA.init(Cipher.ENCRYPT_MODE, pubKey);
            cipherRSAinSize = cipherRSA.getBlockSize();
            cipherRSAoutSize = cipherRSA.getOutputSize(cipherRSAinSize);
            if (cipherRSAinSize != 117 || cipherRSAoutSize != 128) {
                throw new Exception(
                        "block size invalid, inSize=" + cipherRSAinSize + "; outSize=" + cipherRSAoutSize);
            }
        } catch (Throwable t) {
            Logger.error(this, "Error in encrypt, RSA preparation : " + t.toString());
            return null;
        }

        // encrypt keybytes with RSA
        byte rsaEncData[] = null;
        try {
            byte[] rsaInpData = new byte[cipherRSAinSize]; // input for RSA encryption
            // build 128 byte, first 16 byte the AES session key, remaining bytes are random data
            byte[] randomBytes = new byte[cipherRSAinSize - aesKeyBytes.length];
            getSecureRandom().nextBytes(randomBytes);

            System.arraycopy(aesKeyBytes, 0, rsaInpData, 0, aesKeyBytes.length);
            System.arraycopy(randomBytes, 0, rsaInpData, aesKeyBytes.length, randomBytes.length);

            rsaEncData = cipherRSA.doFinal(rsaInpData, 0, rsaInpData.length);
            if (rsaEncData.length != cipherRSAoutSize) {
                throw new Exception("RSA out block size invalid: " + rsaEncData.length);
            }
        } catch (Throwable t) {
            Logger.error(this, "Error in encrypt, RSA encryption:" + t.toString());
            return null;
        }

        // encrypt content using AES

        ByteArrayOutputStream plainOut = new ByteArrayOutputStream(
                what.length + (what.length / 10) + rsaEncData.length);
        try {
            // write RSA encrypted data
            plainOut.write(rsaEncData);

            // encrypt
            CipherOutputStream cOut = new CipherOutputStream(plainOut, cipherAES);
            cOut.write(what);

            cOut.close();
        } catch (IOException e) {
            Logger.error(this, "Error in encrypt, AES encryption: " + e.toString());
            return null;
        }
        return plainOut.toByteArray();
    }

    public synchronized String decrypt(String what, String privateKey) {
        try {
            byte[] encBytes = Base64.decode(what.getBytes("ISO-8859-1"));
            byte[] decBytes = decrypt(encBytes, privateKey);
            return new String(decBytes, "UTF-8");
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 encoding is not supported : " + ex.toString());
        }
        return null;
    }

    /**
     * Decrypts a byte array.
     *
     * The first 128 byte in array must be the RSA encrypted AES key,
     * remaining data is the AES data. See encrypt().
     */
    public synchronized byte[] decrypt(byte[] what, String privateKey) {

        Cipher cipherAES = null;
        Cipher cipherRSA = null;

        int cipherRSAinSize = 0;
        int cipherRSAoutSize = 0;

        // prepare RSA, we need chiperRSA
        try {
            StringTokenizer keycutter = new StringTokenizer(privateKey, ":");
            RSAPrivateCrtKeySpec privKeySpec = new RSAPrivateCrtKeySpec(
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())),
                    new BigInteger(Base64.decode(keycutter.nextToken())));

            KeyFactory fact = KeyFactory.getInstance("RSA", "BC");
            PrivateKey privKey = fact.generatePrivate(privKeySpec);
            cipherRSA = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
            cipherRSA.init(Cipher.DECRYPT_MODE, privKey);
            cipherRSAinSize = cipherRSA.getBlockSize();
            cipherRSAoutSize = cipherRSA.getOutputSize(cipherRSAinSize);
            if (cipherRSAinSize != 128 || cipherRSAoutSize != 117) {
                throw new Exception("RSA decryption block size invalid, inSize=" + cipherRSAinSize + "; outSize="
                        + cipherRSAoutSize);
            }
        } catch (Throwable e) {
            Logger.error(this, "Error in decrypt, RSA preparation: " + e.toString());
            return null;
        }

        // decrypt rsa and get aes key
        byte[] aesKeyBytes = null;
        try {
            byte[] sessionKeyBytes = cipherRSA.doFinal(what, 0, cipherRSAinSize);
            if (sessionKeyBytes == null) {
                throw new Exception("RSA decryption failed, sessionKeyBytes = null");
            }
            if (sessionKeyBytes.length != cipherRSAoutSize) {
                throw new Exception("RSA decryption failed, sessionKeyBytes.length = " + sessionKeyBytes.length
                        + ", must be " + cipherRSAoutSize);
            }
            // we need the first 16 byte
            aesKeyBytes = new byte[16];
            System.arraycopy(sessionKeyBytes, 0, aesKeyBytes, 0, aesKeyBytes.length);
        } catch (Throwable e) {
            Logger.debug(this, "Error in decrypt, RSA decryption: " + e.toString());
            return null;
        }

        // prepare AES, we need cipherAES
        cipherAES = buildCipherAES(Cipher.DECRYPT_MODE, aesKeyBytes);
        ;
        if (cipherAES == null) {
            return null;
        }

        // decrypt aes
        ByteArrayOutputStream plainOut = new ByteArrayOutputStream(what.length - cipherRSAinSize);

        ByteArrayInputStream bIn = new ByteArrayInputStream(what, cipherRSAinSize, what.length - cipherRSAinSize);
        CipherInputStream cIn = new CipherInputStream(bIn, cipherAES);

        try {
            byte[] buf = new byte[1024];
            while (true) {
                int bLen = cIn.read(buf);
                if (bLen < 0) {
                    break; // eof
                }
                plainOut.write(buf, 0, bLen);
            }
            cIn.close();
        } catch (Throwable e) {
            Logger.error(this, "Error in decrypt, AES decryption: " + e.toString());
            return null;
        }
        return plainOut.toByteArray();
    }

    public synchronized String detachedSign(String message, String key) {
        try {
            byte[] msgBytes = message.getBytes("UTF-8");
            return detachedSign(msgBytes, key);
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 encoding is not supported: " + ex.toString());
        }
        return null;
    }

    public synchronized String detachedSign(byte[] message, String key) {

        StringTokenizer keycutter = new StringTokenizer(key, ":");
        RSAPrivateCrtKeyParameters privKey = new RSAPrivateCrtKeyParameters(
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())),
                new BigInteger(Base64.decode(keycutter.nextToken())));

        signer.init(true, privKey);
        signer.update(message, 0, message.length);

        byte[] signature = null;
        try {
            signature = signer.generateSignature();
        } catch (CryptoException e) {
            Logger.error(this, "Exception thrown in detachedSign(String message, String key): " + e.toString());
        }
        signer.reset();
        try {
            String result = new String(Base64.encode(signature), "ISO-8859-1");
            return result;
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "ISO-8859-1 encoding is not supported: " + ex.toString());
        }
        return null;
    }

    public synchronized boolean detachedVerify(String message, String key, String sig) {
        try {
            byte[] msgBytes = message.getBytes("UTF-8");
            return detachedVerify(msgBytes, key, sig);
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 encoding is not supported: " + ex.toString());
        }
        return false;
    }

    public synchronized boolean detachedVerify(byte[] message, String key, String _sig) {
        try {
            byte[] sig = Base64.decode(_sig.getBytes("ISO-8859-1"));

            StringTokenizer keycutter = new StringTokenizer(key, ":");
            BigInteger Exponent = new BigInteger(Base64.decode(keycutter.nextToken()));
            BigInteger Modulus = new BigInteger(Base64.decode(keycutter.nextToken()));

            signer.init(false, new RSAKeyParameters(true, Modulus, Exponent));

            signer.update(message, 0, message.length);
            boolean result = signer.verifySignature(sig);
            signer.reset();
            return result;
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "ISO-8859-1 encoding is not supported: " + ex.toString());
        }
        return false;
    }

    public synchronized SecureRandom getSecureRandom() {
        return secureRandom;
    }

    public String decode64(String what) {
        try {
            byte[] whatBytes = what.getBytes("ISO-8859-1");
            return new String(Base64.decode(whatBytes), "UTF-8");
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 or ISO-8859-1 encoding is not supported: " + ex.toString());
        }
        return null;
    }

    public String encode64(String what) {
        try {
            byte[] whatBytes = what.getBytes("UTF-8");
            return new String(Base64.encode(whatBytes), "ISO-8859-1");
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 or ISO-8859-1 encoding is not supported: " + ex.toString());
        }
        return null;
    }

    /**
     * Called by encrypt() to generate a new random session key for AES.
     * Must be called synchronized (we use a global object)!
     * Currently only called by synchronized encrypt().
     *
     * @return the new session key or null.
     */
    private byte[] generateAESSessionKey() {
        if (keyGeneratorAES == null) {
            try {
                keyGeneratorAES = KeyGenerator.getInstance("AES");
            } catch (NoSuchAlgorithmException e) {
                Logger.error(this, "Could not get a KeyGenerator for AES: " + e.toString());
                return null;
            }
            keyGeneratorAES.init(128); // 192 and 256 bits may not be available!
        }
        SecretKey skey = keyGeneratorAES.generateKey();
        byte[] keyBytes = skey.getEncoded(); // 16 bytes
        return keyBytes;
    }

    /**
     * Builds and returns a new Cipher for AES.
     */
    private Cipher buildCipherAES(int mode, byte[] aesKey) {
        Cipher cipherAES = null;
        try {
            if (aesKey == null) {
                return null;
            }
            SecretKeySpec sessionKey = new SecretKeySpec(aesKey, "AES");
            cipherAES = Cipher.getInstance("AES", "BC");
            cipherAES.init(mode, sessionKey);

        } catch (Throwable t) {
            Logger.error(this, "Error in AES preparation: " + t.toString());
            return null;
        }
        return cipherAES;
    }

    /**
     * Computes the SHA256 checksum of utf-8 string.
     */
    public String computeChecksumSHA256(String message) {
        try {
            byte[] food = message.getBytes("UTF-8");
            return computeChecksumSHA256(food);
        } catch (UnsupportedEncodingException ex) {
            Logger.error(this, "UTF-8 encoding is not supported: " + ex.toString());
        }
        return null;
    }

    /**
     * Computes the SHA256 checksum of bytes.
     */
    public String computeChecksumSHA256(byte[] message) {
        try {
            byte[] food = message;

            MessageDigest sha256 = MessageDigest.getInstance("SHA256", "BC");
            sha256.update(food);
            byte[] poop = sha256.digest();

            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < poop.length; i++) {
                sb.append(Integer.toString((poop[i] & 0xff) + 0x100, 16).substring(1));
            }
            return sb.toString().toUpperCase();
        } catch (NoSuchAlgorithmException ex) {
            Logger.error(this, "Algorithm SHA256 not supported: " + ex.toString());
        } catch (NoSuchProviderException ex) {
            Logger.error(this, "Provider BC not supported: " + ex.toString());
        }
        return null;
    }

    /**
     * Computes the SHA256 checksum of a file.
     */
    public String computeChecksumSHA256(File file) {
        try {
            FileChannel chan = null;
            try {
                chan = (new FileInputStream(file)).getChannel();
            } catch (Throwable e) {
                Logger.error(this, "Exception thrown 1: " + e.toString());
                return null;
            }

            MessageDigest sha256 = MessageDigest.getInstance("SHA256", "BC");

            byte[] temp = new byte[4 * 1024];
            ByteBuffer _temp = ByteBuffer.wrap(temp);
            try {
                while (true) {
                    //if (y >= file.length()) break;
                    //if (y > file.length()) y = file.length();
                    int pos = _temp.position();
                    int read = chan.read(_temp);
                    if (read == -1)
                        break;
                    sha256.update(temp, pos, read);
                    if (_temp.remaining() == 0)
                        _temp.position(0);
                }
                chan.close();
            } catch (Throwable e) {
                Logger.error(this, "Exception thrown 2 : " + e.toString());
            }

            byte[] poop = sha256.digest();

            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < poop.length; i++) {
                sb.append(Integer.toString((poop[i] & 0xff) + 0x100, 16).substring(1));
            }
            return sb.toString().toUpperCase();

        } catch (NoSuchAlgorithmException ex) {
            Logger.error(this, "Algorithm SHA256 not supported: " + ex.toString());
        } catch (NoSuchProviderException ex) {
            Logger.error(this, "Provider BC not supported: " + ex.toString());
        }
        return null;
    }
}