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