libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 7769c74a2f4e756ebf8f1ca79a885810ebc75765
parent b5f49fe4dc38c8e4f818fc45eba7e1264160f581
Author: MS <ms@taler.net>
Date:   Wed, 25 Oct 2023 13:21:49 +0200

EBICS (3) upload doer and helpers.

Diffstat:
Anexus/src/main/kotlin/tech/libeufin/nexus/Ebics3.kt | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt | 142++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mutil/src/main/kotlin/Ebics.kt | 54++----------------------------------------------------
3 files changed, 230 insertions(+), 57 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics3.kt @@ -0,0 +1,90 @@ +package tech.libeufin.nexus + +import tech.libeufin.util.PreparedUploadData +import tech.libeufin.util.XMLUtil +import tech.libeufin.util.ebics_h005.Ebics3Request +import tech.libeufin.util.getNonce +import tech.libeufin.util.toHexString +import java.math.BigInteger +import java.util.* +import javax.xml.datatype.DatatypeFactory + +/** + * Creates the EBICS 3 document for the init phase of an upload + * transaction. + * + * @param cfg configuration handle. + * @param preparedUploadData business payload to send. + * @param bankkeys bank public keys. + * @param clientKeys client private keys. + * @param orderService EBICS 3 document defining the request type + * @return raw XML of the EBICS 3 init phase. + */ +fun createEbics3RequestForUploadInitialization( + cfg: EbicsSetupConfig, + preparedUploadData: PreparedUploadData, + bankkeys: BankPublicKeysFile, + clientKeys: ClientPrivateKeysFile, + orderService: Ebics3Request.OrderDetails.Service +): String { + val nonce = getNonce(128) + val req = Ebics3Request.createForUploadInitializationPhase( + preparedUploadData.transactionKey, + preparedUploadData.userSignatureDataEncrypted, + preparedUploadData.dataDigest, + cfg.ebicsHostId, + nonce, + cfg.ebicsPartnerId, + cfg.ebicsUserId, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + bankkeys.bank_authentication_public_key, + bankkeys.bank_encryption_public_key, + BigInteger.ONE, + orderService + ) + val doc = XMLUtil.convertJaxbToDocument( + req, + withSchemaLocation = "urn:org:ebics:H005 ebics_request_H005.xsd" + ) + tech.libeufin.util.logger.debug("Created EBICS 3 document for upload initialization," + + " nonce: ${nonce.toHexString()}") + XMLUtil.signEbicsDocument( + doc, + clientKeys.authentication_private_key, + withEbics3 = true + ) + return XMLUtil.convertDomToString(doc) +} + +/** + * Crafts one EBICS 3 request for the upload transfer phase. Currently + * only 1-chunk payloads are supported. + * + * @param cfg configuration handle. + * @param clientKeys client private keys. + * @param transactionId EBICS transaction ID obtained from an init phase. + * @param uploadData business content to upload. + * + * @return raw XML document. + */ +fun createEbics3RequestForUploadTransferPhase( + cfg: EbicsSetupConfig, + clientKeys: ClientPrivateKeysFile, + transactionId: String, + uploadData: PreparedUploadData +): String { + val chunkIndex = 1 // only 1-chunk communication currently supported. + val req = Ebics3Request.createForUploadTransferPhase( + cfg.ebicsHostId, + transactionId, + BigInteger.valueOf(chunkIndex.toLong()), + uploadData.encryptedPayloadChunks[chunkIndex] + ) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument( + doc, + clientKeys.authentication_private_key, + withEbics3 = true + ) + return XMLUtil.convertDomToString(doc) +} +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt @@ -42,15 +42,16 @@ import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* -import tech.libeufin.nexus.BankPublicKeysFile -import tech.libeufin.nexus.ClientPrivateKeysFile -import tech.libeufin.nexus.EbicsSetupConfig +import tech.libeufin.nexus.* import tech.libeufin.util.* +import tech.libeufin.util.ebics_h005.Ebics3Request +import tech.libeufin.util.logger import java.io.ByteArrayOutputStream import java.security.interfaces.RSAPrivateCrtKey import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* +import java.util.zip.DeflaterInputStream /** * Decrypts and decompresses the business payload that was @@ -257,7 +258,7 @@ suspend fun postEbicsAndCheckReturnCodes( return respObj } /** - * Collects all the steps of an EBICS download transaction. Namely + * Collects all the steps of an EBICS download transaction. Namely, * it conducts: init -> transfer -> receipt phases. * * @param client HTTP client for POSTing to the bank. @@ -279,7 +280,7 @@ suspend fun doEbicsDownload( ): String? { val initResp = postEbicsAndCheckReturnCodes(client, cfg, bankKeys, reqXml, isEbics3) if (initResp == null) { - tech.libeufin.nexus.logger.error("Could not get past the EBICS init phase, failing.") + tech.libeufin.nexus.logger.error("EBICS download: could not get past the EBICS init phase, failing.") return null } val howManySegments = initResp.numSegments @@ -381,4 +382,135 @@ fun parseAndValidateEbicsResponse( if (withEbics3) return ebics3toInternalRepr(responseStr) return ebics25toInternalRepr(responseStr) +} + +/** + * Signs and the encrypts the data to send via EBICS. + * + * @param cfg configuration handle. + * @param clientKeys client keys. + * @param bankKeys bank keys. + * @param payload business payload to send to the bank, typically ISO20022. + * @param isEbics3 true if the payload travels on EBICS 3. + * @return [PreparedUploadData] + */ +fun prepareUloadPayload( + cfg: EbicsSetupConfig, + clientKeys: ClientPrivateKeysFile, + bankKeys: BankPublicKeysFile, + payload: ByteArray, + isEbics3: Boolean +): PreparedUploadData { + val encryptionResult: CryptoUtil.EncryptionResult = if (isEbics3) { + val innerSignedEbicsXml = signOrderEbics3( // A006 signature. + payload, + clientKeys.signature_private_key, + cfg.ebicsPartnerId, + cfg.ebicsUserId + ) + val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002( + EbicsOrderUtil.encodeOrderDataXml(innerSignedEbicsXml), + bankKeys.bank_encryption_public_key + ) + userSignatureDataEncrypted + } else { + val innerSignedEbicsXml = signOrder( // A006 signature. + payload, + clientKeys.signature_private_key, + cfg.ebicsPartnerId, + cfg.ebicsUserId + ) + val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002( + EbicsOrderUtil.encodeOrderDataXml(innerSignedEbicsXml), + bankKeys.bank_encryption_public_key + ) + userSignatureDataEncrypted + } + val plainTransactionKey = encryptionResult.plainTransactionKey + if (plainTransactionKey == null) + throw Exception("Could not generate the transaction key, cannot encrypt the payload!") + // Then only E002 symmetric (with ephemeral key) encrypt. + val compressedInnerPayload = DeflaterInputStream( + payload.inputStream() + ).use { it.readAllBytes() } + val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( + compressedInnerPayload, + bankKeys.bank_encryption_public_key, + plainTransactionKey + ) + val encodedEncryptedPayload = Base64.getEncoder().encodeToString(encryptedPayload.encryptedData) + + return PreparedUploadData( + encryptionResult.encryptedTransactionKey, // ephemeral key + encryptionResult.encryptedData, // bank-pub-encrypted A006 signature. + CryptoUtil.digestEbicsOrderA006(payload), // used by EBICS 3 + listOf(encodedEncryptedPayload) // actual payload E002 encrypted. + ) +} + +/** + * Collects all the steps of an EBICS 3 upload transaction. + * NOTE: this function could conveniently be reused for an EBICS 2.x + * transaction, hence this function stays in this file. + * + * @param client HTTP client for POSTing to the bank. + * @param cfg configuration handle. + * @param clientKeys client EBICS private keys. + * @param bankKeys bank EBICS public keys. + * @param payload binary business paylaod. + * @return [EbicsResponseContent] or null upon errors. + */ +suspend fun doEbicsUpload( + client: HttpClient, + cfg: EbicsSetupConfig, + clientKeys: ClientPrivateKeysFile, + bankKeys: BankPublicKeysFile, + orderService: Ebics3Request.OrderDetails.Service, + payload: ByteArray +): EbicsResponseContent? { + val preparedPayload = prepareUloadPayload(cfg, clientKeys, bankKeys, payload, isEbics3 = true) + val initXml = createEbics3RequestForUploadInitialization( + cfg, + preparedPayload, + bankKeys, + clientKeys, + orderService + ) + val initResp = postEbicsAndCheckReturnCodes( + client, + cfg, + bankKeys, + initXml, + isEbics3 = true + ) + if (initResp == null) { + tech.libeufin.nexus.logger.error("EBICS upload init phase failed.") + return null + } + + // Init phase OK, proceeding with the transfer phase. + val tId = initResp.transactionID + if (tId == null) { + logger.error("EBICS upload init phase did not return a transaction ID, cannot do the transfer phase.") + return null + } + val transferXml = createEbics3RequestForUploadTransferPhase( + cfg, + clientKeys, + tId, + preparedPayload + ) + val transferResp = postEbicsAndCheckReturnCodes( + client, + cfg, + bankKeys, + initXml, + isEbics3 = true + ) + if (transferResp == null) { + tech.libeufin.nexus.logger.error("EBICS transfer phase failed.") + return null + } + // EBICS- and bank-technical codes were both EBICS_OK, success! + return transferResp } \ No newline at end of file diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -156,7 +156,7 @@ fun makeEbics3DateRange(ebicsDateRange: EbicsDateRange?): Ebics3Request.DateRang else null } -private fun signOrder( +fun signOrder( orderBlob: ByteArray, signKey: RSAPrivateCrtKey, partnerId: String, @@ -179,7 +179,7 @@ private fun signOrder( return userSignatureData } -private fun signOrderEbics3( +fun signOrderEbics3( orderBlob: ByteArray, signKey: RSAPrivateCrtKey, partnerId: String, @@ -256,56 +256,6 @@ data class PreparedUploadData( } } -fun prepareUploadPayload( - subscriberDetails: EbicsClientSubscriberDetails, - payload: ByteArray, - isEbics3: Boolean = false -): PreparedUploadData { - // First A006-sign the payload, then E002-encrypt with bank's pub. - val encryptionResult = if (isEbics3) { - val innerSignedEbicsXml = signOrderEbics3( // A006 signature. - payload, - subscriberDetails.customerSignPriv, - subscriberDetails.partnerId, - subscriberDetails.userId - ) - val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002( - EbicsOrderUtil.encodeOrderDataXml(innerSignedEbicsXml), - subscriberDetails.bankEncPub!! - ) - userSignatureDataEncrypted - } else { - val innerSignedEbicsXml = signOrder( // A006 signature. - payload, - subscriberDetails.customerSignPriv, - subscriberDetails.partnerId, - subscriberDetails.userId - ) - val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002( - EbicsOrderUtil.encodeOrderDataXml(innerSignedEbicsXml), - subscriberDetails.bankEncPub!! - ) - userSignatureDataEncrypted - } - // Then only E002 symmetric (with ephemeral key) encrypt. - val compressedInnerPayload = DeflaterInputStream( - payload.inputStream() - ).use { it.readAllBytes() } - val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( - compressedInnerPayload, - subscriberDetails.bankEncPub!!, - encryptionResult.plainTransactionKey!! - ) - val encodedEncryptedPayload = Base64.getEncoder().encodeToString(encryptedPayload.encryptedData) - - return PreparedUploadData( - encryptionResult.encryptedTransactionKey, // ephemeral key - encryptionResult.encryptedData, // bank-pub-encrypted A006 signature. - CryptoUtil.digestEbicsOrderA006(payload), // used by EBICS 3 - listOf(encodedEncryptedPayload) // actual payload E002 encrypted. - ) -} - // Creates the EBICS 3 upload init request. fun createEbicsRequestForUploadInitialization( subscriberDetails: EbicsClientSubscriberDetails,