package net.i2p.crypto;
/*
* free (adj.): unencumbered; not under the control of others
* Written by jrandom in 2003 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*
*/
import java.security.InvalidKeyException;
// for using system version
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import freenet.support.CPUInformation.CPUID;
import freenet.support.CPUInformation.CPUInfo;
import freenet.support.CPUInformation.UnknownCPUException;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.data.SessionKey;
import net.i2p.util.Log;
import net.i2p.util.SimpleByteCache;
import net.i2p.util.SystemVersion;
/**
* Wrapper for AES cypher operation using Cryptix's Rijndael implementation. Implements
* CBC with a 16 byte IV.
* Problems:
* Only supports data of size mod 16 bytes - no inherent padding.
*
* @author jrandom, thecrypto
*/
public final class CryptixAESEngine extends AESEngine {
private final static CryptixRijndael_Algorithm _algo = new CryptixRijndael_Algorithm();
// keys are now cached in the SessionKey objects
//private CryptixAESKeyCache _cache;
/** see test results below */
private static final int MIN_SYSTEM_AES_LENGTH = 704;
private static final boolean USE_SYSTEM_AES = hasAESNI() && CryptoCheck.isUnlimited();
/**
* Do we have AES-NI support in the processor and JVM?
* Only on 64-bit x86 Java 7 fast JVMs, with AES-NI support.
* See comments in main() below.
* @since 0.9.14
*/
private static boolean hasAESNI() {
if (SystemVersion.isX86() && SystemVersion.is64Bit() && SystemVersion.isJava7() &&
!SystemVersion.isApache() && !SystemVersion.isGNU()) {
try {
return CPUID.getInfo().hasAES();
} catch (UnknownCPUException e) {
return false;
}
} else {
return false;
}
}
/** */
public CryptixAESEngine(I2PAppContext context) {
super(context);
//_cache = new CryptixAESKeyCache();
}
/**
* @param iv must be 16 bytes
* @param length must be a multiple of 16
*/
@Override
public void encrypt(byte payload[], int payloadIndex, byte out[], int outIndex, SessionKey sessionKey, byte iv[], int length) {
encrypt(payload, payloadIndex, out, outIndex, sessionKey, iv, 0, length);
}
/**
* @param iv must be 16 bytes
* @param length must be a multiple of 16
*/
@Override
public void encrypt(byte payload[], int payloadIndex, byte out[], int outIndex, SessionKey sessionKey, byte iv[], int ivOffset, int length) {
if (payload == null)
throw new NullPointerException("invalid args to aes - payload");
if (out == null)
throw new NullPointerException("invalid args to aes - out");
if (sessionKey == null)
throw new NullPointerException("invalid args to aes - sessionKey");
if (iv == null)
throw new NullPointerException("invalid args to aes - iv");
if (payload.length < payloadIndex + length)
throw new IllegalArgumentException("Payload is too short");
if (out.length < outIndex + length)
throw new IllegalArgumentException("Output is too short");
if (length <= 0)
throw new IllegalArgumentException("Length is too small");
if (length % 16 != 0)
throw new IllegalArgumentException("Only lengths mod 16 are supported here");
if (USE_SYSTEM_AES && length >= MIN_SYSTEM_AES_LENGTH) {
try {
SecretKeySpec key = new SecretKeySpec(sessionKey.getData(), "AES");
IvParameterSpec ivps = new IvParameterSpec(iv, ivOffset, 16);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key, ivps, _context.random());
cipher.doFinal(payload, payloadIndex, length, out, outIndex);
return;
} catch (GeneralSecurityException gse) {
if (_log.shouldLog(Log.WARN))
_log.warn("Java encrypt fail", gse);
}
}
int numblock = length / 16;
DataHelper.xor(iv, ivOffset, payload, payloadIndex, out, outIndex, 16);
encryptBlock(out, outIndex, sessionKey, out, outIndex);
for (int x = 1; x < numblock; x++) {
DataHelper.xor(out, outIndex + (x-1) * 16, payload, payloadIndex + x * 16, out, outIndex + x * 16, 16);
encryptBlock(out, outIndex + x * 16, sessionKey, out, outIndex + x * 16);
}
}
/**
* @param iv 16 bytes
* @param length must be a multiple of 16 (will overrun to next mod 16 if not)
*/
@Override
public void decrypt(byte payload[], int payloadIndex, byte out[], int outIndex, SessionKey sessionKey, byte iv[], int length) {
decrypt(payload, payloadIndex, out, outIndex, sessionKey, iv, 0, length);
}
/**
* @param iv 16 bytes starting at ivOffset
* @param length must be a multiple of 16 (will overrun to next mod 16 if not)
*/
@Override
public void decrypt(byte payload[], int payloadIndex, byte out[], int outIndex, SessionKey sessionKey, byte iv[], int ivOffset, int length) {
if ((iv== null) || (payload == null) || (payload.length <= 0) || (sessionKey == null) )
throw new IllegalArgumentException("bad setup");
else if (out == null)
throw new IllegalArgumentException("out is null");
else if (out.length - outIndex < length)
throw new IllegalArgumentException("out is too small (out.length=" + out.length
+ " outIndex=" + outIndex + " length=" + length);
if (USE_SYSTEM_AES && length >= MIN_SYSTEM_AES_LENGTH) {
try {
SecretKeySpec key = new SecretKeySpec(sessionKey.getData(), "AES");
IvParameterSpec ivps = new IvParameterSpec(iv, ivOffset, 16);
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, key, ivps, _context.random());
cipher.doFinal(payload, payloadIndex, length, out, outIndex);
return;
} catch (GeneralSecurityException gse) {
if (_log.shouldLog(Log.WARN))
_log.warn("Java decrypt fail", gse);
}
}
int numblock = length / 16;
if (length % 16 != 0) {
// may not work, it will overrun payload length and could AIOOBE
numblock++;
if (_log.shouldLog(Log.WARN))
_log.warn("not %16 " + length, new Exception());
}
byte prev[] = SimpleByteCache.acquire(16);
byte cur[] = SimpleByteCache.acquire(16);
System.arraycopy(iv, ivOffset, prev, 0, 16);
for (int x = 0; x < numblock; x++) {
System.arraycopy(payload, payloadIndex, cur, 0, 16);
decryptBlock(payload, payloadIndex, sessionKey, out, outIndex);
payloadIndex += 16;
//DataHelper.xor(out, outIndex + x * 16, prev, 0, out, outIndex + x * 16, 16);
for (int i = 0; i < 16; i++) {
out[outIndex++] ^= prev[i];
}
iv = prev; // just use IV to switch 'em around
prev = cur;
cur = iv;
}
/*
decryptBlock(payload, payloadIndex, sessionKey, out, outIndex);
DataHelper.xor(out, outIndex, iv, 0, out, outIndex, 16);
for (int x = 1; x < numblock; x++) {
decryptBlock(payload, payloadIndex + (x * 16), sessionKey, out, outIndex + (x * 16));
DataHelper.xor(out, outIndex + x * 16, payload, payloadIndex + (x - 1) * 16, out, outIndex + x * 16, 16);
}
*/
SimpleByteCache.release(prev);
SimpleByteCache.release(cur);
}
/** encrypt exactly 16 bytes using the session key
* @param payload plaintext data, 16 bytes starting at inIndex
* @param sessionKey private session key
* @param out out parameter, 16 bytes starting at outIndex
*/
@Override
public final void encryptBlock(byte payload[], int inIndex, SessionKey sessionKey, byte out[], int outIndex) {
Object pkey = sessionKey.getPreparedKey();
if (pkey == null) {
try {
pkey = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16);
sessionKey.setPreparedKey(pkey);
} catch (InvalidKeyException ike) {
_log.log(Log.CRIT, "Invalid key", ike);
throw new IllegalArgumentException("invalid key? " + ike.getMessage());
}
}
CryptixRijndael_Algorithm.blockEncrypt(payload, out, inIndex, outIndex, pkey);
}
/** decrypt exactly 16 bytes of data with the session key provided
* @param payload encrypted data, 16 bytes starting at inIndex
* @param sessionKey private session key
* @param rv out parameter, 16 bytes starting at outIndex
*/
@Override
public final void decryptBlock(byte payload[], int inIndex, SessionKey sessionKey, byte rv[], int outIndex) {
// just let it throw NPE or IAE later for speed, you'll figure it out
//if ( (payload == null) || (rv == null) )
// throw new IllegalArgumentException("null block args");
//if (payload.length - inIndex > rv.length - outIndex)
// throw new IllegalArgumentException("bad block args [payload.len=" + payload.length
// + " inIndex=" + inIndex + " rv.len=" + rv.length
// + " outIndex="+outIndex);
Object pkey = sessionKey.getPreparedKey();
if (pkey == null) {
try {
pkey = CryptixRijndael_Algorithm.makeKey(sessionKey.getData(), 16);
sessionKey.setPreparedKey(pkey);
} catch (InvalidKeyException ike) {
_log.log(Log.CRIT, "Invalid key", ike);
throw new IllegalArgumentException("invalid key? " + ike.getMessage());
}
}
CryptixRijndael_Algorithm.blockDecrypt(payload, rv, inIndex, outIndex, pkey);
}
/******
private static final int MATCH_RUNS = 11000;
private static final int TIMING_RUNS = 100000;
******/
/**
* Test results 10K timing runs.
* July 2011 eeepc.
* Not worth enabling System version.
* And we can't get rid of Cryptix because AES-256 is unavailable
* in several JVMs.
* Make USE_SYSTEM_AES above non-final to run this.
* You also must comment out the length check in encrypt() and decrypt() above.
*
*<pre>
* JVM Cryptix (ms) System (ms)
* Sun 8662 n/a
* OpenJDK 8616 8510
* Harmony 14732 16986
* JamVM 50013 761494 (!)
* gij 51130 761693 (!)
* jrockit 9780 n/a
*</pre>
*
* Speed ups with AES-NI:
* May 2014 AMD Hexcore 100K runs (1024 bytes):
*<pre>
* JVM Cryptix (ms) System (ms)
* OpenJDK 6 3314 5030
* OpenJDK 7 3285 2476
*</pre>
*
* Cryptix is faster for data smaller than 704 bytes.
*/
/*******
public static void main(String args[]) {
I2PAppContext ctx = I2PAppContext.getGlobalContext();
try {
boolean canTestSystem = USE_SYSTEM_AES;
if (!canTestSystem)
System.out.println("System AES 256 not available, testing Cryptix only");
testEDBlock(ctx);
testEDBlock2(ctx);
testED2(ctx);
if (canTestSystem) {
System.out.println("Start Cryptix vs. System verification run of " + MATCH_RUNS);
for (int i = 0; i < MATCH_RUNS; i++) {
testED(ctx, false, true, 1024);
testED(ctx, true, false, 1024);
}
}
for (int len = 512; len <= 1024; len += 32) {
System.out.println("Start Cryptix run of " + TIMING_RUNS + " length " + len);
long start = System.currentTimeMillis();
for (int i = 0; i < TIMING_RUNS; i++) {
testED(ctx, false, false, len);
}
long cryptix = System.currentTimeMillis() - start;
System.out.println("Cryptix took " + cryptix);
if (canTestSystem) {
System.out.println("Start System run of " + TIMING_RUNS + " length " + len);
start = System.currentTimeMillis();
for (int i = 0; i < TIMING_RUNS; i++) {
testED(ctx, true, true, len);
}
long system = System.currentTimeMillis() - start;
System.out.println("System took " + system);
if (system < cryptix)
System.out.println("***System is " + (100 - (system * 100 / cryptix)) + "% better");
else
System.out.println("***System is " + ((system * 100 / cryptix) - 100) + "% worse");
}
}
//testFake(ctx);
//testNull(ctx);
} catch (Exception e) {
e.printStackTrace();
}
//try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
}
private static final byte[] _iv = new byte[16];
private static byte[] _orig = new byte[1024];
private static byte[] _encrypted = new byte[1024];
private static byte[] _decrypted = new byte[1024];
private static void testED(I2PAppContext ctx, boolean systemEnc, boolean systemDec, int len) {
SessionKey key = ctx.keyGenerator().generateSessionKey();
ctx.random().nextBytes(_iv);
ctx.random().nextBytes(_orig);
CryptixAESEngine aes = new CryptixAESEngine(ctx);
USE_SYSTEM_AES = systemEnc;
aes.encrypt(_orig, 0, _encrypted, 0, key, _iv, len);
USE_SYSTEM_AES = systemDec;
aes.decrypt(_encrypted, 0, _decrypted, 0, key, _iv, len);
if (!DataHelper.eq(_decrypted, 0, _orig, 0, len))
throw new RuntimeException("full D(E(orig)) != orig");
//else
// System.out.println("full D(E(orig)) == orig");
}
// this verifies decryption in-place
private static void testED2(I2PAppContext ctx) {
SessionKey key = ctx.keyGenerator().generateSessionKey();
byte iv[] = new byte[16];
byte orig[] = new byte[128];
byte data[] = new byte[128];
ctx.random().nextBytes(iv);
ctx.random().nextBytes(orig);
CryptixAESEngine aes = new CryptixAESEngine(ctx);
aes.encrypt(orig, 0, data, 0, key, iv, data.length);
aes.decrypt(data, 0, data, 0, key, iv, data.length);
if (!DataHelper.eq(data,orig))
throw new RuntimeException("full D(E(orig)) != orig");
else
System.out.println("full D(E(orig)) == orig");
}
private static void testFake(I2PAppContext ctx) {
SessionKey key = ctx.keyGenerator().generateSessionKey();
SessionKey wrongKey = ctx.keyGenerator().generateSessionKey();
byte iv[] = new byte[16];
byte orig[] = new byte[128];
byte encrypted[] = new byte[128];
byte decrypted[] = new byte[128];
ctx.random().nextBytes(iv);
ctx.random().nextBytes(orig);
CryptixAESEngine aes = new CryptixAESEngine(ctx);
aes.encrypt(orig, 0, encrypted, 0, key, iv, orig.length);
aes.decrypt(encrypted, 0, decrypted, 0, wrongKey, iv, encrypted.length);
if (DataHelper.eq(decrypted,orig))
throw new RuntimeException("full D(E(orig)) == orig when we used the wrong key!");
else
System.out.println("full D(E(orig)) != orig when we used the wrong key");
}
private static void testNull(I2PAppContext ctx) {
SessionKey key = ctx.keyGenerator().generateSessionKey();
SessionKey wrongKey = ctx.keyGenerator().generateSessionKey();
byte iv[] = new byte[16];
byte orig[] = new byte[128];
byte encrypted[] = new byte[128];
byte decrypted[] = new byte[128];
ctx.random().nextBytes(iv);
ctx.random().nextBytes(orig);
CryptixAESEngine aes = new CryptixAESEngine(ctx);
aes.encrypt(orig, 0, encrypted, 0, key, iv, orig.length);
try {
aes.decrypt(null, 0, null, 0, wrongKey, iv, encrypted.length);
} catch (IllegalArgumentException iae) {
return;
}
throw new RuntimeException("full D(E(orig)) didn't fail when we used null!");
}
private static void testEDBlock(I2PAppContext ctx) {
SessionKey key = ctx.keyGenerator().generateSessionKey();
byte iv[] = new byte[16];
byte orig[] = new byte[16];
byte encrypted[] = new byte[16];
byte decrypted[] = new byte[16];
ctx.random().nextBytes(iv);
ctx.random().nextBytes(orig);
CryptixAESEngine aes = new CryptixAESEngine(ctx);
aes.encryptBlock(orig, 0, key, encrypted, 0);
aes.decryptBlock(encrypted, 0, key, decrypted, 0);
if (!DataHelper.eq(decrypted,orig))
throw new RuntimeException("block D(E(orig)) != orig");
else
System.out.println("block D(E(orig)) == orig");
}
private static void testEDBlock2(I2PAppContext ctx) {
SessionKey key = ctx.keyGenerator().generateSessionKey();
byte iv[] = new byte[16];
byte orig[] = new byte[16];
byte data[] = new byte[16];
ctx.random().nextBytes(iv);
ctx.random().nextBytes(orig);
CryptixAESEngine aes = new CryptixAESEngine(ctx);
aes.encryptBlock(orig, 0, key, data, 0);
aes.decryptBlock(data, 0, key, data, 0);
if (!DataHelper.eq(data,orig))
throw new RuntimeException("block D(E(orig)) != orig");
else
System.out.println("block D(E(orig)) == orig");
}
*******/
}