Skip to content

Instantly share code, notes, and snippets.

@milosmns
Created June 17, 2019 16:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save milosmns/3eb0f59bc2abb9c22eaa9437f4d79627 to your computer and use it in GitHub Desktop.
Save milosmns/3eb0f59bc2abb9c22eaa9437f4d79627 to your computer and use it in GitHub Desktop.
Encryption helper
package util
import at.favre.lib.bytes.Bytes
import java.nio.ByteBuffer
import java.security.Provider
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
/**
* Implements AES (Advanced Encryption Standard) with Galois/Counter Mode (GCM),
* which is a mode of operation for symmetric key cryptographic block ciphers
* that has been widely adopted because of its efficiency and performance.
*
* Every encryption produces a new 12 byte random IV (initialization vector)
* (see http://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf)
* because the security of GCM depends on choosing a unique initialization vector
* for every encryption performed with the same key.
*
* The IV, encrypted content and auth tag will be encoded to the following format:
* ```
* val out = byteArrayOf(x, y, y, y, y, y, y, y, y, y, y, y, y, z, z, z, ...)
* ```
*
* - x = IV length as byte
* - y = IV bytes
* - z = content bytes (encrypted content, auth tag)
*
* @author Patrick Favre-Bulle
* ([Source](https://proandroiddev.com/security-best-practices-symmetric-encryption-with-aes-in-java-7616beaaade9)),
* Milos Marinkovic (Kotlin)
*/
class Encryption(
private val secureRandom: SecureRandom = SecureRandom(),
private val provider: Provider? = null
) {
class AuthenticatedEncryptionException(message: String?, cause: Throwable?) : Throwable(message, cause)
companion object {
private const val ALGORITHM = "AES/GCM/NoPadding"
private const val TAG_LENGTH_BIT = 128
private const val IV_LENGTH_BYTE = 12
}
private var cipherWrapper: ThreadLocal<Cipher> = ThreadLocal()
@Throws(AuthenticatedEncryptionException::class, IllegalArgumentException::class)
fun encrypt(rawEncryptionKey: ByteArray, rawData: ByteArray, associatedData: ByteArray? = null): ByteArray {
if (rawEncryptionKey.size < 16) {
throw IllegalArgumentException("Key length must be longer than 16 bytes")
}
var iv: ByteArray? = null
var encrypted: ByteArray? = null
try {
iv = ByteArray(IV_LENGTH_BYTE)
secureRandom.nextBytes(iv)
val cipherEnc: Cipher = getCipher()
val keySpec = SecretKeySpec(rawEncryptionKey, "AES")
val paramSpec = GCMParameterSpec(TAG_LENGTH_BIT, iv)
cipherEnc.init(Cipher.ENCRYPT_MODE, keySpec, paramSpec)
associatedData?.let { cipherEnc.updateAAD(it) }
encrypted = cipherEnc.doFinal(rawData)
return ByteBuffer.allocate(1 + iv.size + encrypted.size).apply {
put(iv.size.toByte())
put(iv)
put(encrypted)
}.array()
} catch (error: Throwable) {
throw AuthenticatedEncryptionException("Could not encrypt", error)
} finally {
Bytes.wrapNullSafe(iv).mutable().secureWipe()
Bytes.wrapNullSafe(encrypted).mutable().secureWipe()
}
}
@Throws(AuthenticatedEncryptionException::class)
fun decrypt(rawEncryptionKey: ByteArray, encryptedData: ByteArray, associatedData: ByteArray? = null): ByteArray {
var iv: ByteArray? = null
var encrypted: ByteArray? = null
try {
val byteBuffer = ByteBuffer.wrap(encryptedData)
val ivLength = byteBuffer.get()
iv = ByteArray(ivLength.toInt())
byteBuffer.get(iv)
encrypted = ByteArray(byteBuffer.remaining())
byteBuffer.get(encrypted)
val cipherDec = getCipher()
val keySpec = SecretKeySpec(rawEncryptionKey, "AES")
val paramSpec = GCMParameterSpec(TAG_LENGTH_BIT, iv)
cipherDec.init(Cipher.DECRYPT_MODE, keySpec, paramSpec)
associatedData?.let { cipherDec.updateAAD(it) }
return cipherDec.doFinal(encrypted)
} catch (error: Throwable) {
throw AuthenticatedEncryptionException("Could not decrypt", error)
} finally {
Bytes.wrapNullSafe(iv).mutable().secureWipe()
Bytes.wrapNullSafe(encrypted).mutable().secureWipe()
}
}
@Throws(IllegalStateException::class)
private fun getCipher(): Cipher = cipherWrapper.get()?.let { it } // try getting one first
?: try {
// no cached cipher, create a new one
val cipher = provider?.let { Cipher.getInstance(ALGORITHM, it) } ?: Cipher.getInstance(ALGORITHM)
// and save it
cipherWrapper.set(cipher)
cipherWrapper.get()
} catch (error: Throwable) {
throw IllegalStateException("Could not get cipher instance", error)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment