Added PBKDF2 to crypto component.

This commit is contained in:
akwizgran
2013-04-16 12:04:23 +01:00
parent 0dcc1a6d54
commit e343c9f4bb
4 changed files with 175 additions and 16 deletions

View File

@@ -96,15 +96,33 @@ public interface CryptoComponent {
long connection);
/**
* Encrypts the given plaintext so it can be written to temporary storage.
* The ciphertext will not be decryptable after the app restarts.
* Encrypts and authenticates the given plaintext so it can be written to
* temporary storage. The ciphertext will not be decryptable after the app
* restarts.
*/
byte[] encryptTemporaryStorage(byte[] plaintext);
/**
* Decrypts the given ciphertext that has been read from temporary storage.
* Returns null if the ciphertext is not decryptable (for example, if it
* was written before the app restarted).
* Decrypts and authenticates the given ciphertext that has been read from
* temporary storage. Returns null if the ciphertext cannot be decrypted
* and authenticated (for example, if it was written before the app
* restarted).
*/
byte[] decryptTemporaryStorage(byte[] ciphertext);
/**
* Encrypts and authenticates the given plaintext so it can be written to
* storage. The encryption and authentication keys are derived from the
* given password. The ciphertext will be decryptable using the same
* password after the app restarts.
*/
byte[] encryptWithPassword(byte[] plaintext, char[] password);
/**
* Decrypts and authenticates the given ciphertext that has been read from
* storage. The encryption and authentication keys are derived from the
* given password. Returns null if the ciphertext cannot be decrypted and
* authenticated (for example, if the password is wrong).
*/
byte[] decryptWithPassword(byte[] ciphertext, char[] password);
}

View File

@@ -6,6 +6,8 @@ import static net.sf.briar.api.invitation.InvitationConstants.CODE_BITS;
import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
@@ -37,10 +39,14 @@ import net.sf.briar.api.crypto.MessageDigest;
import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.util.ByteUtils;
import org.spongycastle.crypto.CipherParameters;
import org.spongycastle.crypto.engines.AESEngine;
import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.spongycastle.crypto.modes.AEADBlockCipher;
import org.spongycastle.crypto.modes.GCMBlockCipher;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.util.Strings;
class CryptoComponentImpl implements CryptoComponent {
@@ -55,9 +61,11 @@ class CryptoComponentImpl implements CryptoComponent {
private static final String SIGNATURE_KEY_PAIR_ALGO = "ECDSA";
private static final int SIGNATURE_KEY_PAIR_BITS = 384;
private static final String TAG_CIPHER_ALGO = "AES/ECB/NoPadding";
private static final int GCM_MAC_LENGTH = 16; // 128 bits
private static final int GCM_MAC_BYTES = 16; // 128 bits
private static final String STORAGE_CIPHER_ALGO = "AES/GCM/NoPadding";
private static final int STORAGE_IV_LENGTH = 32; // 256 bits
private static final int STORAGE_IV_BYTES = 16; // 128 bits
private static final int PBKDF_SALT_BYTES = 16; // 128 bits
private static final int PBKDF_ITERATIONS = 10 * 1000; // FIXME: How many?
private static final String KEY_DERIVATION_ALGO = "AES/CTR/NoPadding";
private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits
@@ -345,7 +353,7 @@ class CryptoComponentImpl implements CryptoComponent {
// This code is specific to Spongy Castle because javax.crypto.Cipher
// doesn't support additional authenticated data until Java 7
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
return new AuthenticatedCipherImpl(cipher, GCM_MAC_LENGTH);
return new AuthenticatedCipherImpl(cipher, GCM_MAC_BYTES);
}
public void encodeTag(byte[] tag, Cipher tagCipher, ErasableKey tagKey,
@@ -366,19 +374,19 @@ class CryptoComponentImpl implements CryptoComponent {
public byte[] encryptTemporaryStorage(byte[] input) {
// Generate a random IV
byte[] ivBytes = new byte[STORAGE_IV_LENGTH];
byte[] ivBytes = new byte[STORAGE_IV_BYTES];
secureRandom.nextBytes(ivBytes);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
// The output contains the IV, ciphertext and MAC
int outputLen = STORAGE_IV_LENGTH + input.length + GCM_MAC_LENGTH;
int outputLen = STORAGE_IV_BYTES + input.length + GCM_MAC_BYTES;
byte[] output = new byte[outputLen];
System.arraycopy(ivBytes, 0, output, 0, STORAGE_IV_LENGTH);
System.arraycopy(ivBytes, 0, output, 0, STORAGE_IV_BYTES);
// Initialise the cipher and encrypt the plaintext
Cipher cipher;
try {
cipher = Cipher.getInstance(STORAGE_CIPHER_ALGO, PROVIDER);
cipher.init(ENCRYPT_MODE, temporaryStorageKey, iv);
cipher.doFinal(input, 0, input.length, output, STORAGE_IV_LENGTH);
cipher.doFinal(input, 0, input.length, output, STORAGE_IV_BYTES);
return output;
} catch(GeneralSecurityException e) {
throw new RuntimeException(e);
@@ -387,9 +395,9 @@ class CryptoComponentImpl implements CryptoComponent {
public byte[] decryptTemporaryStorage(byte[] input) {
// The input contains the IV, ciphertext and MAC
if(input.length < STORAGE_IV_LENGTH + GCM_MAC_LENGTH)
if(input.length < STORAGE_IV_BYTES + GCM_MAC_BYTES)
return null; // Invalid
IvParameterSpec iv = new IvParameterSpec(input, 0, STORAGE_IV_LENGTH);
IvParameterSpec iv = new IvParameterSpec(input, 0, STORAGE_IV_BYTES);
// Initialise the cipher
Cipher cipher;
try {
@@ -400,13 +408,77 @@ class CryptoComponentImpl implements CryptoComponent {
}
// Try to decrypt the ciphertext (may be invalid)
try {
return cipher.doFinal(input, STORAGE_IV_LENGTH,
input.length - STORAGE_IV_LENGTH);
return cipher.doFinal(input, STORAGE_IV_BYTES,
input.length - STORAGE_IV_BYTES);
} catch(GeneralSecurityException e) {
return null; // Invalid
}
}
public byte[] encryptWithPassword(byte[] input, char[] password) {
// Generate a random salt
byte[] salt = new byte[PBKDF_SALT_BYTES];
secureRandom.nextBytes(salt);
// Derive the key from the password
byte[] keyBytes = pbkdf2(password, salt);
ErasableKey key = new ErasableKeyImpl(keyBytes, SECRET_KEY_ALGO);
// Generate a random IV
byte[] ivBytes = new byte[STORAGE_IV_BYTES];
secureRandom.nextBytes(ivBytes);
IvParameterSpec iv = new IvParameterSpec(ivBytes);
// The output contains the salt, IV, ciphertext and MAC
int outputLen = PBKDF_SALT_BYTES + STORAGE_IV_BYTES + input.length
+ GCM_MAC_BYTES;
byte[] output = new byte[outputLen];
System.arraycopy(salt, 0, output, 0, PBKDF_SALT_BYTES);
System.arraycopy(ivBytes, 0, output, PBKDF_SALT_BYTES,
STORAGE_IV_BYTES);
// Initialise the cipher and encrypt the plaintext
Cipher cipher;
try {
cipher = Cipher.getInstance(STORAGE_CIPHER_ALGO, PROVIDER);
cipher.init(ENCRYPT_MODE, key, iv);
cipher.doFinal(input, 0, input.length, output,
PBKDF_SALT_BYTES + STORAGE_IV_BYTES);
return output;
} catch(GeneralSecurityException e) {
throw new RuntimeException(e);
} finally {
key.erase();
}
}
public byte[] decryptWithPassword(byte[] input, char[] password) {
// The input contains the salt, IV, ciphertext and MAC
if(input.length < PBKDF_SALT_BYTES + STORAGE_IV_BYTES + GCM_MAC_BYTES)
return null; // Invalid
byte[] salt = new byte[PBKDF_SALT_BYTES];
System.arraycopy(input, 0, salt, 0, PBKDF_SALT_BYTES);
IvParameterSpec iv = new IvParameterSpec(input, PBKDF_SALT_BYTES,
STORAGE_IV_BYTES);
// Derive the key from the password
byte[] keyBytes = pbkdf2(password, salt);
ErasableKey key = new ErasableKeyImpl(keyBytes, SECRET_KEY_ALGO);
// Initialise the cipher
Cipher cipher;
try {
cipher = Cipher.getInstance(STORAGE_CIPHER_ALGO, PROVIDER);
cipher.init(DECRYPT_MODE, key, iv);
} catch(GeneralSecurityException e) {
key.erase();
throw new RuntimeException(e);
}
// Try to decrypt the ciphertext (may be invalid)
try {
return cipher.doFinal(input, PBKDF_SALT_BYTES + STORAGE_IV_BYTES,
input.length - PBKDF_SALT_BYTES - STORAGE_IV_BYTES);
} catch(GeneralSecurityException e) {
return null; // Invalid
} finally {
key.erase();
}
}
private ECPublicKey checkP384Params(PublicKey publicKey) {
if(!(publicKey instanceof ECPublicKey)) throw new RuntimeException();
ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
@@ -480,4 +552,31 @@ class CryptoComponentImpl implements CryptoComponent {
throw new RuntimeException(e);
}
}
// Password-based key derivation function - see PKCS#5 v2.1, section 5.2
private byte[] pbkdf2(char[] password, byte[] salt) {
// This code is specific to Spongy Castle because the password-based
// KDF exposed through the JCE interface is PKCS#12
byte[] utf8 = toUtf8ByteArray(password);
PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator();
gen.init(utf8, salt, PBKDF_ITERATIONS);
int keyLengthInBits = SECRET_KEY_BYTES * 8;
CipherParameters p = gen.generateDerivedParameters(keyLengthInBits);
ByteUtils.erase(utf8);
return ((KeyParameter) p).getKey();
}
byte[] toUtf8ByteArray(char[] c) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
Strings.toUTF8ByteArray(c, out);
byte[] utf8 = out.toByteArray();
// Erase the output stream's buffer
out.reset();
out.write(new byte[utf8.length]);
return utf8;
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -75,6 +75,7 @@
<test name='net.sf.briar.crypto.KeyAgreementTest'/>
<test name='net.sf.briar.crypto.KeyDerivationTest'/>
<test name='net.sf.briar.crypto.KeyEncodingAndParsingTest'/>
<test name="net.sf.briar.crypto.PasswordBasedKdfTest"/>
<test name='net.sf.briar.db.BasicH2Test'/>
<test name='net.sf.briar.db.DatabaseCleanerImplTest'/>
<test name='net.sf.briar.db.DatabaseComponentImplTest'/>

View File

@@ -0,0 +1,41 @@
package net.sf.briar.crypto;
import static org.junit.Assert.assertArrayEquals;
import java.util.Random;
import net.sf.briar.BriarTestCase;
import net.sf.briar.api.crypto.CryptoComponent;
import org.junit.Test;
public class PasswordBasedKdfTest extends BriarTestCase {
@Test
public void testEncryptionAndDecryption() {
CryptoComponent crypto = new CryptoComponentImpl();
Random random = new Random();
byte[] input = new byte[123];
random.nextBytes(input);
char[] password = "password".toCharArray();
byte[] ciphertext = crypto.encryptWithPassword(input, password);
byte[] output = crypto.decryptWithPassword(ciphertext, password);
assertArrayEquals(input, output);
}
@Test
public void testInvalidCiphertextReturnsNull() {
CryptoComponent crypto = new CryptoComponentImpl();
Random random = new Random();
byte[] input = new byte[123];
random.nextBytes(input);
char[] password = "password".toCharArray();
byte[] ciphertext = crypto.encryptWithPassword(input, password);
// Modify the ciphertext
int position = random.nextInt(ciphertext.length);
int value = random.nextInt(256);
ciphertext[position] = (byte) value;
byte[] output = crypto.decryptWithPassword(ciphertext, password);
assertNull(output);
}
}