DEV Community

Takuya Matsuyama
Takuya Matsuyama

Posted on

How to encrypt/decrypt with AES-GCM in Kotlin

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 } 
Enter fullscreen mode Exit fullscreen mode

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) } } 
Enter fullscreen mode Exit fullscreen mode

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) } } 
Enter fullscreen mode Exit fullscreen mode

See also

Top comments (1)

Collapse
 
marius_duna_25f87d6fd329d profile image
Marius Duna

WritableNativeMap - is not recognized