How to encrypt/decrypt with AES-GCM in Kotlin
Tags: android, kotlin, crypto, reactnative
Published: 2021-01-27 11:39:50

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](https://www.inkdrop.app/). Here is my working memo.

## Requirements

* Android >= 19

## References

* [Cipher  |  Android Developers](https://developer.android.com/reference/javax/crypto/Cipher)
* [java - Android encryption / decryption using AES - Stack Overflow](https://stackoverflow.com/questions/6788018/android-encryption-decryption-using-aes)

## Convert hexadecimal strings

First, you have to extend `ByteArray` and `String` to convert hexadecimal strings.

```kotlin
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

```kotlin
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

```kotlin
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)
    }
}

```

## See also

{% link https://dev.to/craftzdog/how-to-encrypt-decrypt-with-aes-gcm-using-cryptokit-in-swift-24h1 %}