AndroidAPS/app/src/main/java/info/nightscout/androidaps/utils/CryptoUtil.kt
Dominik Dzienia 9dca938698 Refactored CryptoUtil to make it injectable and more testable
Added checks for vanilla JVM AES 256 issues in tests
2020-04-06 23:48:25 +02:00

157 lines
5.6 KiB
Kotlin

package info.nightscout.androidaps.utils
import info.nightscout.androidaps.logging.AAPSLogger
import org.spongycastle.util.encoders.Base64
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.spec.KeySpec
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
import javax.inject.Singleton
private val HEX_CHARS = "0123456789abcdef"
private val HEX_CHARS_ARRAY = "0123456789abcdef".toCharArray()
fun String.hexStringToByteArray() : ByteArray {
val upperCased = this.toLowerCase()
val result = ByteArray(length / 2)
for (i in 0 until length step 2) {
val firstIndex = HEX_CHARS.indexOf(upperCased[i]);
val secondIndex = HEX_CHARS.indexOf(upperCased[i + 1]);
val octet = firstIndex.shl(4).or(secondIndex)
result.set(i.shr(1), octet.toByte())
}
return result
}
fun ByteArray.toHex() : String{
val result = StringBuffer()
forEach {
val octet = it.toInt()
val firstIndex = (octet and 0xF0).ushr(4)
val secondIndex = octet and 0x0F
result.append(HEX_CHARS_ARRAY[firstIndex])
result.append(HEX_CHARS_ARRAY[secondIndex])
}
return result.toString()
}
@Singleton
class CryptoUtil @Inject constructor(
val aapsLogger: AAPSLogger
) {
companion object {
private const val IV_LENGTH_BYTE = 12
private const val TAG_LENGTH_BIT = 128
private const val AES_KEY_SIZE_BIT = 256
private const val PBKDF2_ITERATIONS = 50000 // check delays it cause on real device
private const val SALT_SIZE_BYTE = 32
}
private val secureRandom: SecureRandom = SecureRandom()
var lastException: Exception? = null
fun sha256(source: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashRaw = digest.digest(source.toByteArray())
return hashRaw.toHex()
}
fun hmac256(str: String, secret: String): String? {
val sha256_HMAC = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(secret.toByteArray(), "HmacSHA256")
sha256_HMAC.init(secretKey)
return sha256_HMAC.doFinal(str.toByteArray()).toHex()
}
private fun prepCipherKey(passPhrase: String, salt:ByteArray, iterationCount:Int = PBKDF2_ITERATIONS, keyStrength:Int = AES_KEY_SIZE_BIT): SecretKeySpec {
val factory: SecretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1")
val spec: KeySpec = PBEKeySpec(passPhrase.toCharArray(), salt, iterationCount, keyStrength)
val tmp: SecretKey = factory.generateSecret(spec)
return SecretKeySpec(tmp.getEncoded(), "AES")
}
fun mineSalt(len :Int = SALT_SIZE_BYTE): ByteArray {
val salt = ByteArray(len)
secureRandom.nextBytes(salt)
return salt
}
fun encrypt(passPhrase: String, salt:ByteArray, rawData: String ): String? {
val iv: ByteArray?
val encrypted: ByteArray?
return try {
lastException = null
iv = ByteArray(IV_LENGTH_BYTE)
secureRandom.nextBytes(iv)
val cipherEnc: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipherEnc.init(Cipher.ENCRYPT_MODE, prepCipherKey(passPhrase, salt), GCMParameterSpec(TAG_LENGTH_BIT, iv))
encrypted = cipherEnc.doFinal(rawData.toByteArray())
val byteBuffer: ByteBuffer = ByteBuffer.allocate(1 + iv.size + encrypted.size)
byteBuffer.put(iv.size.toByte())
byteBuffer.put(iv)
byteBuffer.put(encrypted)
String(Base64.encode(byteBuffer.array()))
} catch (e: Exception) {
lastException = e
aapsLogger.error("Encryption failed due to technical exception: ${e}")
null
}
}
fun decrypt(passPhrase: String, salt:ByteArray, encryptedData: String): String? {
val iv: ByteArray?
val encrypted: ByteArray?
return try {
lastException = null
val byteBuffer = ByteBuffer.wrap(Base64.decode(encryptedData))
val ivLength = byteBuffer.get().toInt()
iv = ByteArray(ivLength)
byteBuffer[iv]
encrypted = ByteArray(byteBuffer.remaining())
byteBuffer[encrypted]
val cipherDec: Cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipherDec.init(Cipher.DECRYPT_MODE, prepCipherKey(passPhrase, salt), GCMParameterSpec(TAG_LENGTH_BIT, iv))
val dec = cipherDec.doFinal(encrypted)
String(dec)
} catch (e: Exception) {
lastException = e
aapsLogger.error("Decryption failed due to technical exception: ${e}")
null
}
}
fun checkPassword(password: String, referenceHash: String): Boolean {
return if (referenceHash.startsWith("hmac:")) {
val hashSegments = referenceHash.split(":")
if (hashSegments.size != 3)
return false
return hmac256(password, hashSegments[1]) == hashSegments[2]
} else {
password == referenceHash
}
}
fun hashPassword(password: String): String {
return if (!password.startsWith("hmac:")) {
val salt = mineSalt().toHex()
return "hmac:${salt}:${hmac256(password, salt)}"
} else {
password
}
}
}