-
Notifications
You must be signed in to change notification settings - Fork 47
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Replace weak crypto implementation (#158)
Replace crypto implementation with stronger key generation method.
- Loading branch information
Showing
10 changed files
with
264 additions
and
26 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
35 changes: 35 additions & 0 deletions
35
line-sdk/src/main/java/com/linecorp/android/security/encryption/CipherData.kt
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,35 @@ | ||
package com.linecorp.android.security.encryption | ||
|
||
import android.util.Base64 | ||
|
||
class CipherData( | ||
val encryptedData: ByteArray, | ||
val initialVector: ByteArray, | ||
val hmacValue: ByteArray, | ||
) { | ||
fun encodeToBase64String(): String = | ||
listOf(encryptedData, initialVector, hmacValue) | ||
.joinToString(SEPARATOR) { it.encodeBase64() } | ||
|
||
companion object { | ||
private const val SEPARATOR = ";" | ||
private const val SIZE_DATA_TYPES = 3 | ||
|
||
fun decodeFromBase64String(cipherDataBase64String: String): CipherData { | ||
val parts = cipherDataBase64String.split(SEPARATOR) | ||
require(parts.size == SIZE_DATA_TYPES) { | ||
"Failed to split encrypted text `$cipherDataBase64String`" | ||
} | ||
|
||
return CipherData( | ||
encryptedData = parts[0].decodeBase64(), | ||
initialVector = parts[1].decodeBase64(), | ||
hmacValue = parts[2].decodeBase64() | ||
) | ||
} | ||
} | ||
} | ||
|
||
private fun ByteArray.encodeBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP) | ||
|
||
private fun String.decodeBase64(): ByteArray = Base64.decode(this, Base64.NO_WRAP) |
187 changes: 187 additions & 0 deletions
187
line-sdk/src/main/java/com/linecorp/android/security/encryption/StringAesCipher.kt
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,187 @@ | ||
package com.linecorp.android.security.encryption | ||
|
||
import android.content.Context | ||
import android.security.keystore.KeyGenParameterSpec | ||
import android.security.keystore.KeyProperties | ||
import android.security.keystore.KeyProperties.PURPOSE_DECRYPT | ||
import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT | ||
import android.security.keystore.KeyProperties.PURPOSE_SIGN | ||
import android.security.keystore.KeyProperties.PURPOSE_VERIFY | ||
import java.security.KeyStore | ||
import java.security.MessageDigest | ||
import javax.crypto.Cipher | ||
import javax.crypto.KeyGenerator | ||
import javax.crypto.Mac | ||
import javax.crypto.SecretKey | ||
import javax.crypto.spec.IvParameterSpec | ||
|
||
/** | ||
* AES cipher by AndroidKeyStore | ||
*/ | ||
class StringAesCipher : StringCipher { | ||
private val keyStore: KeyStore by lazy { | ||
KeyStore.getInstance(ANDROID_KEY_STORE).also { | ||
it.load(null) | ||
} | ||
} | ||
|
||
private lateinit var hmac: Mac | ||
|
||
override fun initialize(context: Context) { | ||
if (::hmac.isInitialized) { | ||
return | ||
} | ||
|
||
synchronized(this) { | ||
getAesSecretKey() | ||
val integrityKey = getIntegrityKey() | ||
|
||
hmac = Mac.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256).apply { | ||
init(integrityKey) | ||
} | ||
} | ||
} | ||
|
||
override fun encrypt(context: Context, plainText: String): String { | ||
synchronized(this) { | ||
initialize(context) | ||
|
||
try { | ||
val secretKey = getAesSecretKey() | ||
|
||
val cipher = Cipher.getInstance(TRANSFORMATION_FORMAT).apply { | ||
init(Cipher.ENCRYPT_MODE, secretKey) | ||
} | ||
val encryptedData: ByteArray = cipher.doFinal(plainText.toByteArray()) | ||
|
||
return CipherData( | ||
encryptedData = encryptedData, | ||
initialVector = cipher.iv, | ||
hmacValue = hmac.calculateHmacValue(encryptedData, cipher.iv) | ||
).encodeToBase64String() | ||
} catch (e: Exception) { | ||
throw EncryptionException("Failed to encrypt", e) | ||
} | ||
} | ||
} | ||
|
||
override fun decrypt(context: Context, cipherText: String): String { | ||
synchronized(this) { | ||
try { | ||
val secretKey = getAesSecretKey() | ||
|
||
val cipherData = CipherData.decodeFromBase64String(cipherText) | ||
|
||
cipherData.verifyHmacValue(hmac) | ||
|
||
val ivSpec = IvParameterSpec(cipherData.initialVector) | ||
|
||
return Cipher.getInstance(TRANSFORMATION_FORMAT) | ||
.apply { init(Cipher.DECRYPT_MODE, secretKey, ivSpec) } | ||
.run { doFinal(cipherData.encryptedData) } | ||
.let { | ||
String(it) | ||
} | ||
} catch (e: Exception) { | ||
throw EncryptionException("Failed to decrypt", e) | ||
} | ||
} | ||
} | ||
|
||
private fun getAesSecretKey(): SecretKey { | ||
return if (keyStore.containsAlias(AES_KEY_ALIAS)) { | ||
val secretKeyEntry = | ||
keyStore.getEntry(AES_KEY_ALIAS, null) as KeyStore.SecretKeyEntry | ||
|
||
secretKeyEntry.secretKey | ||
} else { | ||
createAesKey() | ||
} | ||
} | ||
|
||
private fun getIntegrityKey(): SecretKey { | ||
return if (keyStore.containsAlias(INTEGRITY_KEY_ALIAS)) { | ||
val secretKeyEntry = | ||
keyStore.getEntry(INTEGRITY_KEY_ALIAS, null) as KeyStore.SecretKeyEntry | ||
|
||
secretKeyEntry.secretKey | ||
} else { | ||
createIntegrityKey() | ||
} | ||
} | ||
|
||
/** | ||
* Create a new AES key in the Android KeyStore. This key will be used for | ||
* encrypting and decrypting data. The key is generated with a size of 256 bits, | ||
* using the CBC block mode and PKCS7 padding. | ||
*/ | ||
private fun createAesKey(): SecretKey { | ||
val keyGenerator = KeyGenerator | ||
.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) | ||
val keyGenParameterSpec = KeyGenParameterSpec.Builder( | ||
AES_KEY_ALIAS, | ||
PURPOSE_ENCRYPT or PURPOSE_DECRYPT | ||
) | ||
.setKeySize(KEY_SIZE_IN_BIT) | ||
.setBlockModes(KeyProperties.BLOCK_MODE_CBC) | ||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) | ||
.build() | ||
|
||
keyGenerator.run { | ||
init(keyGenParameterSpec) | ||
return generateKey() | ||
} | ||
} | ||
|
||
private fun createIntegrityKey(): SecretKey { | ||
val keyGenerator = KeyGenerator | ||
.getInstance(KeyProperties.KEY_ALGORITHM_HMAC_SHA256, ANDROID_KEY_STORE) | ||
val keyGenParameterSpec = KeyGenParameterSpec.Builder( | ||
INTEGRITY_KEY_ALIAS, | ||
PURPOSE_SIGN or PURPOSE_VERIFY | ||
) | ||
.build() | ||
|
||
keyGenerator.run { | ||
init(keyGenParameterSpec) | ||
return generateKey() | ||
} | ||
} | ||
|
||
private fun Mac.calculateHmacValue( | ||
encryptedData: ByteArray, | ||
initialVector: ByteArray | ||
): ByteArray = doFinal(encryptedData + initialVector) | ||
|
||
/** | ||
* Validate the HMAC value | ||
* | ||
* @throws SecurityException if the HMAC value doesn't match with [encryptedData] | ||
*/ | ||
private fun CipherData.verifyHmacValue(mac: Mac) { | ||
val expectedHmacValue: ByteArray = mac.calculateHmacValue( | ||
encryptedData = encryptedData, | ||
initialVector = initialVector | ||
) | ||
|
||
if (!MessageDigest.isEqual(expectedHmacValue, hmacValue)) { | ||
throw SecurityException("Cipher text has been tampered with.") | ||
} | ||
} | ||
|
||
companion object { | ||
private const val AES_KEY_ALIAS = | ||
"com.linecorp.android.security.encryption.StringAesCipher" | ||
|
||
private const val INTEGRITY_KEY_ALIAS = | ||
"com.linecorp.android.security.encryption.StringAesCipher.INTEGRITY_KEY" | ||
|
||
private const val ANDROID_KEY_STORE = "AndroidKeyStore" | ||
private const val KEY_SIZE_IN_BIT = 256 | ||
|
||
private const val TRANSFORMATION_FORMAT = | ||
KeyProperties.KEY_ALGORITHM_AES + | ||
"/${KeyProperties.BLOCK_MODE_CBC}" + | ||
"/${KeyProperties.ENCRYPTION_PADDING_PKCS7}" | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
line-sdk/src/main/java/com/linecorp/android/security/encryption/StringCipher.kt
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,12 @@ | ||
package com.linecorp.android.security.encryption | ||
|
||
import android.content.Context | ||
|
||
interface StringCipher { | ||
|
||
fun initialize(context: Context) | ||
|
||
fun encrypt(context: Context, plainText: String): String | ||
|
||
fun decrypt(context: Context, cipherText: String): String | ||
} |
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
Oops, something went wrong.