How to decrypt a ciphertext locally using Java / Kotlin from an exported key that supported convergent encryption in hashicorp vault?

Viewed 77

I have a usecase where I would want to keep a backup of the encryption keys in case the vault server goes down and decrypt it locally. I am using Hashicorp Transit Secrets Engine, and to test it out I am using Vault Test Container with Spring Vault. I have managed to locally decrypt non-convergent keys but have not been able to figure out how to do that for convergent keys. I am not a crypto expert so I might be missing some basic understanding.

Below is the code that is working for non-convergent keys

    @Test
    fun backUpAndDecrypt(){
        val dataKeyName = "test-key"
        val dataToEncrypt = "Customer Password"

        val vTemp = VaultTemplate(
            VaultEndpoint.from(URI.create("http://" + vaultContainer?.host + ":" + vaultContainer?.firstMappedPort)),
            TokenAuthentication("my-root-token")
        )

        val transitOperations: VaultTransitOperations = vTemp.opsForTransit()
        transitOperations.createKey(
            dataKeyName,
            VaultTransitKeyCreationRequest.builder().exportable(true).build()
        )

        val ciphertext = transitOperations.encrypt(
            dataKeyName,
            dataToEncrypt
        )

        val keyBackups = transitOperations.exportKey(dataKeyName, TransitKeyType.ENCRYPTION_KEY)
        
        assertThat(keyBackups.keys.get("1")?.let { decrypt(ciphertext.substring(9), it) }).isEqualTo(dataToEncrypt)
    }

    @Throws(GeneralSecurityException::class)
    fun decrypt(cyphertext: String?, encodedKey: String): String? {
        val GCM_TAG_SIZE_BITS = 128
        val GCM_IV_SIZE_BYTES = 12

        val decodedKey: ByteArray = Base64.getDecoder().decode(encodedKey)
        val originalKey: SecretKey = SecretKeySpec(decodedKey, 0, decodedKey.size, "AES")

        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val ivAndCTWithTag = Base64.getDecoder().decode(cyphertext)
        val spec = GCMParameterSpec(GCM_TAG_SIZE_BITS, ivAndCTWithTag, 0, GCM_IV_SIZE_BYTES)
        cipher.init(Cipher.DECRYPT_MODE, originalKey, spec)
        val plaintext = cipher.doFinal(ivAndCTWithTag, GCM_IV_SIZE_BYTES, ivAndCTWithTag.size - GCM_IV_SIZE_BYTES)
        return String(plaintext, Charset.defaultCharset())
    }

Now there is a scenario in which I need to pass a context and generate a convergent ciphertext. The question is how to decrypt it locally. Below is the stub which is to be populated

    @Test
    fun backUpAndDecryptConvergent(){
        val convergentDataKeyName = "test-convergent-key"
        val dataToEncrypt = "Customer Password"
        val contextOne = "{\"service\": \"customers\", \"id\": \"some-customer-uuid\"}".toByteArray()

        val vTemp = VaultTemplate(
            VaultEndpoint.from(URI.create("http://" + vaultContainer?.host + ":" + vaultContainer?.firstMappedPort)),
            TokenAuthentication("my-root-token")
        )

        val transitOperations: VaultTransitOperations = vTemp.opsForTransit()
        transitOperations.createKey(
            convergentDataKeyName,
            VaultTransitKeyCreationRequest.builder().convergentEncryption(true).derived(true).exportable(true).build()
        )

        val convergentCiphertext = transitOperations.encrypt(
            convergentDataKeyName,
            dataToEncrypt.toByteArray(),
            VaultTransitContext.fromContext(contextOne)
        )

        val keyBackups = transitOperations.exportKey(convergentDataKeyName, TransitKeyType.ENCRYPTION_KEY)

        assertThat(keyBackups.keys.get("1")?.let { decrypt(convergentCiphertext.substring(9), it) }).isEqualTo(dataToEncrypt)
    }

The only thing available on the official page is that

Version 3 uses a different algorithm designed to be resistant to offline plaintext-confirmation attacks. It is similar to AES-SIV in that it uses a PRF to generate the nonce from the plaintext.

0 Answers
Related