/*
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;
}
}