Skip to content

Instantly share code, notes, and snippets.

@diegojfer
Last active July 10, 2024 23:34
Show Gist options
  • Save diegojfer/a5ccf3848967aceccb8fe8e111572917 to your computer and use it in GitHub Desktop.
Save diegojfer/a5ccf3848967aceccb8fe8e111572917 to your computer and use it in GitHub Desktop.
Redsys Signature Service for Kotlin - Generate Parameter Signature
package com.codanbaru.service
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.macs.HMac
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.util.encoders.Base64
import java.security.Key
import java.security.Provider
import java.security.Security
import java.security.spec.AlgorithmParameterSpec
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.DESedeKeySpec
import javax.crypto.spec.IvParameterSpec
class RedsysSignatureService(
secret: String
) {
data class Result(
val signatureVersion: String,
val merchantParameters: String,
val signature: String,
)
private val secretBytes: ByteArray
private val vectorBytes: ByteArray = ByteArray(8) { 0 }
sealed class Exception(message: String?, cause: Throwable?): Throwable(message, cause) {
class InvalidSecret(cause: Throwable? = null): Exception(message = "Invalid Redsys secret.", cause)
class InvalidParameters(cause: Throwable? = null): Exception(message = "Invalid Redsys parameters.", cause)
class InvalidOrder(cause: Throwable? = null): Exception(message = "Invalid Redsys merchant order.", cause)
class Unknown(cause: Throwable): Exception(message = "Unknown exception. See cause.", cause)
}
init {
val secretBytes: ByteArray = try { Base64.decode(secret) } catch (throwable: Throwable) { throw RedsysSignatureService.Exception.InvalidSecret(throwable) }
if (secretBytes.size < 24) throw Exception.InvalidSecret()
this.secretBytes = if (secretBytes.size == 24) { secretBytes } else { secretBytes.sliceArray(0..23) }
}
private fun ensureBouncyCastleIsAvailable() {
val bcp: Provider? = Security.getProviders().firstOrNull { it.name == BouncyCastleProvider.PROVIDER_NAME }
if (bcp == null) Security.addProvider(BouncyCastleProvider())
}
private fun encrypt(bytes: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
ensureBouncyCastleIsAvailable()
if (bytes.size % 8 != 0) throw IllegalArgumentException("Invalid bytes padding.")
if (key.size != 24) throw IllegalArgumentException("Invalid key size.")
if (iv.size != 8) throw IllegalArgumentException("Invalid iv size.")
val keySpec = DESedeKeySpec(key)
val ivSpec = IvParameterSpec(iv)
val keyFactory = SecretKeyFactory.getInstance("DESede", BouncyCastleProvider.PROVIDER_NAME)
val keyO: Key = keyFactory.generateSecret(keySpec)
val ivO: AlgorithmParameterSpec = ivSpec
val cipher: Cipher = Cipher.getInstance("DESede/CBC/NoPadding", BouncyCastleProvider.PROVIDER_NAME)
cipher.init(Cipher.ENCRYPT_MODE, keyO, ivO)
return cipher.doFinal(bytes)
}
private fun sign(bytes: ByteArray, key: ByteArray): ByteArray {
ensureBouncyCastleIsAvailable()
val keyO = KeyParameter(key)
val digest = SHA256Digest()
val hmac = HMac(digest)
hmac.init(keyO)
hmac.update(bytes, 0, bytes.size)
val result = ByteArray(hmac.macSize) { 0 }
hmac.doFinal(result, 0)
return result
}
private fun ByteArray.padded(): ByteArray {
val blockSize = 8
val paddingBytesSize = if(size % blockSize == 0) { 0 } else { blockSize - (size % blockSize) }
val paddingBytes = ByteArray(paddingBytesSize)
return plus(paddingBytes)
}
fun create(parameters: JsonElement): Result {
val parametersElement: JsonElement = parameters
val parametersObject: JsonObject = if (parametersElement is JsonObject) { parametersElement } else { throw Exception.InvalidParameters() }
val orderKey = parametersObject.keys.sorted().firstOrNull { it.uppercase() == "DS_MERCHANT_ORDER" || it.uppercase() == "DS_ORDER" } ?: throw Exception.InvalidOrder()
val orderElement: JsonElement = parametersObject[orderKey] ?: throw Exception.InvalidOrder()
val orderPrimitive: JsonPrimitive = if (orderElement is JsonPrimitive) { orderElement } else { throw Exception.InvalidOrder() }
val order: String = if (orderPrimitive.isString) { orderPrimitive.content } else { throw Exception.InvalidOrder() }
if (order.length < 4) throw Exception.InvalidOrder()
if (order.length > 12) throw Exception.InvalidOrder()
val orderBytes = order.toByteArray(Charsets.UTF_8).padded()
val derivedOrderBytes = try { encrypt(orderBytes, secretBytes, vectorBytes) } catch (throwable: Throwable) { throw Exception.Unknown(throwable) }
val serializedParameters = Json.encodeToString(parameters)
val serializedParameterBytes: ByteArray = serializedParameters.toByteArray(Charsets.UTF_8)
val serializedParameterBase64: String = Base64.encode(serializedParameterBytes).toString(Charsets.UTF_8)
/* FIXME: Redsys strange data conversion!? */ val serializedParameterBase64Fixed: ByteArray = Base64.encode(serializedParameterBytes)
val signatureBytes = try { sign(serializedParameterBase64Fixed, derivedOrderBytes) } catch (throwable: Throwable) { throw Exception.Unknown(throwable) }
val signature = Base64.encode(signatureBytes).toString(Charsets.UTF_8)
return Result(
signatureVersion = "HMAC_SHA256_V1",
merchantParameters = serializedParameterBase64,
signature = signature
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment