ENT-9876: Encrypting the ledger recovery participant distribution list

This commit is contained in:
Shams Asari
2023-07-25 11:58:32 +01:00
parent 6ec8855c6e
commit de67ab7377
19 changed files with 785 additions and 143 deletions

View File

@ -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)
}
}

View File

@ -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))
}
}