diff --git a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java index c500cc3ccc13394a617d9a1e9d2634fc0e737983..517a7c6d2afdaef844de9cb7ff30eabfed202a57 100644 --- a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java +++ b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java @@ -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); } diff --git a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java index 8b634030aa6cafc5de0fff32930439c3b594f91e..7526c3708da9fba24eece4402d02f33c9883c330 100644 --- a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java +++ b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java @@ -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); + } + } } diff --git a/briar-tests/build.xml b/briar-tests/build.xml index cf248ed5b3b87cdf199046e5403d75638173792a..655e6f80edbd4e7c6b3a48b5d6e4955ef31c3c64 100644 --- a/briar-tests/build.xml +++ b/briar-tests/build.xml @@ -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'/> diff --git a/briar-tests/src/net/sf/briar/crypto/PasswordBasedKdfTest.java b/briar-tests/src/net/sf/briar/crypto/PasswordBasedKdfTest.java new file mode 100644 index 0000000000000000000000000000000000000000..93b92638c45e270e1bf3a93a8c76f5bc7d340677 --- /dev/null +++ b/briar-tests/src/net/sf/briar/crypto/PasswordBasedKdfTest.java @@ -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); + } +}