Last active
July 10, 2024 23:34
-
-
Save diegojfer/a5ccf3848967aceccb8fe8e111572917 to your computer and use it in GitHub Desktop.
Redsys Signature Service for Kotlin - Generate Parameter Signature
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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