Created
June 17, 2019 16:44
-
-
Save milosmns/3eb0f59bc2abb9c22eaa9437f4d79627 to your computer and use it in GitHub Desktop.
Encryption helper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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