-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #31 from patrickfav/feat-6-kitkat-cbc
Add encryption support for KitKat
- Loading branch information
Showing
11 changed files
with
786 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
212 changes: 212 additions & 0 deletions
212
armadillo/src/main/java/at/favre/lib/armadillo/AesCbcEncryption.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.