Skip to content

Commit

Permalink
Merge pull request #31 from patrickfav/feat-6-kitkat-cbc
Browse files Browse the repository at this point in the history
Add encryption support for KitKat
  • Loading branch information
patrickfav committed Nov 8, 2018
2 parents 1bbc3e0 + e574a51 commit e9129aa
Show file tree
Hide file tree
Showing 11 changed files with 786 additions and 128 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## v0.6.0

* [Security] Fix bcrypt implementation #16, #8
* [Security] Add full Kitkat support #6, #31
* Add change password feature #13, #22
* Add change key stretching feature #16

Expand Down
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ SharedPreferences preferences = Armadillo.create(context, "myCustomPreferences")
.secureRandom(new SecureRandom()) //provide your own secure random for salt/iv generation
.encryptionFingerprint(context, userId.getBytes(StandardCharsets.UTF_8)) //add the user id to fingerprint
.supportVerifyPassword(true) //enables optional password validation support `.isValidPassword()`
.enableKitKatSupport(true) //enable optional kitkat support
.build();
```

Expand All @@ -76,6 +77,28 @@ first put operation:

```

### KitKat Support

Unfortunately [Android SDK 19 (KITKAT)](https://en.wikipedia.org/wiki/Android_KitKat) does not fully support AES GCM mode.
Therefore a backwards compatible implementation of AES using [CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_(CBC))
with [Encrypt-then-MAC](https://en.wikipedia.org/wiki/Authenticated_encryption#Encrypt-then-MAC_(EtM))
can be used to support this library on older devices. This should provide
the same security strength as the GCM version, however the support must
be enabled manually:


```java
SharedPreferences preferences = Armadillo.create(context, "myCustomPreferences")
.enableKitKatSupport(true)
...
.build();
```

In this mode, if on a KitKat device the backwards-compatible implementation is
used, the default AES-GCM version otherwise. Upgrading to a newer OS version
the content should still be decryptable, while newer content will then be
encrypted with the AES-GCM version.

## Description

### Design Choices
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package at.favre.lib.armadillo;

import org.junit.Ignore;
import org.junit.Test;

import java.nio.charset.StandardCharsets;
Expand All @@ -15,6 +16,7 @@
import at.favre.lib.bytes.Bytes;
import at.favre.lib.crypto.bcrypt.BCrypt;

@Ignore
public class BcryptMicroBenchmark {

private final Random rnd = new Random();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.provider.Settings;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;
Expand All @@ -13,6 +14,7 @@
import java.security.SecureRandom;
import java.security.Security;

import static junit.framework.TestCase.assertEquals;
import at.favre.lib.bytes.Bytes;

/**
Expand All @@ -24,29 +26,37 @@
public class SecureSharedPreferencesTest extends ASecureSharedPreferencesTest {
protected Armadillo.Builder create(String name, char[] pw) {
return Armadillo.create(InstrumentationRegistry.getTargetContext(), name)
.encryptionFingerprint(InstrumentationRegistry.getTargetContext())
.password(pw);
.encryptionFingerprint(InstrumentationRegistry.getTargetContext())
.enableKitKatSupport(isKitKatOrBelow())
.password(pw);
}

@Override
protected boolean isKitKatOrBelow() {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP;
}

@Test
public void quickStartTest() throws Exception {
public void quickStartTest() {
Context context = InstrumentationRegistry.getTargetContext();
SharedPreferences preferences = Armadillo.create(context, "myPrefs")
.encryptionFingerprint(context)
.build();
.encryptionFingerprint(context)
.enableKitKatSupport(isKitKatOrBelow()).build();

preferences.edit().putString("key1", "string").apply();
String s = preferences.getString("key1", null);

assertEquals("string", s);
}

@Test
public void testWithDifferentKeyStrength() throws Exception {
public void testWithDifferentKeyStrength() {
preferenceSmokeTest(create("fingerprint", null)
.encryptionKeyStrength(AuthenticatedEncryption.STRENGTH_VERY_HIGH).build());
.encryptionKeyStrength(AuthenticatedEncryption.STRENGTH_VERY_HIGH).build());
}

@Test
public void advancedTest() throws Exception {
public void advancedTest() {
Context context = InstrumentationRegistry.getTargetContext();
String userId = "1234";
SharedPreferences preferences = Armadillo.create(context, "myCustomPreferences")
Expand All @@ -57,10 +67,13 @@ public void advancedTest() throws Exception {
.secureRandom(new SecureRandom()) //provide your own secure random for salt/iv generation
.encryptionFingerprint(context, userId.getBytes(StandardCharsets.UTF_8)) //add the user id to fingerprint
.supportVerifyPassword(true) //enables optional password validation support `.isValidPassword()`
.enableKitKatSupport(true) //enable optional kitkat support
.build();

preferences.edit().putString("key1", "string").apply();
String s = preferences.getString("key1", null);

assertEquals("string", s);
}

private String getAndroidId(Context context) {
Expand Down
212 changes: 212 additions & 0 deletions armadillo/src/main/java/at/favre/lib/armadillo/AesCbcEncryption.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package at.favre.lib.armadillo;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.Provider;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import at.favre.lib.bytes.Bytes;
import at.favre.lib.crypto.HKDF;

/**
* Implements AES (Advanced Encryption Standard) with Cipher Block Chaining (CBC), which is a mode of
* operation for symmetric key cryptographic block ciphers. For integrity it uses HMAC with SHA-256,
* using the encrypt-then-mac schema.
* <p>
* The iv, mac and encrypted content will be encoded to the following format:
* <p>
* out = byte[] {x y y y y y y y y y y y y i j j ... z z z ...}
* <p>
* x = IV length as byte
* y = IV bytes
* i = mac length as byte
* j = mac bytes
* z = content bytes (encrypted content, auth tag)
*
* @author Patrick Favre-Bulle
* @since 27.10.2018
* @deprecated this is only meant for Kitkat backwards compatibility as this version and below does not
* support AES-GCM via JCA/JCE.
*/
@SuppressWarnings({"WeakerAccess", "DeprecatedIsStillUsed"})
@Deprecated
final class AesCbcEncryption implements AuthenticatedEncryption {
private static final String ALGORITHM = "AES/CBC/PKCS5Padding";
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final int IV_LENGTH_BYTE = 16;

private final SecureRandom secureRandom;
private final Provider provider;
private Cipher cipher;
private Mac hmac;

public AesCbcEncryption() {
this(new SecureRandom(), null);
}

public AesCbcEncryption(SecureRandom secureRandom) {
this(secureRandom, null);
}

public AesCbcEncryption(SecureRandom secureRandom, Provider provider) {
this.secureRandom = secureRandom;
this.provider = provider;
}

@Override
public byte[] encrypt(byte[] rawEncryptionKey, byte[] rawData, @Nullable byte[] associatedData) throws AuthenticatedEncryptionException {
checkAesKey(rawEncryptionKey);

byte[] iv = null;
byte[] encrypted = null;
byte[] mac = null;
try {
iv = new byte[IV_LENGTH_BYTE];
secureRandom.nextBytes(iv);

final Cipher cipherEnc = getCipher();
cipherEnc.init(Cipher.ENCRYPT_MODE, createEncryptionKey(rawEncryptionKey), new IvParameterSpec(iv));
encrypted = cipherEnc.doFinal(rawData);

mac = macCipherText(rawEncryptionKey, encrypted, iv, associatedData);

ByteBuffer byteBuffer = ByteBuffer.allocate(1 + iv.length + 1 + mac.length + encrypted.length);
byteBuffer.put((byte) iv.length);
byteBuffer.put(iv);
byteBuffer.put((byte) mac.length);
byteBuffer.put(mac);
byteBuffer.put(encrypted);

return byteBuffer.array();
} catch (Exception e) {
throw new AuthenticatedEncryptionException("could not encrypt", e);
} finally {
Bytes.wrapNullSafe(iv).mutable().secureWipe();
Bytes.wrapNullSafe(encrypted).mutable().secureWipe();
Bytes.wrapNullSafe(mac).mutable().secureWipe();
}
}

@NonNull
private SecretKeySpec createEncryptionKey(byte[] rawEncryptionKey) {
return new SecretKeySpec(HKDF.fromHmacSha256().expand(rawEncryptionKey, Bytes.from("encKey").array(), rawEncryptionKey.length), "AES");
}

private byte[] macCipherText(byte[] rawEncryptionKey, byte[] cipherText, byte[] iv, @Nullable byte[] associatedData) {
SecretKey macKey = createMacKey(rawEncryptionKey);

try {
createHmacInstance();
hmac.init(macKey);
hmac.update(iv);
hmac.update(cipherText);
} catch (InvalidKeyException e) {
// due to key generation in createMacKey(byte[]) this actually can not happen
throw new IllegalStateException("error during HMAC calculation");
}

if (associatedData != null) {
hmac.update(associatedData);
}

return hmac.doFinal();
}

@NonNull
private SecretKey createMacKey(byte[] rawEncryptionKey) {
byte[] derivedMacKey = HKDF.fromHmacSha256().expand(rawEncryptionKey, Bytes.from("macKey").array(), 32);
return new SecretKeySpec(derivedMacKey, HMAC_ALGORITHM);
}

private synchronized Mac createHmacInstance() {
if (hmac == null) {
try {
hmac = Mac.getInstance(HMAC_ALGORITHM);
} catch (Exception e) {
throw new IllegalStateException("could not get cipher instance", e);
}
}
return hmac;
}

@Override
public byte[] decrypt(byte[] rawEncryptionKey, byte[] encryptedData, @Nullable byte[] associatedData) throws AuthenticatedEncryptionException {
checkAesKey(rawEncryptionKey);

byte[] iv = null;
byte[] mac = null;
byte[] encrypted = null;
try {
ByteBuffer byteBuffer = ByteBuffer.wrap(encryptedData);

int ivLength = (byteBuffer.get() & 0xFF);
iv = new byte[ivLength];
byteBuffer.get(iv);

int macLength = (byteBuffer.get() & 0xFF);
mac = new byte[macLength];
byteBuffer.get(mac);

encrypted = new byte[byteBuffer.remaining()];
byteBuffer.get(encrypted);

verifyMac(rawEncryptionKey, encrypted, iv, mac, associatedData);

final Cipher cipherDec = getCipher();
cipherDec.init(Cipher.DECRYPT_MODE, createEncryptionKey(rawEncryptionKey), new IvParameterSpec(iv));
return cipherDec.doFinal(encrypted);
} catch (Exception e) {
throw new AuthenticatedEncryptionException("could not decrypt", e);
} finally {
Bytes.wrapNullSafe(iv).mutable().secureWipe();
Bytes.wrapNullSafe(encrypted).mutable().secureWipe();
Bytes.wrapNullSafe(mac).mutable().secureWipe();
}
}

private void verifyMac(byte[] rawEncryptionKey, byte[] cipherText, byte[] iv, byte[] mac, @Nullable byte[] associatedData)
throws AuthenticatedEncryptionException {
byte[] actualMac = macCipherText(rawEncryptionKey, cipherText, iv, associatedData);

if (!Bytes.wrap(mac).equalsConstantTime(actualMac)) {
throw new AuthenticatedEncryptionException("encryption integrity exception: mac does not match");
}
}

@Override
public int byteSizeLength(@KeyStrength int keyStrengthType) {
return ((keyStrengthType == STRENGTH_HIGH) ? 16 : 32);
}

private void checkAesKey(byte[] rawAesKey) throws IllegalArgumentException {
int keyLen = rawAesKey.length;

if ((keyLen != 16) && (keyLen != 32)) {
throw new IllegalArgumentException("AES key length must be 16, 24, or 32 bytes");
}
}

private synchronized Cipher getCipher() {
if (cipher == null) {
try {
if (provider != null) {
cipher = Cipher.getInstance(ALGORITHM, provider);
} else {
cipher = Cipher.getInstance(ALGORITHM);
}
} catch (Exception e) {
throw new IllegalStateException("could not get cipher instance", e);
}
}
return cipher;
}
}
Loading

0 comments on commit e9129aa

Please sign in to comment.