From 06452b9adc4d149bdb1532a3ea3160909eb51c9a Mon Sep 17 00:00:00 2001 From: MS Date: Thu, 29 Jun 2023 10:40:06 +0200 Subject: EBICS 3. Getting to both upload and download documents to/from the PostFinance test platform. --- .../tech/libeufin/nexus/ebics/EbicsClient.kt | 92 +++-- .../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 68 +-- nexus/src/test/kotlin/EbicsTest.kt | 69 ++-- nexus/src/test/kotlin/PostFinance.kt | 31 +- util/src/main/kotlin/Ebics.kt | 457 +++++++++++++-------- util/src/main/kotlin/XMLUtil.kt | 22 +- util/src/main/kotlin/ebics_h005/Ebics3Request.kt | 48 ++- util/src/main/kotlin/ebics_h005/Ebics3Response.kt | 351 ++++++++++++++++ util/src/main/kotlin/ebics_h005/Ebics3Types.kt | 1 - util/src/main/kotlin/ebics_h005/package-info.java | 1 + util/src/main/kotlin/ebics_s002/SignatureTypes.kt | 91 ++++ .../kotlin/ebics_s002/UserSignatureDataEbics3.kt | 27 ++ util/src/main/kotlin/ebics_s002/package-info.java | 13 + 13 files changed, 1009 insertions(+), 262 deletions(-) create mode 100644 util/src/main/kotlin/ebics_h005/Ebics3Response.kt create mode 100644 util/src/main/kotlin/ebics_s002/SignatureTypes.kt create mode 100644 util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt create mode 100644 util/src/main/kotlin/ebics_s002/package-info.java diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt index 8bd1db6c..f7944d55 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -112,16 +112,31 @@ suspend fun doEbicsDownloadTransaction( ): EbicsDownloadResult { // Initialization phase - val initDownloadRequestStr = createEbicsRequestForDownloadInitialization( - subscriberDetails, - fetchSpec.orderType, - fetchSpec.orderParams, - fetchSpec.ebics3Service - ) + val initDownloadRequestStr = if (fetchSpec.isEbics3) { + if (fetchSpec.ebics3Service == null) + throw internalServerError("Expected EBICS 3 fetch spec but null was found.") + createEbicsRequestForDownloadInitialization( + subscriberDetails, + fetchSpec.ebics3Service, + fetchSpec.orderParams + ) + } + else { + if (fetchSpec.orderType == null) + throw internalServerError("Expected EBICS 2.5 order type but null was found.") + createEbicsRequestForDownloadInitialization( + subscriberDetails, + fetchSpec.orderType, + fetchSpec.orderParams + ) + } val payloadChunks = LinkedList() val initResponseStr = client.postToBank(subscriberDetails.ebicsUrl, initDownloadRequestStr) - val initResponse = parseAndValidateEbicsResponse(subscriberDetails, initResponseStr) - + val initResponse = parseAndValidateEbicsResponse( + subscriberDetails, + initResponseStr, + withEbics3 = fetchSpec.isEbics3 + ) val transactionID: String? = initResponse.transactionID // Checking for EBICS communication problems. when (initResponse.technicalReturnCode) { @@ -192,7 +207,8 @@ suspend fun doEbicsDownloadTransaction( subscriberDetails, transactionID, x, - numSegments + numSegments, + fetchSpec.isEbics3 ) logger.debug("EBICS download transfer phase of ${transactionID}: sending segment $x") val transferResponseStr = client.postToBank(subscriberDetails.ebicsUrl, transferReqStr) @@ -236,13 +252,18 @@ suspend fun doEbicsDownloadTransaction( // Acknowledgement phase val ackRequest = createEbicsRequestForDownloadReceipt( subscriberDetails, - transactionID + transactionID, + fetchSpec.isEbics3 ) val ackResponseStr = client.postToBank( subscriberDetails.ebicsUrl, ackRequest ) - val ackResponse = parseAndValidateEbicsResponse(subscriberDetails, ackResponseStr) + val ackResponse = parseAndValidateEbicsResponse( + subscriberDetails, + ackResponseStr, + withEbics3 = fetchSpec.isEbics3 + ) when (ackResponse.technicalReturnCode) { EbicsReturnCode.EBICS_DOWNLOAD_POSTPROCESS_DONE -> { } @@ -263,28 +284,46 @@ suspend fun doEbicsDownloadTransaction( suspend fun doEbicsUploadTransaction( client: HttpClient, subscriberDetails: EbicsClientSubscriberDetails, - orderType: String? = null, - payload: ByteArray, - orderParams: EbicsOrderParams, - ebics3OrderService: Ebics3Request.OrderDetails.Service? = null + uploadSpec: EbicsUploadSpec, + payload: ByteArray ) { if (subscriberDetails.bankEncPub == null) { throw NexusError(HttpStatusCode.BadRequest, "bank encryption key unknown, request HPB first" ) } - val preparedUploadData = prepareUploadPayload(subscriberDetails, payload) - val req = createEbicsRequestForUploadInitialization( + val preparedUploadData = prepareUploadPayload( subscriberDetails, - orderType, - orderParams, - preparedUploadData, - ebics3OrderService = ebics3OrderService + payload, + isEbics3 = uploadSpec.isEbics3 ) + val req: String = if (uploadSpec.isEbics3) { + if (uploadSpec.ebics3Service == null) + throw internalServerError("EBICS 3 service data was expected, but null was found.") + createEbicsRequestForUploadInitialization( + subscriberDetails, + uploadSpec.ebics3Service, + uploadSpec.orderParams, + preparedUploadData + ) + } else { + if (uploadSpec.orderType == null) + throw internalServerError("EBICS 2.5 order type was expected, but null was found.") + createEbicsRequestForUploadInitialization( + subscriberDetails, + uploadSpec.orderType, + uploadSpec.orderParams ?: EbicsStandardOrderParams(), + preparedUploadData + ) + } logger.debug("EBICS upload message to: ${subscriberDetails.ebicsUrl}") val responseStr = client.postToBank(subscriberDetails.ebicsUrl, req) - val initResponse = parseAndValidateEbicsResponse(subscriberDetails, responseStr) + val initResponse = parseAndValidateEbicsResponse( + subscriberDetails, + responseStr, + withEbics3 = uploadSpec.isEbics3 + ) // The bank indicated one error, hence Nexus sent invalid data. if (initResponse.technicalReturnCode != EbicsReturnCode.EBICS_OK) { throw NexusError( @@ -311,13 +350,18 @@ suspend fun doEbicsUploadTransaction( subscriberDetails, transactionID, preparedUploadData, - 0 + 0, + withEbics3 = uploadSpec.isEbics3 ) val txRespStr = client.postToBank( subscriberDetails.ebicsUrl, ebicsPayload ) - val txResp = parseAndValidateEbicsResponse(subscriberDetails, txRespStr) + val txResp = parseAndValidateEbicsResponse( + subscriberDetails, + txRespStr, + withEbics3 = uploadSpec.isEbics3 + ) when (txResp.technicalReturnCode) { EbicsReturnCode.EBICS_OK -> {/* do nothing */} else -> { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt index d1c44e0f..5e1c14d1 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -78,26 +78,21 @@ data class EbicsFetchSpec( val ebics3Service: Ebics3Request.OrderDetails.Service? = null, // unused for 2.5 // Not always available, for example at raw POST /download/${ebicsMessageName} calls. // It helps to trace back the original level. - val originalLevel: FetchLevel? = null + val originalLevel: FetchLevel? = null, + val isEbics3: Boolean = false ) /** - * Maps EBICS specific history types to their camt - * counterparts. That allows the database to store - * camt types per-se, without any reference to the - * EBICS message that brought them. For example, a - * EBICS "Z52" and "C52" will both bring a camt.052. - * Such camt.052 is associated with the more generic - * type of FetchLevel.REPORT. + * Collects EBICS 2.5 and/or 3.0 parameters for a unified + * way of passing parameters. Individual helpers will then + * act according to the EBICS version. */ -private fun getFetchLevelFromEbicsOrder(ebicsHistoryType: String): FetchLevel { - return when(ebicsHistoryType) { - "C52", "Z52" -> FetchLevel.REPORT - "C53", "Z53" -> FetchLevel.STATEMENT - "C54", "Z54" -> FetchLevel.NOTIFICATION - else -> throw internalServerError("EBICS history type '$ebicsHistoryType' not supported") - } -} +data class EbicsUploadSpec( + val isEbics3: Boolean = false, + val ebics3Service: Ebics3Request.OrderDetails.Service? = null, // unused for 2.5 + val orderType: String? = null, + val orderParams: EbicsOrderParams? = null +) // Validate and store the received document for later ingestion. private fun validateAndStoreCamt( @@ -206,7 +201,6 @@ private suspend fun fetchEbicsTransactions( // re-throw in any other error case. throw e } - handleEbicsDownloadResult( response, bankConnectionId, @@ -523,10 +517,17 @@ private fun getNotificationSpecAfterDialect(dialect: String? = null, p: EbicsOrd orderParams = p, ebics3Service = Ebics3Request.OrderDetails.Service().apply { serviceName = "REP" - messageName = "camt.054" + messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { + value = "camt.054" + version = "04" + } scope = "CH" + container = Ebics3Request.OrderDetails.Service.Container().apply { + containerType = "ZIP" + } }, - originalLevel = FetchLevel.NOTIFICATION + originalLevel = FetchLevel.NOTIFICATION, + isEbics3 = true ) else -> EbicsFetchSpec( orderType = "C54", @@ -737,7 +738,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { subscriberDetails ) } catch (e: Exception) { - logger.warn("Fetching transactions (${spec.orderType}) excepted: ${e.message}.") + logger.warn("Fetching transactions (${spec.originalLevel}) excepted: ${e.message}.") errors.add(e) } } @@ -782,20 +783,27 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { val subscriberDetails = subscriberDetails } } - // Used to validate here, then removed for performances reasons. - doEbicsUploadTransaction( - httpClient, - dbData.subscriberDetails, - getSubmissionTypeAfterDialect(dbData.subscriberDetails.dialect), - dbData.painXml.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams(), - ebics3OrderService = if (dbData.subscriberDetails.dialect == "pf") { + val isPoFi = dbData.subscriberDetails.dialect == "pf" + val uploadSpec = EbicsUploadSpec( + isEbics3 = isPoFi, + orderType = if (!isPoFi) getSubmissionTypeAfterDialect(dbData.subscriberDetails.dialect) else null, + orderParams = EbicsStandardOrderParams(), + ebics3Service = if (isPoFi) Ebics3Request.OrderDetails.Service().apply { serviceName = "MCT" scope = "CH" - messageName = "pain.001" + messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { + value = "pain.001" + version = "09" + } } - } else null + else null + ) + doEbicsUploadTransaction( + httpClient, + dbData.subscriberDetails, + uploadSpec, + dbData.painXml.toByteArray(Charsets.UTF_8) ) transaction { val payment = getPaymentInitiation(paymentInitiationId) diff --git a/nexus/src/test/kotlin/EbicsTest.kt b/nexus/src/test/kotlin/EbicsTest.kt index 7fe09f38..19646d6a 100644 --- a/nexus/src/test/kotlin/EbicsTest.kt +++ b/nexus/src/test/kotlin/EbicsTest.kt @@ -13,9 +13,7 @@ import tech.libeufin.nexus.* import tech.libeufin.nexus.bankaccount.addPaymentInitiation import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations -import tech.libeufin.nexus.ebics.EbicsBankConnectionProtocol -import tech.libeufin.nexus.ebics.doEbicsUploadTransaction -import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails +import tech.libeufin.nexus.ebics.* import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData import tech.libeufin.nexus.iso20022.createPain001document import tech.libeufin.nexus.server.FetchLevel @@ -204,9 +202,12 @@ class DownloadAndSubmit { doEbicsUploadTransaction( client, unallowedSubscriber, - "CCT", - painMessage.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams() + EbicsUploadSpec( + orderType = "CCT", + isEbics3 = false, + orderParams = EbicsStandardOrderParams() + ), + painMessage.toByteArray(Charsets.UTF_8) ) } catch (e: EbicsProtocolError) { if (e.ebicsTechnicalCode == @@ -322,36 +323,56 @@ class DownloadAndSubmit { class EbicsTest { + @Test + fun genEbics3Upload() { + withTestDatabase { + prepNexusDb() + val foo = transaction { getEbicsSubscriberDetails("foo") } + val uploadDoc = createEbicsRequestForUploadInitialization( + subscriberDetails = foo, + ebics3OrderService = Ebics3Request.OrderDetails.Service().apply { + serviceName = "OTH" + scope = "BIL" + serviceOption = "CH002LMF" + messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { + value = "csv" + } + }, + null, + prepareUploadPayload( + foo, + "foo".toByteArray(), + isEbics3 = true + ) + ) + assert(XMLUtil.validateFromString(uploadDoc)) + } + } + /** * Tests the validity of EBICS 3.0 messages. */ @Test - fun genEbics3() { + fun genEbics3Download() { withTestDatabase { prepNexusDb() val foo = transaction { getEbicsSubscriberDetails("foo") } val downloadDoc = createEbicsRequestForDownloadInitialization( - foo, - orderType = null, // triggers 3.0 - EbicsStandardOrderParams(), - Ebics3Request.OrderDetails.Service().apply { - messageName = "camt.054" + subscriberDetails = foo, + ebics3OrderService = Ebics3Request.OrderDetails.Service().apply { + messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { + value = "camt.054" + version = "04" + } scope = "CH" serviceName = "REP" - } + container = Ebics3Request.OrderDetails.Service.Container().apply { + containerType = "ZIP" + } + }, + orderParams = EbicsStandardOrderParams() ) assert(XMLUtil.validateFromString(downloadDoc)) - val uploadDoc = createEbicsRequestForDownloadInitialization( - foo, - orderType = null, // triggers 3.0 - EbicsStandardOrderParams(), - Ebics3Request.OrderDetails.Service().apply { - messageName = "pain.001" - scope = "EU" - serviceName = "MCT" - } - ) - assert(XMLUtil.validateFromString(uploadDoc)) } } } \ No newline at end of file diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt index fd798dc1..daec6c77 100644 --- a/nexus/src/test/kotlin/PostFinance.kt +++ b/nexus/src/test/kotlin/PostFinance.kt @@ -5,16 +5,18 @@ import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.nexus.bankaccount.addPaymentInitiation import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions import tech.libeufin.nexus.bankaccount.getBankAccount +import tech.libeufin.nexus.ebics.EbicsUploadSpec import tech.libeufin.nexus.ebics.doEbicsUploadTransaction import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails import tech.libeufin.nexus.getConnectionPlugin import tech.libeufin.nexus.getNexusUser import tech.libeufin.nexus.server.* import tech.libeufin.util.EbicsStandardOrderParams +import tech.libeufin.util.ebics_h005.Ebics3Request import java.io.BufferedReader import java.io.File -// Submits a Z54 to the bank, expecting a camt.054 back. +// Asks a camt.054 to the bank. private fun downloadPayment() { val httpClient = HttpClient() runBlocking { @@ -42,15 +44,24 @@ private fun uploadQrrPayment() { doEbicsUploadTransaction( httpClient, getEbicsSubscriberDetails("postfinance"), - "XTC", - qrr.toByteArray(Charsets.UTF_8), - EbicsStandardOrderParams() + EbicsUploadSpec( + ebics3Service = Ebics3Request.OrderDetails.Service().apply { + serviceName = "OTH" + scope = "BIL" + serviceOption = "CH002LMF" + messageName = Ebics3Request.OrderDetails.Service.MessageName().apply { + value = "csv" + } + }, + isEbics3 = true + ), + qrr.toByteArray(Charsets.UTF_8) ) } } /** - * Submits a XE2 (+ pain.001 version 2019) message to the bank. + * Submits a pain.001 version 2019 message to the bank. * * Causes one DBIT payment to show up in the camt.054. This one * however lacks the AcctSvcrRef, so other ways to pin it are needed. @@ -75,13 +86,12 @@ private fun uploadPain001Payment() { } val ebicsConn = getConnectionPlugin("ebics") val httpClient = HttpClient() - runBlocking { - ebicsConn.submitPaymentInitiation(httpClient, 1L) - } + runBlocking { ebicsConn.submitPaymentInitiation(httpClient, 1L) } } fun main() { // Loads EBICS subscriber's keys from disk. + // The keys should be found under libeufin-internal.git/convenience/ val bufferedReader: BufferedReader = File("/tmp/pofi.json").bufferedReader() val accessDataTxt = bufferedReader.use { it.readText() } val ebicsConn = getConnectionPlugin("ebics") @@ -102,6 +112,7 @@ fun main() { fooBankAccount.iban = "CH9789144829733648596" } } - // uploadQrrPayment() - // downloadPayment() + uploadQrrPayment() + downloadPayment() + uploadPain001Payment() } \ No newline at end of file diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt index ae5e5cda..2d90dd2c 100644 --- a/util/src/main/kotlin/Ebics.kt +++ b/util/src/main/kotlin/Ebics.kt @@ -29,7 +29,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.util.ebics_h004.* import tech.libeufin.util.ebics_h005.Ebics3Request -import tech.libeufin.util.ebics_h005.Ebics3Types +import tech.libeufin.util.ebics_h005.Ebics3Response import tech.libeufin.util.ebics_hev.HEVRequest import tech.libeufin.util.ebics_hev.HEVResponse import tech.libeufin.util.ebics_s001.UserSignatureData @@ -40,6 +40,7 @@ import java.security.interfaces.RSAPublicKey import java.time.ZonedDateTime import java.util.* import java.util.zip.DeflaterInputStream +import javax.xml.bind.JAXBElement import javax.xml.datatype.DatatypeFactory import javax.xml.datatype.XMLGregorianCalendar @@ -167,15 +168,48 @@ private fun signOrder( return userSignatureData } +private fun signOrderEbics3( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): tech.libeufin.util.ebics_s002.UserSignatureDataEbics3 { + val ES_signature = CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ) + val userSignatureData = tech.libeufin.util.ebics_s002.UserSignatureDataEbics3().apply { + orderSignatureList = listOf( + tech.libeufin.util.ebics_s002.UserSignatureDataEbics3.OrderSignatureData().apply { + signatureVersion = "A006" + signatureValue = ES_signature + partnerID = partnerId + userID = userId + } + ) + } + return userSignatureData +} + fun createEbicsRequestForDownloadReceipt( subscriberDetails: EbicsClientSubscriberDetails, - transactionID: String? + transactionID: String?, + withEbics3: Boolean = false ): String { - val req = EbicsRequest.createForDownloadReceiptPhase( - transactionID, - subscriberDetails.hostId - ) - val doc = XMLUtil.convertJaxbToDocument(req) + val doc = if (withEbics3) { + val req = Ebics3Request.createForDownloadReceiptPhase( + transactionID, + subscriberDetails.hostId + ) + XMLUtil.convertJaxbToDocument(req) + + } else { + val req = EbicsRequest.createForDownloadReceiptPhase( + transactionID, + subscriberDetails.hostId + ) + XMLUtil.convertJaxbToDocument(req) + } XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) return XMLUtil.convertDomToString(doc) } @@ -183,6 +217,7 @@ fun createEbicsRequestForDownloadReceipt( data class PreparedUploadData( val transactionKey: ByteArray, val userSignatureDataEncrypted: ByteArray, + val dataDigest: ByteArray, val encryptedPayloadChunks: List ) { override fun equals(other: Any?): Boolean { @@ -206,32 +241,90 @@ data class PreparedUploadData( } } -fun prepareUploadPayload(subscriberDetails: EbicsClientSubscriberDetails, payload: ByteArray): PreparedUploadData { - val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002( - EbicsOrderUtil.encodeOrderDataXml( - signOrder( - payload, - subscriberDetails.customerSignPriv, - subscriberDetails.partnerId, - subscriberDetails.userId - ) - ), - subscriberDetails.bankEncPub!! - ) +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!!, - userSignatureDataEncrypted.plainTransactionKey!! + encryptionResult.plainTransactionKey!! ) val encodedEncryptedPayload = Base64.getEncoder().encodeToString(encryptedPayload.encryptedData) + return PreparedUploadData( - userSignatureDataEncrypted.encryptedTransactionKey, - userSignatureDataEncrypted.encryptedData, - listOf(encodedEncryptedPayload) + 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, + ebics3OrderService: Ebics3Request.OrderDetails.Service, + orderParams: EbicsOrderParams? = null, + preparedUploadData: PreparedUploadData +): String { + val nonce = getNonce(128) + val req = Ebics3Request.createForUploadInitializationPhase( + preparedUploadData.transactionKey, + preparedUploadData.userSignatureDataEncrypted, + preparedUploadData.dataDigest, + subscriberDetails.hostId, + nonce, + subscriberDetails.partnerId, + subscriberDetails.userId, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + subscriberDetails.bankAuthPub!!, + subscriberDetails.bankEncPub!!, + BigInteger.ONE, + ebics3OrderService + ) + val doc = XMLUtil.convertJaxbToDocument( + req, + withSchemaLocation = "urn:org:ebics:H005 ebics_request_H005.xsd" + ) + logger.debug("Created EBICS 3 document for upload initialization," + + " nonce: ${nonce.toHexString()}") + XMLUtil.signEbicsDocument( + doc, + subscriberDetails.customerAuthPriv, + withEbics3 = true ) + return XMLUtil.convertDomToString(doc) } /** @@ -241,124 +334,96 @@ fun prepareUploadPayload(subscriberDetails: EbicsClientSubscriberDetails, payloa */ fun createEbicsRequestForUploadInitialization( subscriberDetails: EbicsClientSubscriberDetails, - orderType: String? = null, + orderType: String, orderParams: EbicsOrderParams, - preparedUploadData: PreparedUploadData, - ebics3OrderService: Ebics3Request.OrderDetails.Service? = null + preparedUploadData: PreparedUploadData ): String { - // Check if the call is consistent: (only) ONE instruction is expected. - if (orderType == null && ebics3OrderService == null) - throw internalServerError("Need exactly one upload instruction but zero was found.") - if (orderType != null && ebics3OrderService != null) - throw internalServerError("Need exactly one upload instruction but two were found") val nonce = getNonce(128) - val doc = if (orderType != null) { - val req = EbicsRequest.createForUploadInitializationPhase( - preparedUploadData.transactionKey, - preparedUploadData.userSignatureDataEncrypted, - subscriberDetails.hostId, - nonce, - subscriberDetails.partnerId, - subscriberDetails.userId, - DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), - subscriberDetails.bankAuthPub!!, - subscriberDetails.bankEncPub!!, - BigInteger.ONE, - orderType, - makeOrderParams(orderParams) - ) - XMLUtil.convertJaxbToDocument(req) - } else { - val req = Ebics3Request.createForUploadInitializationPhase( - preparedUploadData.transactionKey, - preparedUploadData.userSignatureDataEncrypted, - subscriberDetails.hostId, - nonce, - subscriberDetails.partnerId, - subscriberDetails.userId, - DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), - subscriberDetails.bankAuthPub!!, - subscriberDetails.bankEncPub!!, - BigInteger.ONE, - ebics3OrderService!! - ) - XMLUtil.convertJaxbToDocument(req) - } - /** - * FIXME: this log should be made by the caller. - * That way, all the EBICS transaction steps would be logged in only one function, - * as opposed to have them spread through the helpers here. This function - * returning a string blocks now, since the caller should parse and stringify - * again the message, only to get its nonce. - */ + val req = EbicsRequest.createForUploadInitializationPhase( + preparedUploadData.transactionKey, + preparedUploadData.userSignatureDataEncrypted, + subscriberDetails.hostId, + nonce, + subscriberDetails.partnerId, + subscriberDetails.userId, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + subscriberDetails.bankAuthPub!!, + subscriberDetails.bankEncPub!!, + BigInteger.ONE, + orderType, + makeOrderParams(orderParams) + ) + val doc = XMLUtil.convertJaxbToDocument(req) logger.debug("Created EBICS $orderType document for upload initialization," + " nonce: ${nonce.toHexString()}") + XMLUtil.signEbicsDocument( + doc, + subscriberDetails.customerAuthPriv + ) + return XMLUtil.convertDomToString(doc) +} + +// Generates a EBICS 2.5 signed document for the download init phase. +fun createEbicsRequestForDownloadInitialization( + subscriberDetails: EbicsClientSubscriberDetails, + orderType: String, + orderParams: EbicsOrderParams +): String { + val nonce = getNonce(128) + val req = EbicsRequest.createForDownloadInitializationPhase( + subscriberDetails.userId, + subscriberDetails.partnerId, + subscriberDetails.hostId, + nonce, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + subscriberDetails.bankEncPub ?: throw EbicsProtocolError( + HttpStatusCode.BadRequest, + "Invalid subscriber state 'bankEncPub' missing, please send HPB first" + ), + subscriberDetails.bankAuthPub ?: throw EbicsProtocolError( + HttpStatusCode.BadRequest, + "Invalid subscriber state 'bankAuthPub' missing, please send HPB first" + ), + orderType, + makeOrderParams(orderParams) + ) + logger.debug("Created EBICS document for download initialization, nonce: ${nonce.toHexString()}") + val doc = XMLUtil.convertJaxbToDocument(req) XMLUtil.signEbicsDocument( doc, subscriberDetails.customerAuthPriv, - withEbics3 = ebics3OrderService != null + withEbics3 = false ) return XMLUtil.convertDomToString(doc) } fun createEbicsRequestForDownloadInitialization( subscriberDetails: EbicsClientSubscriberDetails, - orderType: String? = null, + ebics3OrderService: Ebics3Request.OrderDetails.Service, orderParams: EbicsOrderParams, - ebics3OrderService: Ebics3Request.OrderDetails.Service? = null ): String { - // Check if the call is consistent: (only) ONE instruction is expected. - if (orderType == null && ebics3OrderService == null) - throw internalServerError("Need exactly one download instruction but zero was found.") - if (orderType != null && ebics3OrderService != null) - throw internalServerError("Need exactly one download instruction but two were found") val nonce = getNonce(128) - - val doc = if (orderType != null) { - val req = EbicsRequest.createForDownloadInitializationPhase( - subscriberDetails.userId, - subscriberDetails.partnerId, - subscriberDetails.hostId, - nonce, - DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), - subscriberDetails.bankEncPub ?: throw EbicsProtocolError( - HttpStatusCode.BadRequest, - "Invalid subscriber state 'bankEncPub' missing, please send HPB first" - ), - subscriberDetails.bankAuthPub ?: throw EbicsProtocolError( - HttpStatusCode.BadRequest, - "Invalid subscriber state 'bankAuthPub' missing, please send HPB first" - ), - orderType, - makeOrderParams(orderParams) - ) - XMLUtil.convertJaxbToDocument(req) - } else { - val req = Ebics3Request.createForDownloadInitializationPhase( - subscriberDetails.userId, - subscriberDetails.partnerId, - subscriberDetails.hostId, - nonce, - DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), - subscriberDetails.bankEncPub ?: throw EbicsProtocolError( - HttpStatusCode.BadRequest, - "Invalid subscriber state 'bankEncPub' missing, please send HPB first" - ), - subscriberDetails.bankAuthPub ?: throw EbicsProtocolError( - HttpStatusCode.BadRequest, - "Invalid subscriber state 'bankAuthPub' missing, please send HPB first" - ), - ebics3OrderService!! - ) - XMLUtil.convertJaxbToDocument(req) - } - - logger.debug("Created EBICS $orderType document for download initialization," + - " nonce: ${nonce.toHexString()}") + val req = Ebics3Request.createForDownloadInitializationPhase( + subscriberDetails.userId, + subscriberDetails.partnerId, + subscriberDetails.hostId, + nonce, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + subscriberDetails.bankEncPub ?: throw EbicsProtocolError( + HttpStatusCode.BadRequest, + "Invalid subscriber state 'bankEncPub' missing, please send HPB first" + ), + subscriberDetails.bankAuthPub ?: throw EbicsProtocolError( + HttpStatusCode.BadRequest, + "Invalid subscriber state 'bankAuthPub' missing, please send HPB first" + ), + ebics3OrderService + ) + val doc = XMLUtil.convertJaxbToDocument(req) XMLUtil.signEbicsDocument( doc, subscriberDetails.customerAuthPriv, - withEbics3 = ebics3OrderService != null + withEbics3 = true ) return XMLUtil.convertDomToString(doc) } @@ -367,16 +432,31 @@ fun createEbicsRequestForDownloadTransferPhase( subscriberDetails: EbicsClientSubscriberDetails, transactionID: String?, segmentNumber: Int, - numSegments: Int + numSegments: Int, + withEbics3: Boolean = false ): String { - val req = EbicsRequest.createForDownloadTransferPhase( - subscriberDetails.hostId, - transactionID, - segmentNumber, - numSegments + val doc = if (withEbics3) { + val req = Ebics3Request.createForDownloadTransferPhase( + subscriberDetails.hostId, + transactionID, + segmentNumber, + numSegments + ) + XMLUtil.convertJaxbToDocument(req) + } else { + val req = EbicsRequest.createForDownloadTransferPhase( + subscriberDetails.hostId, + transactionID, + segmentNumber, + numSegments + ) + XMLUtil.convertJaxbToDocument(req) + } + XMLUtil.signEbicsDocument( + doc, + subscriberDetails.customerAuthPriv, + withEbics3 = withEbics3 ) - val doc = XMLUtil.convertJaxbToDocument(req) - XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) return XMLUtil.convertDomToString(doc) } @@ -384,17 +464,29 @@ fun createEbicsRequestForUploadTransferPhase( subscriberDetails: EbicsClientSubscriberDetails, transactionID: String?, preparedUploadData: PreparedUploadData, - chunkIndex: Int + chunkIndex: Int, + withEbics3: Boolean = false ): String { - val req = EbicsRequest.createForUploadTransferPhase( - subscriberDetails.hostId, - transactionID, - // chunks are 1-indexed - BigInteger.valueOf(chunkIndex.toLong() + 1), - preparedUploadData.encryptedPayloadChunks[chunkIndex] - ) - val doc = XMLUtil.convertJaxbToDocument(req) - XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + val doc = if (withEbics3) { + val req = Ebics3Request.createForUploadTransferPhase( + subscriberDetails.hostId, + transactionID, + // chunks are 1-indexed + BigInteger.valueOf(chunkIndex.toLong() + 1), + preparedUploadData.encryptedPayloadChunks[chunkIndex] + ) + XMLUtil.convertJaxbToDocument(req) + } else { + val req = EbicsRequest.createForUploadTransferPhase( + subscriberDetails.hostId, + transactionID, + // chunks are 1-indexed + BigInteger.valueOf(chunkIndex.toLong() + 1), + preparedUploadData.encryptedPayloadChunks[chunkIndex] + ) + XMLUtil.convertJaxbToDocument(req) + } + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv, withEbics3) return XMLUtil.convertDomToString(doc) } @@ -434,6 +526,7 @@ enum class EbicsReturnCode(val errorCode: String) { EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), EBICS_AMOUNT_CHECK_FAILED("091303"), EBICS_EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), + EBICS_INVALID_XML("091010"), EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"); companion object { @@ -525,40 +618,51 @@ fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData { ) } -fun parseAndValidateEbicsResponse( - subscriberDetails: EbicsClientSubscriberDetails, - responseStr: String -): EbicsResponseContent { - val responseDocument = try { - XMLUtil.parseStringIntoDom(responseStr) +private fun ebics3toInternalRepr(response: String): EbicsResponseContent { + val resp: JAXBElement = try { + XMLUtil.convertStringToJaxb(response) } catch (e: Exception) { throw EbicsProtocolError( HttpStatusCode.InternalServerError, - "Invalid XML (as EbicsResponse) received from bank" + "Could not transform string-response from bank into JAXB" ) } - if (!XMLUtil.verifyEbicsDocument( - responseDocument, - subscriberDetails.bankAuthPub ?: throw EbicsProtocolError( - HttpStatusCode.InternalServerError, - "Bank's signature verification failed" - ) - ) - ) { - throw EbicsProtocolError( - HttpStatusCode.InternalServerError, - "Bank's signature verification failed" - ) + val bankReturnCodeStr = resp.value.body.returnCode.value + val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) + + val techReturnCodeStr = resp.value.header.mutable.returnCode + val techReturnCode = EbicsReturnCode.lookup(techReturnCodeStr) + + val reportText = resp.value.header.mutable.reportText + + val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo + val dataEncryptionInfo = if (daeXml == null) { + null + } else { + DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) } - val resp = try { - XMLUtil.convertStringToJaxb(responseStr) + + return EbicsResponseContent( + transactionID = resp.value.header._static.transactionID, + bankReturnCode = bankReturnCode, + technicalReturnCode = techReturnCode, + reportText = reportText, + orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, + dataEncryptionInfo = dataEncryptionInfo, + numSegments = resp.value.header._static.numSegments?.toInt(), + segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() + ) +} + +private fun ebics25toInternalRepr(response: String): EbicsResponseContent { + val resp: JAXBElement = try { + XMLUtil.convertStringToJaxb(response) } catch (e: Exception) { throw EbicsProtocolError( HttpStatusCode.InternalServerError, "Could not transform string-response from bank into JAXB" ) } - val bankReturnCodeStr = resp.value.body.returnCode.value val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) @@ -585,6 +689,37 @@ fun parseAndValidateEbicsResponse( segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() ) } +fun parseAndValidateEbicsResponse( + subscriberDetails: EbicsClientSubscriberDetails, + responseStr: String, + withEbics3: Boolean = false +): EbicsResponseContent { + val responseDocument = try { + XMLUtil.parseStringIntoDom(responseStr) + } catch (e: Exception) { + throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Invalid XML (as EbicsResponse) received from bank" + ) + } + if (!XMLUtil.verifyEbicsDocument( + responseDocument, + subscriberDetails.bankAuthPub ?: throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Bank's signature verification failed" + ), + withEbics3 = withEbics3 + ) + ) { + throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Bank's signature verification failed" + ) + } + if (withEbics3) + return ebics3toInternalRepr(responseStr) + return ebics25toInternalRepr(responseStr) +} /** * Get the private key that matches the given public key digest. diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt index 6d923ae6..97c56180 100644 --- a/util/src/main/kotlin/XMLUtil.kt +++ b/util/src/main/kotlin/XMLUtil.kt @@ -66,11 +66,11 @@ import logger class DefaultNamespaces : NamespacePrefixMapper() { override fun getPreferredPrefix(namespaceUri: String?, suggestion: String?, requirePrefix: Boolean): String? { if (namespaceUri == "http://www.w3.org/2000/09/xmldsig#") return "ds" + if (namespaceUri == XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI) return "xsi" return null } } - class DOMInputImpl : LSInput { var fPublicId: String? = null var fSystemId: String? = null @@ -292,23 +292,35 @@ class XMLUtil private constructor() { return validate(xmlSource) } - inline fun convertJaxbToString(obj: T): String { + inline fun convertJaxbToString( + obj: T, + withSchemaLocation: String? = null + ): String { val sw = StringWriter() val jc = JAXBContext.newInstance(T::class.java) val m = jc.createMarshaller() m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + if (withSchemaLocation != null) { + m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) + } m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) m.marshal(obj, sw) return sw.toString() } - inline fun convertJaxbToDocument(obj: T): Document { + inline fun convertJaxbToDocument( + obj: T, + withSchemaLocation: String? = null + ): Document { val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() dbf.isNamespaceAware = true val doc = dbf.newDocumentBuilder().newDocument() val jc = JAXBContext.newInstance(T::class.java) val m = jc.createMarshaller() m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + if (withSchemaLocation != null) { + m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) + } m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) m.marshal(obj, doc) return doc @@ -340,8 +352,6 @@ class XMLUtil private constructor() { val tf = TransformerFactory.newInstance() val t = tf.newTransformer() - //t.setOutputProperty(OutputKeys.INDENT, "yes") - /* Make string writer. */ val sw = StringWriter() @@ -409,7 +419,7 @@ class XMLUtil private constructor() { doc: Document, signingPriv: PrivateKey, withEbics3: Boolean = false - ): Unit { + ) { val xpath = XPathFactory.newInstance().newXPath() xpath.namespaceContext = object : NamespaceContext { override fun getNamespaceURI(p0: String?): String { diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt index b9bcf2ab..2af81601 100644 --- a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt +++ b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt @@ -163,8 +163,7 @@ class Ebics3Request { lateinit var adminOrderType: String @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["serviceName", "scope", "messageName"]) - + @XmlType(propOrder = ["serviceName", "scope", "serviceOption", "container", "messageName"]) class Service { @get:XmlElement(name = "ServiceName", required = true) @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) @@ -174,9 +173,31 @@ class Ebics3Request { @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) lateinit var scope: String + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) + class MessageName { + @XmlValue + lateinit var value: String + + @XmlAttribute(name = "version") + var version: String? = null + } + @get:XmlElement(name = "MsgName", required = true) + lateinit var messageName: MessageName + + @get:XmlElement(name = "ServiceOption", required = true) @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var messageName: String + var serviceOption: String? = null + + @XmlAccessorType(XmlAccessType.NONE) + class Container { + @XmlAttribute(name = "containerType") + lateinit var containerType: String + } + + @get:XmlElement(name = "Container", required = true) + var container: Container? = null } @XmlAccessorType(XmlAccessType.NONE) @@ -250,9 +271,8 @@ class Ebics3Request { var value: ByteArray? = null } - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "orderData", "hostId"]) + @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "dataDigest", "orderData", "hostId"]) class DataTransfer { @get:XmlElement(name = "DataEncryptionInfo") @@ -261,6 +281,18 @@ class Ebics3Request { @get:XmlElement(name = "SignatureData") var signatureData: SignatureData? = null + @XmlAccessorType(XmlAccessType.NONE) + class DataDigest { + @get:XmlAttribute(name = "SignatureVersion", required = true) + var signatureVersion: String = "A006" + + @get:XmlValue + var value: ByteArray? = null + } + + @get:XmlElement(name = "DataDigest") + var dataDigest: DataDigest? = null + @get:XmlElement(name = "OrderData") var orderData: String? = null @@ -411,6 +443,7 @@ class Ebics3Request { fun createForUploadInitializationPhase( encryptedTransactionKey: ByteArray, encryptedSignatureData: ByteArray, + aDataDigest: ByteArray, hostId: String, nonceArg: ByteArray, partnerId: String, @@ -436,7 +469,7 @@ class Ebics3Request { userID = userId orderDetails = OrderDetails().apply { this.adminOrderType = "BTU" - this.btdOrderParams = OrderDetails.BTOrderParams().apply { + this.btuOrderParams = OrderDetails.BTOrderParams().apply { service = aOrderService } } @@ -467,6 +500,9 @@ class Ebics3Request { authenticate = true value = encryptedSignatureData } + dataDigest = DataTransfer.DataDigest().apply { + value = aDataDigest + } dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply { transactionKey = encryptedTransactionKey authenticate = true diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Response.kt b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt new file mode 100644 index 00000000..56de5ffb --- /dev/null +++ b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt @@ -0,0 +1,351 @@ +package tech.libeufin.util.ebics_h005 + +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.apache.xml.security.binding.xmldsig.SignedInfoType +import tech.libeufin.util.CryptoUtil +import tech.libeufin.util.XMLUtil +import tech.libeufin.util.ebics_h004.EbicsTypes +import java.math.BigInteger +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import kotlin.math.min + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) +@XmlRootElement(name = "ebicsResponse") +class Ebics3Response { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(required = true) + lateinit var header: Header + + @get:XmlElement(name = "AuthSignature", required = true) + lateinit var authSignature: SignatureType + + @get:XmlElement(required = true) + lateinit var body: Body + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["_static", "mutable"]) + class Header { + @get:XmlElement(name = "static", required = true) + lateinit var _static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: MutableHeaderType + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) + class Body { + @get:XmlElement(name = "DataTransfer") + var dataTransfer: DataTransferResponseType? = null + + @get:XmlElement(name = "ReturnCode", required = true) + lateinit var returnCode: ReturnCode + + @get:XmlElement(name = "TimestampBankParameter") + var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["transactionPhase", "segmentNumber", "orderID", "returnCode", "reportText"] + ) + class MutableHeaderType { + @get:XmlElement(name = "TransactionPhase", required = true) + @get:XmlSchemaType(name = "token") + lateinit var transactionPhase: EbicsTypes.TransactionPhaseType + + @get:XmlElement(name = "SegmentNumber") + var segmentNumber: EbicsTypes.SegmentNumber? = null + + @get:XmlElement(name = "OrderID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + var orderID: String? = null + + @get:XmlElement(name = "ReturnCode", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var returnCode: String + + @get:XmlElement(name = "ReportText", required = true) + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + @get:XmlSchemaType(name = "normalizedString") + lateinit var reportText: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class OrderData { + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class ReturnCode { + @get:XmlValue + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"]) + class DataTransferResponseType { + @get:XmlElement(name = "DataEncryptionInfo") + var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null + + @get:XmlElement(name = "OrderData", required = true) + lateinit var orderData: OrderData + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) + class StaticHeaderType { + @get:XmlElement(name = "TransactionID") + var transactionID: String? = null + + @get:XmlElement(name = "NumSegments") + @get:XmlSchemaType(name = "positiveInteger") + var numSegments: BigInteger? = null + } + + companion object { + + fun createForUploadWithError( + errorText: String, errorCode: String, phase: EbicsTypes.TransactionPhaseType + ): Ebics3Response { + val resp = Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Ebics3Response.Header().apply { + this.authenticate = true + this.mutable = Ebics3Response.MutableHeaderType().apply { + this.reportText = errorText + this.returnCode = errorCode + this.transactionPhase = phase + } + _static = Ebics3Response.StaticHeaderType() + } + this.authSignature = SignatureType() + this.body = Ebics3Response.Body().apply { + this.returnCode = Ebics3Response.ReturnCode().apply { + this.authenticate = true + this.value = errorCode + } + } + } + return resp + } + + fun createForUploadInitializationPhase(transactionID: String, orderID: String): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.RECEIPT + if (positiveAck) { + this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_DONE] Received positive receipt" + this.returnCode = "011000" + } else { + this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_SKIPPED] Received negative receipt" + this.returnCode = "011001" + } + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForUploadTransferPhase( + transactionID: String, + segmentNumber: Int, + lastSegment: Boolean, + orderID: String + ): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + if (lastSegment) { + this.lastSegment = true + } + } + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + /** + * @param requestedSegment requested segment as a 1-based index + */ + fun createForDownloadTransferPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + encodedData: String, + requestedSegment: Int + ): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.lastSegment = numSegments == requestedSegment + this.value = BigInteger.valueOf(requestedSegment.toLong()) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = DataTransferResponseType().apply { + this.orderData = OrderData().apply { + val start = segmentSize * (requestedSegment - 1) + this.value = encodedData.substring(start, min(start + segmentSize, encodedData.length)) + } + } + } + } + } + + fun createForDownloadInitializationPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + enc: CryptoUtil.EncryptionResult, + encodedData: String + ): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.lastSegment = (numSegments == 1) + this.value = BigInteger.valueOf(1) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = DataTransferResponseType().apply { + this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + this.authenticate = true + this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest() + .apply { + this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + this.version = "E002" + this.value = enc.pubKeyDigest + } + this.transactionKey = enc.encryptedTransactionKey + } + this.orderData = OrderData().apply { + this.value = encodedData.substring(0, min(segmentSize, encodedData.length)) + } + } + } + } + } + } +} diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Types.kt b/util/src/main/kotlin/ebics_h005/Ebics3Types.kt index 74c7af0f..2bb0659a 100644 --- a/util/src/main/kotlin/ebics_h005/Ebics3Types.kt +++ b/util/src/main/kotlin/ebics_h005/Ebics3Types.kt @@ -63,7 +63,6 @@ object Ebics3Types { var lastSegment: Boolean? = null } - @XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"]) @XmlAccessorType(XmlAccessType.NONE) class DataEncryptionInfo { diff --git a/util/src/main/kotlin/ebics_h005/package-info.java b/util/src/main/kotlin/ebics_h005/package-info.java index 3300f657..6f65a4b2 100644 --- a/util/src/main/kotlin/ebics_h005/package-info.java +++ b/util/src/main/kotlin/ebics_h005/package-info.java @@ -8,5 +8,6 @@ elementFormDefault = XmlNsForm.QUALIFIED ) package tech.libeufin.util.ebics_h005; +import javax.xml.bind.annotation.XmlNs; import javax.xml.bind.annotation.XmlNsForm; import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s002/SignatureTypes.kt b/util/src/main/kotlin/ebics_s002/SignatureTypes.kt new file mode 100644 index 00000000..9e367fc9 --- /dev/null +++ b/util/src/main/kotlin/ebics_s002/SignatureTypes.kt @@ -0,0 +1,91 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * + */ + +package tech.libeufin.util.ebics_s002 + +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.apache.xml.security.binding.xmldsig.X509DataType +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import javax.xml.datatype.XMLGregorianCalendar + + +object SignatureTypes { + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "PubKeyValueType", namespace = "http://www.ebics.org/S002", propOrder = [ + "rsaKeyValue", + "timeStamp" + ] + ) + class PubKeyValueType { + @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) + lateinit var rsaKeyValue: RSAKeyValueType + + @get:XmlElement(name = "TimeStamp") + @get:XmlSchemaType(name = "dateTime") + var timeStamp: XMLGregorianCalendar? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = [ + "x509Data", + "pubKeyValue", + "signatureVersion" + ] + ) + class SignaturePubKeyInfoType { + @get:XmlElement(name = "X509Data") + var x509Data: X509DataType? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "SignatureVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var signatureVersion: String + } + + /** + * EBICS INI payload. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["signaturePubKeyInfo", "partnerID", "userID"] + ) + @XmlRootElement(name = "SignaturePubKeyOrderData") + class SignaturePubKeyOrderData { + @get:XmlElement(name = "SignaturePubKeyInfo", required = true) + lateinit var signaturePubKeyInfo: SignaturePubKeyInfoType + + @get:XmlElement(name = "PartnerID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var partnerID: String + + @get:XmlElement(name = "UserID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var userID: String + } +} \ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt b/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt new file mode 100644 index 00000000..6d7012a1 --- /dev/null +++ b/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt @@ -0,0 +1,27 @@ +package tech.libeufin.util.ebics_s002 + +import javax.xml.bind.annotation.* + +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "UserSignatureData") +@XmlType(name = "", propOrder = ["orderSignatureList"]) +class UserSignatureDataEbics3 { + @XmlElement(name = "OrderSignatureData", type = OrderSignatureData::class) + var orderSignatureList: List? = null + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["signatureVersion", "signatureValue", "partnerID", "userID"]) + class OrderSignatureData { + @XmlElement(name = "SignatureVersion") + lateinit var signatureVersion: String + + @XmlElement(name = "SignatureValue") + lateinit var signatureValue: ByteArray + + @XmlElement(name = "PartnerID") + lateinit var partnerID: String + + @XmlElement(name = "UserID") + lateinit var userID: String + } +} \ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s002/package-info.java b/util/src/main/kotlin/ebics_s002/package-info.java new file mode 100644 index 00000000..a9f7729a --- /dev/null +++ b/util/src/main/kotlin/ebics_s002/package-info.java @@ -0,0 +1,13 @@ +/** + * This package-info.java file defines the default namespace for the JAXB bindings + * defined in the package. + */ + +@XmlSchema( + namespace = "http://www.ebics.org/S002", + elementFormDefault = XmlNsForm.QUALIFIED +) +package tech.libeufin.util.ebics_s002; + +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file -- cgit v1.2.3