mirror of
https://github.com/corda/corda.git
synced 2025-06-17 22:58:19 +00:00
ENT-9876: Encrypting the ledger recovery participant distribution list
This commit is contained in:
@ -0,0 +1,65 @@
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.crypto.secureRandomBytes
|
||||
import java.nio.ByteBuffer
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
object AesEncryption {
|
||||
const val KEY_SIZE_BYTES = 16
|
||||
internal const val IV_SIZE_BYTES = 12
|
||||
private const val TAG_SIZE_BYTES = 16
|
||||
private const val TAG_SIZE_BITS = TAG_SIZE_BYTES * 8
|
||||
|
||||
/**
|
||||
* Generates a random 128-bit AES key.
|
||||
*/
|
||||
fun randomKey(): SecretKey {
|
||||
return SecretKeySpec(secureRandomBytes(KEY_SIZE_BYTES), "AES")
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given [plaintext] with AES using the given [aesKey].
|
||||
*
|
||||
* An optional public [additionalData] bytes can also be provided which will be authenticated alongside the ciphertext but not encrypted.
|
||||
* This may be metadata for example. The same authenticated data bytes must be provided to [decrypt] to be able to decrypt the
|
||||
* ciphertext. Typically these bytes are serialised alongside the ciphertext. Since it's authenticated in the ciphertext, it cannot be
|
||||
* modified undetected.
|
||||
*/
|
||||
fun encrypt(aesKey: SecretKey, plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray {
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
val iv = secureRandomBytes(IV_SIZE_BYTES) // Never use the same IV with the same key!
|
||||
cipher.init(Cipher.ENCRYPT_MODE, aesKey, GCMParameterSpec(TAG_SIZE_BITS, iv))
|
||||
val buffer = ByteBuffer.allocate(IV_SIZE_BYTES + plaintext.size + TAG_SIZE_BYTES)
|
||||
buffer.put(iv)
|
||||
if (additionalData != null) {
|
||||
cipher.updateAAD(additionalData)
|
||||
}
|
||||
cipher.doFinal(ByteBuffer.wrap(plaintext), buffer)
|
||||
return buffer.array()
|
||||
}
|
||||
|
||||
fun encrypt(aesKey: ByteArray, plaintext: ByteArray, additionalData: ByteArray? = null): ByteArray {
|
||||
return encrypt(SecretKeySpec(aesKey, "AES"), plaintext, additionalData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt ciphertext that was encrypted with the same key using [encrypt].
|
||||
*
|
||||
* If additional data was used for the encryption then it must also be provided. If doesn't match then the decryption will fail.
|
||||
*/
|
||||
fun decrypt(aesKey: SecretKey, ciphertext: ByteArray, additionalData: ByteArray? = null): ByteArray {
|
||||
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
|
||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, GCMParameterSpec(TAG_SIZE_BITS, ciphertext, 0, IV_SIZE_BYTES))
|
||||
if (additionalData != null) {
|
||||
cipher.updateAAD(additionalData)
|
||||
}
|
||||
return cipher.doFinal(ciphertext, IV_SIZE_BYTES, ciphertext.size - IV_SIZE_BYTES)
|
||||
}
|
||||
|
||||
fun decrypt(aesKey: ByteArray, ciphertext: ByteArray, additionalData: ByteArray? = null): ByteArray {
|
||||
return decrypt(SecretKeySpec(aesKey, "AES"), ciphertext, additionalData)
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
package net.corda.nodeapi.internal.crypto
|
||||
|
||||
import net.corda.core.crypto.secureRandomBytes
|
||||
import net.corda.nodeapi.internal.crypto.AesEncryption.IV_SIZE_BYTES
|
||||
import net.corda.nodeapi.internal.crypto.AesEncryption.KEY_SIZE_BYTES
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import java.security.GeneralSecurityException
|
||||
|
||||
class AesEncryptionTest {
|
||||
private val aesKey = secureRandomBytes(KEY_SIZE_BYTES)
|
||||
private val plaintext = secureRandomBytes(257) // Intentionally not a power of 2
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ciphertext can be decrypted using the same key`() {
|
||||
val ciphertext = AesEncryption.encrypt(aesKey, plaintext)
|
||||
assertThat(String(ciphertext)).doesNotContain(String(plaintext))
|
||||
val decrypted = AesEncryption.decrypt(aesKey, ciphertext)
|
||||
assertThat(decrypted).isEqualTo(plaintext)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ciphertext with authenticated data can be decrypted using the same key`() {
|
||||
val ciphertext = AesEncryption.encrypt(aesKey, plaintext, "Extra public data".toByteArray())
|
||||
assertThat(String(ciphertext)).doesNotContain(String(plaintext))
|
||||
val decrypted = AesEncryption.decrypt(aesKey, ciphertext, "Extra public data".toByteArray())
|
||||
assertThat(decrypted).isEqualTo(plaintext)
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ciphertext cannot be decrypted with different authenticated data`() {
|
||||
val ciphertext = AesEncryption.encrypt(aesKey, plaintext, "Extra public data".toByteArray())
|
||||
assertThat(String(ciphertext)).doesNotContain(String(plaintext))
|
||||
assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy {
|
||||
AesEncryption.decrypt(aesKey, ciphertext, "Different public data".toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `ciphertext cannot be decrypted with different key`() {
|
||||
val ciphertext = AesEncryption.encrypt(aesKey, plaintext)
|
||||
for (index in aesKey.indices) {
|
||||
aesKey[index]--
|
||||
assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy {
|
||||
AesEncryption.decrypt(aesKey, ciphertext)
|
||||
}
|
||||
aesKey[index]++
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `corrupted ciphertext cannot be decrypted`() {
|
||||
val ciphertext = AesEncryption.encrypt(aesKey, plaintext)
|
||||
for (index in ciphertext.indices) {
|
||||
ciphertext[index]--
|
||||
assertThatExceptionOfType(GeneralSecurityException::class.java).isThrownBy {
|
||||
AesEncryption.decrypt(aesKey, ciphertext)
|
||||
}
|
||||
ciphertext[index]++
|
||||
}
|
||||
}
|
||||
|
||||
@Test(timeout = 300_000)
|
||||
fun `encrypting same plainttext twice with same key does not produce same ciphertext`() {
|
||||
val first = AesEncryption.encrypt(aesKey, plaintext)
|
||||
val second = AesEncryption.encrypt(aesKey, plaintext)
|
||||
// The IV should be different
|
||||
assertThat(first.take(IV_SIZE_BYTES)).isNotEqualTo(second.take(IV_SIZE_BYTES))
|
||||
// Which should cause the encrypted bytes to be different as well
|
||||
assertThat(first.drop(IV_SIZE_BYTES)).isNotEqualTo(second.drop(IV_SIZE_BYTES))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user