I'm developing a native module for React Native that allows you to encrypt/decrypt data with AES-GCM for my Markdown note-taking app. Here is my working memo.
Requirements
- Android >= 19
References
Convert hexadecimal strings
First, you have to extend ByteArray
and String
to convert hexadecimal strings.
private val HEX_CHARS_STR = "0123456789abcdef"
private val HEX_CHARS = HEX_CHARS_STR.toCharArray()
fun ByteArray.toHex() : String{
val result = StringBuffer()
forEach {
val st = String.format("%02x", it)
result.append(st)
}
return result.toString()
}
fun String.hexStringToByteArray() : ByteArray {
val result = ByteArray(length / 2)
for (i in 0 until length step 2) {
val firstIndex = HEX_CHARS_STR.indexOf(this[i]);
val secondIndex = HEX_CHARS_STR.indexOf(this[i + 1]);
val octet = firstIndex.shl(4).or(secondIndex)
result.set(i.shr(1), octet.toByte())
}
return result
}
Encrypt
class EncryptionOutput(val iv: ByteArray,
val tag: ByteArray,
val ciphertext: ByteArray)
fun getSecretKeyFromString(key: ByteArray): SecretKey {
return SecretKeySpec(key, 0, key.size, "AES")
}
fun encryptData(plainData: ByteArray, key: ByteArray): EncryptionOutput {
val secretKey: SecretKey = getSecretKeyFromString(key)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv.copyOf()
val result = cipher.doFinal(plainData)
val ciphertext = result.copyOfRange(0, result.size - GCM_TAG_LENGTH)
val tag = result.copyOfRange(result.size - GCM_TAG_LENGTH, result.size)
return EncryptionOutput(iv, tag, ciphertext)
}
fun encrypt(plainText: String,
inBinary: Boolean,
key: String,
promise: Promise) {
try {
val keyData = Base64.getDecoder().decode(key)
val plainData = if (inBinary) Base64.getDecoder().decode(plainText) else plainText.toByteArray(Charsets.UTF_8)
val sealed = encryptData(plainData, keyData)
var response = WritableNativeMap()
response.putString("iv", sealed.iv.toHex())
response.putString("tag", sealed.tag.toHex())
response.putString("content", Base64.getEncoder().encodeToString(sealed.ciphertext))
promise.resolve(response)
} catch (e: GeneralSecurityException) {
promise.reject("EncryptionError", "Failed to encrypt", e)
} catch (e: Exception) {
promise.reject("EncryptionError", "Unexpected error", e)
}
}
Decrypt
fun decryptData(ciphertext: ByteArray, key: ByteArray, iv: String, tag: String): ByteArray {
val secretKey: SecretKey = getSecretKeyFromString(key)
val ivData = iv.hexStringToByteArray()
val tagData = tag.hexStringToByteArray()
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val spec = GCMParameterSpec(GCM_TAG_LENGTH * 8, ivData)
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
return cipher.doFinal(ciphertext + tagData)
}
fun decrypt(base64CipherText: String,
key: String,
iv: String,
tag: String,
isBinary: Boolean,
promise: Promise) {
try {
val keyData = Base64.getDecoder().decode(key)
val ciphertext: ByteArray = Base64.getDecoder().decode(base64CipherText)
val unsealed: ByteArray = decryptData(ciphertext, keyData, iv, tag)
if (isBinary) {
promise.resolve(Base64.getEncoder().encodeToString(unsealed))
} else {
promise.resolve(unsealed.toString(Charsets.UTF_8))
}
} catch (e: javax.crypto.AEADBadTagException) {
promise.reject("DecryptionError", "Bad auth tag exception", e)
} catch (e: GeneralSecurityException) {
promise.reject("DecryptionError", "Failed to decrypt", e)
} catch (e: Exception) {
promise.reject("DecryptionError", "Unexpected error", e)
}
}