Implement native AES cipher.

This commit is contained in:
Brian Pellin
2009-12-16 22:17:49 -06:00
parent ee31bd3bfe
commit d132e9f291
6 changed files with 561 additions and 2 deletions

View File

@@ -4,10 +4,12 @@ include $(CLEAR_VARS)
LOCAL_MODULE := final-key
LOCAL_SRC_FILES := final_key.c
LOCAL_SRC_FILES := final_key.c aes.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/../openssl-0.9.8l/include
LOCAL_STATIC_LIBRARIES := openssl-crypto
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

138
jni/final_key/aes.c Normal file
View File

@@ -0,0 +1,138 @@
#include <openssl/evp.h>
#include <openssl/aes.h>
#include <jni.h>
#include <stdlib.h>
#include <android/log.h>
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, "KeePassDroidNative", __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG , "KeePassDroidNative", __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , "KeePassDroidNative", __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN , "KeePassDroidNative", __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , "KeePassDroidNative", __VA_ARGS__)
jlong Java_com_keepassdroid_crypto_NativeAESCipherSpi_nativeInit(JNIEnv *env,
jobject this, jboolean encrypt, jbyteArray key, jbyteArray iv,
jboolean padding) {
LOGV("1");
// Convert keys to c
jsize key_len = (*env)->GetArrayLength(env, key);
char *c_key = (char *) malloc(key_len);
(*env)->GetByteArrayRegion(env, key, 0, key_len, c_key);
LOGV("2: Keylen: %d", key_len);
// Covert iv to c
jsize iv_len = (*env)->GetArrayLength(env, iv);
char *c_iv = (char *) malloc(iv_len);
(*env)->GetByteArrayRegion(env, iv, 0, iv_len, c_iv);
LOGV("3: IvLen: %d", iv_len);
EVP_CIPHER_CTX *ctx = (EVP_CIPHER_CTX *) malloc(sizeof(EVP_CIPHER_CTX));
LOGV("3.5: %d", sizeof(EVP_CIPHER_CTX));
EVP_CIPHER_CTX_init(ctx);
EVP_CipherInit_ex(ctx, EVP_aes_256_cbc(), NULL, c_key, c_iv, encrypt);
LOGV("4");
if ( padding ) {
EVP_CIPHER_CTX_set_padding(ctx, 1);
} else {
EVP_CIPHER_CTX_set_padding(ctx, 0);
}
LOGV("5");
// Free allocated memory
free(c_iv);
free(c_key);
LOGV("6: ctxPtr=%d",ctx);
return (jlong) ctx;
}
void Java_com_keepassdroid_crypto_NativeAESCipherSpi_nativeCleanup(JNIEnv *env,
jobject this, jlong ctxPtr) {
LOGV("cleanup");
EVP_CIPHER_CTX *ctx = (EVP_CIPHER_CTX *) ctxPtr;
EVP_CIPHER_CTX_cleanup(ctx);
free(ctx);
}
jint Java_com_keepassdroid_crypto_NativeAESCipherSpi_nativeUpdate(JNIEnv *env,
jobject this, jlong ctxPtr, jbyteArray input, jint inputOffset,
jint inputLen, jbyteArray output, jint outputOffset, jint outputSize) {
LOGV("InputSize: %d; OutputSize: %d", inputLen, outputSize);
if ( inputLen == 0 ) {
return 0;
}
char *c_input = (char *) malloc(inputLen);
(*env)->GetByteArrayRegion(env, input, inputOffset, inputLen, c_input);
int outLen;
char *c_output;
// Worst case is all full blocks with 1 byte on the left and right
//int max_update_size = (((inputLen - 2) / AES_BLOCK_SIZE) + 2) * AES_BLOCK_SIZE;
c_output = (char *) malloc(outputSize);
EVP_CIPHER_CTX *ctx = (EVP_CIPHER_CTX *) ctxPtr;
LOGV("Pre: ctxPtr=%d", ctx);
EVP_CipherUpdate(ctx, c_output, &outLen, c_input, inputLen);
LOGV("Post");
/* output can differ on final
if ( outLen != ((int) outputSize) ) {
LOGV("Outsize differs: %d", outLen);
free(c_output);
free(c_input);
return -1;
}
*/
LOGV("PreOut: OutLen=%d", outLen);
(*env)->SetByteArrayRegion(env, output, outputOffset, outLen, c_output);
free(c_output);
free(c_input);
LOGV("PostOut");
return (jint) outLen; // I think jint should always be bigger than int
}
jint Java_com_keepassdroid_crypto_NativeAESCipherSpi_nativeDoFinal(JNIEnv *env,
jobject this, jlong ctxPtr, jbyteArray output, jint outputOffset,
jint outputSize) {
LOGV("outputOffset=%d", outputOffset);
char *c_output = (char *) malloc(outputSize);
int outLen;
EVP_CIPHER_CTX *ctx = (EVP_CIPHER_CTX *) ctxPtr;
EVP_CipherFinal_ex(ctx, c_output, &outLen);
/*
if ( outLen != ((int) outputSize) ) {
free(c_output);
return -1;
}
*/
LOGV("Final: OutputLen=%d, outputOffset=%d", outLen, (int)outputOffset);
(*env)->SetByteArrayRegion(env, output, outputOffset, outLen, c_output);
free(c_output);
return (jint) outLen;
}

View File

@@ -0,0 +1,255 @@
/*
* Copyright 2009 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.crypto;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidParameterSpecException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
public class NativeAESCipherSpi extends CipherSpi {
private final int AES_BLOCK_SIZE = 16;
private byte[] mIV;
private boolean mIsInited = false;
private boolean mEncrypting = false;
private long mCtxPtr;
private int mBuffered;
private boolean mPadding = false;
@Override
protected byte[] engineDoFinal(byte[] input, int inputOffset, int inputLen)
throws IllegalBlockSizeException, BadPaddingException {
int maxSize = engineGetOutputSize(inputLen);
byte[] output = new byte[maxSize];
int finalSize = doFinal(input, inputOffset, inputLen, output, 0);
if ( maxSize == finalSize ) {
return output;
} else {
// TODO: Special doFinal to avoid this copy
byte[] exact = new byte[finalSize];
System.arraycopy(output, 0, exact, 0, finalSize);
return exact;
}
}
@Override
protected int engineDoFinal(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException,
IllegalBlockSizeException, BadPaddingException {
int result = doFinal(input, inputOffset, inputLen, output, outputOffset);
if ( result == -1 ) {
throw new ShortBufferException();
}
return result;
}
private int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
mBuffered = 0;
int outputSize = engineGetOutputSize(inputLen);
int updateAmt = nativeUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, outputSize);
int finalAmt = nativeDoFinal(mCtxPtr, output, outputOffset + updateAmt, outputSize - updateAmt);
return updateAmt + finalAmt;
}
private native int nativeDoFinal(long ctxPtr, byte[] output, int outputOffest, int outputSize);
@Override
protected int engineGetBlockSize() {
return AES_BLOCK_SIZE;
}
@Override
protected byte[] engineGetIV() {
return mIV;
}
@Override
protected int engineGetOutputSize(int inputLen) {
int totalLen = mBuffered + inputLen;
if ( ! mPadding || ! mEncrypting ) {
return totalLen;
}
int padLen = AES_BLOCK_SIZE - (totalLen % AES_BLOCK_SIZE);
// TODO: Round up to nearest full block (there's probably a better way to do this)
return totalLen + padLen;
}
@Override
protected AlgorithmParameters engineGetParameters() {
// TODO Auto-generated method stub
return null;
}
@Override
protected void engineInit(int opmode, Key key, SecureRandom random)
throws InvalidKeyException {
byte[] ivArray = new byte[16];
random.nextBytes(ivArray);
init(opmode, key, new IvParameterSpec(ivArray));
}
@Override
protected void engineInit(int opmode, Key key,
AlgorithmParameterSpec params, SecureRandom random)
throws InvalidKeyException, InvalidAlgorithmParameterException {
IvParameterSpec ivparam;
if ( params instanceof IvParameterSpec ) {
ivparam = (IvParameterSpec) params;
} else {
throw new InvalidAlgorithmParameterException("params must be an IvParameterSpec.");
}
init(opmode, key, ivparam);
}
@Override
protected void engineInit(int opmode, Key key, AlgorithmParameters params,
SecureRandom random) throws InvalidKeyException,
InvalidAlgorithmParameterException {
try {
engineInit(opmode, key, params.getParameterSpec(AlgorithmParameterSpec.class), random);
} catch (InvalidParameterSpecException e) {
throw new InvalidAlgorithmParameterException(e);
}
}
private void init(int opmode, Key key, IvParameterSpec params) {
if ( mIsInited ) {
cleanup();
NativeLib.init();
}
mIV = params.getIV();
mEncrypting = opmode == Cipher.ENCRYPT_MODE;
mBuffered = 0;
mCtxPtr = nativeInit(mEncrypting, key.getEncoded(), mIV, mPadding);
}
private native long nativeInit(boolean encrypt, byte[] key, byte[] iv, boolean mPadding);
private void cleanup() {
nativeCleanup(mCtxPtr);
mCtxPtr = 0;
}
private native void nativeCleanup(long ctxPtr);
@Override
protected void engineSetMode(String mode) throws NoSuchAlgorithmException {
if ( ! mode.equals("CBC") ) {
throw new NoSuchAlgorithmException("This only supports CBC mode");
}
}
@Override
protected void engineSetPadding(String padding)
throws NoSuchPaddingException {
if ( ! mIsInited ) {
NativeLib.init();
}
if ( padding.length() == 0 ) {
return;
}
if ( ! padding.equals("PKCS5Padding") ) {
throw new NoSuchPaddingException("Only supports PKCS5Padding.");
}
mPadding = true;
}
@Override
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
int maxSize = engineGetOutputSize(inputLen);
byte output[] = new byte[maxSize];
int updateSize = update(input, inputOffset, inputLen, output, 0);
if ( updateSize == maxSize ) {
return output;
} else {
// TODO: We could optimize update for this case to avoid this extra copy
byte[] exact = new byte[updateSize];
System.arraycopy(output, 0, exact, 0, updateSize);
return exact;
}
}
@Override
protected int engineUpdate(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException {
int result = update(input, inputOffset, inputLen, output, outputOffset);
if ( result == -1 ) {
throw new ShortBufferException("Insufficient buffer.");
}
return result;
}
int update(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) {
mBuffered = (mBuffered + inputLen) % AES_BLOCK_SIZE;
return nativeUpdate(mCtxPtr, input, inputOffset, inputLen, output, outputOffset, engineGetOutputSize(inputLen));
}
private native int nativeUpdate(long ctxPtr, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset, int outputSize);
}

View File

@@ -45,7 +45,7 @@ public class AndroidFinalKey extends FinalKey {
try {
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(pKeySeed, "AES"));
} catch (InvalidKeyException e) {
throw new IOException("InvalidKeyException: " + e.getMessage());
throw new IOException("InvalidPasswordException: " + e.getMessage());
}
// Encrypt key rounds times

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2009 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 3 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.tests.crypto;
import static org.junit.Assert.assertArrayEquals;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Random;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import junit.framework.TestCase;
import com.keepassdroid.crypto.finalkey.AESProvider;
public class AESTest extends TestCase {
private Random mRand = new Random();
public void testEncrypt() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException {
// Test above below and at the blocksize
testFinal(15);
testFinal(16);
testFinal(17);
// Test random larger sizes
int size = mRand.nextInt(494) + 18;
testFinal(size);
}
private void testFinal(int dataSize) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
// Generate some input
byte[] input = new byte[dataSize];
mRand.nextBytes(input);
// Generate key
byte[] keyArray = new byte[32];
mRand.nextBytes(keyArray);
SecretKeySpec key = new SecretKeySpec(keyArray, "AES");
// Generate IV
byte[] ivArray = new byte[16];
mRand.nextBytes(ivArray);
IvParameterSpec iv = new IvParameterSpec(ivArray);
Cipher android = Cipher.getInstance("AES/CBC/PKCS5Padding");
android.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outAndroid = android.doFinal(input, 0, dataSize);
AESProvider prov = new AESProvider();
Cipher nat = Cipher.getInstance("AES/CBC/PKCS5Padding", prov);
nat.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] outNative = nat.doFinal(input, 0, dataSize);
assertArrayEquals("Arrays differ on size: " + dataSize, outAndroid, outNative);
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2009 Brian Pellin.
*
* This file is part of KeePassDroid.
*
* KeePassDroid 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 3 of the License, or
* (at your option) any later version.
*
* KeePassDroid 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 KeePassDroid. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.keepassdroid.tests.crypto;
import static org.junit.Assert.assertArrayEquals;
import java.io.IOException;
import java.util.Random;
import junit.framework.TestCase;
import com.keepassdroid.crypto.finalkey.AndroidFinalKey;
import com.keepassdroid.crypto.finalkey.NativeFinalKey;
public class FinalKeyTest extends TestCase {
private Random mRand;
@Override
protected void setUp() throws Exception {
super.setUp();
mRand = new Random();
}
public void testReflect() {
boolean available = NativeFinalKey.availble();
assertTrue("NativeFinalKey library cannot be loaded", available);
byte[] key = new byte[32];
mRand.nextBytes(key);
byte[] out = NativeFinalKey.reflect(key);
assertArrayEquals("Array not reflected correctly", key, out);
}
public void testNativeAndroid() throws IOException {
// Test both an old and an even number to test my flip variable
testNativeFinalKey(5);
testNativeFinalKey(6);
}
private void testNativeFinalKey(int rounds) throws IOException {
byte[] seed = new byte[32];
byte[] key = new byte[32];
byte[] nativeKey;
byte[] androidKey;
mRand.nextBytes(seed);
mRand.nextBytes(key);
AndroidFinalKey aKey = new AndroidFinalKey();
androidKey = aKey.transformMasterKey(seed, key, rounds);
NativeFinalKey nKey = new NativeFinalKey();
nativeKey = nKey.transformMasterKey(seed, key, rounds);
assertArrayEquals("Does not match", androidKey, nativeKey);
}
}