ApiCleCrypto.kt 80 lignes · 2989 octets
package fr.ebii.card2vcf.sync

import org.bouncycastle.crypto.generators.Argon2BytesGenerator
import org.bouncycastle.crypto.modes.XChaCha20Poly1305
import org.bouncycastle.crypto.params.AEADParameters
import org.bouncycastle.crypto.params.Argon2Parameters
import org.bouncycastle.crypto.params.KeyParameter
import java.util.Base64

class ApiCleCryptoException(message: String, cause: Throwable? = null) : Exception(message, cause)

object ApiCleCrypto {
    private const val NONCE_LEN = 24
    private const val TAG_LEN = 16
    private const val KEY_LEN = 32
    private const val SALT_LEN = 16
    private const val ARGON2_M_KB = 19456
    private const val ARGON2_T = 2
    private const val ARGON2_P = 1
    private const val MAC_BITS = 128

    fun decryptApiKey(password: String, saltB64: String, cipherB64: String): String {
        val salt = decodeB64(saltB64, "sel")
        if (salt.size != SALT_LEN) {
            throw ApiCleCryptoException("sel invalide")
        }

        val blob = decodeB64(cipherB64, "cle_chiffree")
        if (blob.size < NONCE_LEN + TAG_LEN) {
            throw ApiCleCryptoException("blob trop court")
        }

        val key = deriveKey(password, salt)
        val nonce = blob.copyOfRange(0, NONCE_LEN)
        val ciphertext = blob.copyOfRange(NONCE_LEN, blob.size)

        val plaintext = try {
            decryptXChaCha20Poly1305(key, nonce, ciphertext)
        } catch (e: Exception) {
            throw ApiCleCryptoException("échec déchiffrement clé API", e)
        }

        return plaintext.toString(Charsets.UTF_8)
    }

    private fun deriveKey(password: String, salt: ByteArray): ByteArray {
        val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
            .withVersion(Argon2Parameters.ARGON2_VERSION_13)
            .withIterations(ARGON2_T)
            .withMemoryAsKB(ARGON2_M_KB)
            .withParallelism(ARGON2_P)
            .withSalt(salt)
            .build()
        val generator = Argon2BytesGenerator()
        generator.init(params)
        val key = ByteArray(KEY_LEN)
        generator.generateBytes(password.toByteArray(Charsets.UTF_8), key)
        return key
    }

    private fun decryptXChaCha20Poly1305(key: ByteArray, nonce24: ByteArray, ciphertext: ByteArray): ByteArray {
        require(nonce24.size == NONCE_LEN)
        val cipher = XChaCha20Poly1305()
        cipher.init(
            false,
            AEADParameters(KeyParameter(key), MAC_BITS, nonce24, null),
        )
        val output = ByteArray(cipher.getOutputSize(ciphertext.size))
        var len = cipher.processBytes(ciphertext, 0, ciphertext.size, output, 0)
        len += cipher.doFinal(output, len)
        return output.copyOf(len)
    }

    private fun decodeB64(value: String, label: String): ByteArray =
        try {
            Base64.getDecoder().decode(value)
        } catch (e: IllegalArgumentException) {
            throw ApiCleCryptoException("$label base64 invalide", e)
        }
}