From 6f079d6469f6d5ab9e603e27b8e965215d38587c Mon Sep 17 00:00:00 2001 From: Antoine A <> Date: Thu, 7 Mar 2024 23:22:04 +0100 Subject: Merge ebics and nexus module --- Makefile | 4 - ebics/build.gradle | 23 -- ebics/codegen.py | 72 ----- ebics/src/main/kotlin/Ebics.kt | 287 ------------------ ebics/src/main/kotlin/Iso20022CodeSets.kt | 330 --------------------- ebics/src/main/kotlin/Iso20022Constants.kt | 35 --- ebics/src/main/kotlin/XMLUtil.kt | 212 ------------- ebics/src/main/kotlin/XmlCombinators.kt | 204 ------------- ebics/src/main/resources/version.txt | 1 - ebics/src/test/kotlin/XmlCombinatorsTest.kt | 76 ----- ebics/src/test/kotlin/XmlUtilTest.kt | 73 ----- ebics/src/test/resources/signature1/doc.xml | 9 - ebics/src/test/resources/signature1/public_key.txt | 1 - nexus/build.gradle | 1 - nexus/codegen.py | 72 +++++ .../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 13 +- .../main/kotlin/tech/libeufin/nexus/EbicsSetup.kt | 1 - .../main/kotlin/tech/libeufin/nexus/Iso20022.kt | 1 - .../kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt | 330 +++++++++++++++++++++ .../tech/libeufin/nexus/Iso20022Constants.kt | 35 +++ .../src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt | 209 +++++++++++++ .../kotlin/tech/libeufin/nexus/XmlCombinators.kt | 204 +++++++++++++ .../main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt | 288 ++++++++++++++++++ .../kotlin/tech/libeufin/nexus/ebics/Ebics2.kt | 2 +- .../kotlin/tech/libeufin/nexus/ebics/Ebics3.kt | 6 +- .../tech/libeufin/nexus/ebics/EbicsCommon.kt | 1 - nexus/src/test/kotlin/Ebics.kt | 2 +- nexus/src/test/kotlin/XmlCombinatorsTest.kt | 76 +++++ nexus/src/test/kotlin/XmlUtilTest.kt | 73 +++++ nexus/src/test/resources/signature1/doc.xml | 9 + nexus/src/test/resources/signature1/public_key.txt | 1 + settings.gradle | 3 +- 32 files changed, 1307 insertions(+), 1347 deletions(-) delete mode 100644 ebics/build.gradle delete mode 100644 ebics/codegen.py delete mode 100644 ebics/src/main/kotlin/Ebics.kt delete mode 100644 ebics/src/main/kotlin/Iso20022CodeSets.kt delete mode 100644 ebics/src/main/kotlin/Iso20022Constants.kt delete mode 100644 ebics/src/main/kotlin/XMLUtil.kt delete mode 100644 ebics/src/main/kotlin/XmlCombinators.kt delete mode 100644 ebics/src/main/resources/version.txt delete mode 100644 ebics/src/test/kotlin/XmlCombinatorsTest.kt delete mode 100644 ebics/src/test/kotlin/XmlUtilTest.kt delete mode 100644 ebics/src/test/resources/signature1/doc.xml delete mode 100644 ebics/src/test/resources/signature1/public_key.txt create mode 100644 nexus/codegen.py create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt create mode 100644 nexus/src/test/kotlin/XmlCombinatorsTest.kt create mode 100644 nexus/src/test/kotlin/XmlUtilTest.kt create mode 100644 nexus/src/test/resources/signature1/doc.xml create mode 100644 nexus/src/test/resources/signature1/public_key.txt diff --git a/Makefile b/Makefile index 864bff0c..004dc256 100644 --- a/Makefile +++ b/Makefile @@ -103,10 +103,6 @@ bank-test: install-nobuild-files nexus-test: install-nobuild-files ./gradlew :nexus:test --tests $(test) -i -.PHONY: ebics-test -ebics-test: install-nobuild-files - ./gradlew :ebics:test --tests $(test) -i - .PHONY: common-test common-test: install-nobuild-files ./gradlew :common:test --tests $(test) -i diff --git a/ebics/build.gradle b/ebics/build.gradle deleted file mode 100644 index c50939dc..00000000 --- a/ebics/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id("java") - id("kotlin") -} - -version = rootProject.version - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -compileKotlin.kotlinOptions.jvmTarget = "17" -compileTestKotlin.kotlinOptions.jvmTarget = "17" - -sourceSets.main.java.srcDirs = ["src/main/kotlin"] - -dependencies { - implementation(project(":common")) - - implementation("io.ktor:ktor-http:$ktor_version") - implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") -} \ No newline at end of file diff --git a/ebics/codegen.py b/ebics/codegen.py deleted file mode 100644 index 7b145662..00000000 --- a/ebics/codegen.py +++ /dev/null @@ -1,72 +0,0 @@ -# Update EBICS constants file using latest external code sets files - -import polars as pl -import requests -from io import BytesIO -from zipfile import ZipFile - - -def iso20022codegen(): - # Get XLSX zip file from server - r = requests.get( - "https://www.iso20022.org/sites/default/files/media/file/ExternalCodeSets_XLSX.zip" - ) - assert r.status_code == 200 - - # Unzip the XLSX file - zip = ZipFile(BytesIO(r.content)) - files = zip.namelist() - assert len(files) == 1 - file = zip.open(files[0]) - - # Parse excel - df = pl.read_excel(file, sheet_name="AllCodeSets") - - def extractCodeSet(setName: str, className: str) -> str: - out = f"enum class {className}(val isoCode: String, val description: String) {{" - - for row in df.filter(pl.col("Code Set") == setName).sort("Code Value").rows(named=True): - (value, isoCode, description) = ( - row["Code Value"], - row["Code Name"], - row["Code Definition"].split("\n", 1)[0].strip(), - ) - out += f'\n\t{value}("{isoCode}", "{description}"),' - - out += "\n}" - return out - - # Write kotlin file - kt = f"""/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 - * - */ - -// THIS FILE IS GENERATED, DO NOT EDIT - -package tech.libeufin.ebics - -{extractCodeSet("ExternalStatusReason1Code", "ExternalStatusReasonCode")} - -{extractCodeSet("ExternalPaymentGroupStatus1Code", "ExternalPaymentGroupStatusCode")} - -{extractCodeSet("ExternalPaymentTransactionStatus1Code", "ExternalPaymentTransactionStatusCode")} -""" - with open("src/main/kotlin/Iso20022CodeSets.kt", "w") as file1: - file1.write(kt) - -iso20022codegen() diff --git a/ebics/src/main/kotlin/Ebics.kt b/ebics/src/main/kotlin/Ebics.kt deleted file mode 100644 index d9639ff5..00000000 --- a/ebics/src/main/kotlin/Ebics.kt +++ /dev/null @@ -1,287 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 - * - */ - -/** - * This is the main "EBICS library interface". Functions here are stateless helpers - * used to implement both an EBICS server and EBICS client. - */ - -package tech.libeufin.ebics - -import io.ktor.http.* -import org.w3c.dom.Document -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.* -import java.io.InputStream -import java.security.SecureRandom -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import javax.xml.datatype.DatatypeFactory -import javax.xml.datatype.XMLGregorianCalendar - -data class EbicsProtocolError( - val httpStatusCode: HttpStatusCode, - val reason: String, - /** - * This class is also used when Nexus finds itself - * in an inconsistent state, without interacting with the - * bank. In this case, the EBICS code below can be left - * null. - */ - val ebicsTechnicalCode: EbicsReturnCode? = null -) : Exception(reason) - -/** - * @param size in bits - */ -fun getNonce(size: Int): ByteArray { - val sr = SecureRandom() - val ret = ByteArray(size / 8) - sr.nextBytes(ret) - return ret -} - -data class PreparedUploadData( - val transactionKey: ByteArray, - val userSignatureDataEncrypted: ByteArray, - val dataDigest: ByteArray, - val encryptedPayloadChunks: List -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as PreparedUploadData - - if (!transactionKey.contentEquals(other.transactionKey)) return false - if (!userSignatureDataEncrypted.contentEquals(other.userSignatureDataEncrypted)) return false - if (encryptedPayloadChunks != other.encryptedPayloadChunks) return false - - return true - } - - override fun hashCode(): Int { - var result = transactionKey.contentHashCode() - result = 31 * result + userSignatureDataEncrypted.contentHashCode() - result = 31 * result + encryptedPayloadChunks.hashCode() - return result - } -} - -data class DataEncryptionInfo( - val transactionKey: ByteArray, - val bankPubDigest: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DataEncryptionInfo - - if (!transactionKey.contentEquals(other.transactionKey)) return false - if (!bankPubDigest.contentEquals(other.bankPubDigest)) return false - - return true - } - - override fun hashCode(): Int { - var result = transactionKey.contentHashCode() - result = 31 * result + bankPubDigest.contentHashCode() - return result - } -} - - -// TODO import missing using a script -@Suppress("SpellCheckingInspection") -enum class EbicsReturnCode(val errorCode: String) { - EBICS_OK("000000"), - EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), - EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), - EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), - EBICS_AUTHENTICATION_FAILED("061001"), - EBICS_INVALID_REQUEST("061002"), - EBICS_INTERNAL_ERROR("061099"), - EBICS_TX_RECOVERY_SYNC("061101"), - EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), - EBICS_INVALID_ORDER_DATA_FORMAT("090004"), - EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"), - EBICS_INVALID_USER_OR_USER_STATE("091002"), - EBICS_USER_UNKNOWN("091003"), - EBICS_EBICS_INVALID_USER_STATE("091004"), - EBICS_INVALID_ORDER_IDENTIFIER("091005"), - EBICS_UNSUPPORTED_ORDER_TYPE("091006"), - EBICS_INVALID_XML("091010"), - EBICS_TX_MESSAGE_REPLAY("091103"), - EBICS_PROCESSING_ERROR("091116"), - EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), - EBICS_AMOUNT_CHECK_FAILED("091303"); - - companion object { - fun lookup(errorCode: String): EbicsReturnCode { - for (x in entries) { - if (x.errorCode == errorCode) { - return x - } - } - throw Exception( - "Unknown EBICS status code: $errorCode" - ) - } - } -} - - -fun signOrderEbics3( - orderBlob: ByteArray, - signKey: RSAPrivateCrtKey, - partnerId: String, - userId: String -): ByteArray { - return XmlBuilder.toString("UserSignatureData") { - attr("xmlns", "http://www.ebics.org/S002") - el("OrderSignatureData") { - el("SignatureVersion", "A006") - el("SignatureValue", CryptoUtil.signEbicsA006( - CryptoUtil.digestEbicsOrderA006(orderBlob), - signKey - ).encodeBase64()) - el("PartnerID", partnerId) - el("UserID", userId) - } - }.toByteArray() -} - -data class EbicsResponseContent( - val transactionID: String?, - val orderID: String?, - val dataEncryptionInfo: DataEncryptionInfo?, - val orderDataEncChunk: String?, - val technicalReturnCode: EbicsReturnCode, - val bankReturnCode: EbicsReturnCode, - val reportText: String, - val segmentNumber: Int?, - // Only present in init phase - val numSegments: Int? -) - -data class EbicsKeyManagementResponseContent( - val technicalReturnCode: EbicsReturnCode, - val bankReturnCode: EbicsReturnCode?, - val orderData: ByteArray? -) - - -class HpbResponseData( - val hostID: String, - val encryptionPubKey: RSAPublicKey, - val encryptionVersion: String, - val authenticationPubKey: RSAPublicKey, - val authenticationVersion: String -) - - -fun ebics3toInternalRepr(response: Document): EbicsResponseContent { - // TODO better ebics response type - return XmlDestructor.fromDoc(response, "ebicsResponse") { - var transactionID: String? = null - var numSegments: Int? = null - lateinit var technicalReturnCode: EbicsReturnCode - lateinit var bankReturnCode: EbicsReturnCode - lateinit var reportText: String - var orderID: String? = null - var segmentNumber: Int? = null - var orderDataEncChunk: String? = null - var dataEncryptionInfo: DataEncryptionInfo? = null - one("header") { - one("static") { - transactionID = opt("TransactionID")?.text() - numSegments = opt("NumSegments")?.text()?.toInt() - } - one("mutable") { - segmentNumber = opt("SegmentNumber")?.text()?.toInt() - orderID = opt("OrderID")?.text() - technicalReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text()) - reportText = one("ReportText").text() - } - } - one("body") { - opt("DataTransfer") { - orderDataEncChunk = one("OrderData").text() - dataEncryptionInfo = opt("DataEncryptionInfo") { - DataEncryptionInfo( - one("TransactionKey").text().decodeBase64(), - one("EncryptionPubKeyDigest").text().decodeBase64() - ) - } - } - bankReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text()) - } - EbicsResponseContent( - transactionID = transactionID, - orderID = orderID, - bankReturnCode = bankReturnCode, - technicalReturnCode = technicalReturnCode, - reportText = reportText, - orderDataEncChunk = orderDataEncChunk, - dataEncryptionInfo = dataEncryptionInfo, - numSegments = numSegments, - segmentNumber = segmentNumber - ) - } -} - -fun parseEbicsHpbOrder(orderDataRaw: InputStream): HpbResponseData { - return XmlDestructor.fromStream(orderDataRaw, "HPBResponseOrderData") { - val (authenticationPubKey, authenticationVersion) = one("AuthenticationPubKeyInfo") { - Pair( - one("PubKeyValue").one("RSAKeyValue") { - CryptoUtil.loadRsaPublicKeyFromComponents( - one("Modulus").text().decodeBase64(), - one("Exponent").text().decodeBase64(), - ) - }, - one("AuthenticationVersion").text() - ) - } - val (encryptionPubKey, encryptionVersion) = one("EncryptionPubKeyInfo") { - Pair( - one("PubKeyValue").one("RSAKeyValue") { - CryptoUtil.loadRsaPublicKeyFromComponents( - one("Modulus").text().decodeBase64(), - one("Exponent").text().decodeBase64(), - ) - }, - one("EncryptionVersion").text() - ) - - } - val hostID: String = one("HostID").text() - HpbResponseData( - hostID = hostID, - encryptionPubKey = encryptionPubKey, - encryptionVersion = encryptionVersion, - authenticationPubKey = authenticationPubKey, - authenticationVersion = authenticationVersion - ) - } -} \ No newline at end of file diff --git a/ebics/src/main/kotlin/Iso20022CodeSets.kt b/ebics/src/main/kotlin/Iso20022CodeSets.kt deleted file mode 100644 index f5e338df..00000000 --- a/ebics/src/main/kotlin/Iso20022CodeSets.kt +++ /dev/null @@ -1,330 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 - * - */ - -// THIS FILE IS GENERATED, DO NOT EDIT - -package tech.libeufin.ebics - -enum class ExternalStatusReasonCode(val isoCode: String, val description: String) { - AB01("AbortedClearingTimeout", "Clearing process aborted due to timeout."), - AB02("AbortedClearingFatalError", "Clearing process aborted due to a fatal error."), - AB03("AbortedSettlementTimeout", "Settlement aborted due to timeout."), - AB04("AbortedSettlementFatalError", "Settlement process aborted due to a fatal error."), - AB05("TimeoutCreditorAgent", "Transaction stopped due to timeout at the Creditor Agent."), - AB06("TimeoutInstructedAgent", "Transaction stopped due to timeout at the Instructed Agent."), - AB07("OfflineAgent", "Agent of message is not online."), - AB08("OfflineCreditorAgent", "Creditor Agent is not online."), - AB09("ErrorCreditorAgent", "Transaction stopped due to error at the Creditor Agent."), - AB10("ErrorInstructedAgent", "Transaction stopped due to error at the Instructed Agent."), - AB11("TimeoutDebtorAgent", "Transaction stopped due to timeout at the Debtor Agent."), - AC01("IncorrectAccountNumber", "Account number is invalid or missing."), - AC02("InvalidDebtorAccountNumber", "Debtor account number invalid or missing"), - AC03("InvalidCreditorAccountNumber", "Creditor account number invalid or missing"), - AC04("ClosedAccountNumber", "Account number specified has been closed on the bank of account's books."), - AC05("ClosedDebtorAccountNumber", "Debtor account number closed"), - AC06("BlockedAccount", "Account specified is blocked, prohibiting posting of transactions against it."), - AC07("ClosedCreditorAccountNumber", "Creditor account number closed"), - AC08("InvalidBranchCode", "Branch code is invalid or missing"), - AC09("InvalidAccountCurrency", "Account currency is invalid or missing"), - AC10("InvalidDebtorAccountCurrency", "Debtor account currency is invalid or missing"), - AC11("InvalidCreditorAccountCurrency", "Creditor account currency is invalid or missing"), - AC12("InvalidAccountType", "Account type missing or invalid."), - AC13("InvalidDebtorAccountType", "Debtor account type missing or invalid"), - AC14("InvalidCreditorAccountType", "Creditor account type missing or invalid"), - AC15("AccountDetailsChanged", "The account details for the counterparty have changed."), - AC16("CardNumberInvalid", "Credit or debit card number is invalid."), - AEXR("AlreadyExpiredRTP", "Request-to-pay Expiry Date and Time has already passed."), - AG01("TransactionForbidden", "Transaction forbidden on this type of account (formerly NoAgreement)"), - AG02("InvalidBankOperationCode", "Bank Operation code specified in the message is not valid for receiver"), - AG03("TransactionNotSupported", "Transaction type not supported/authorized on this account"), - AG04("InvalidAgentCountry", "Agent country code is missing or invalid."), - AG05("InvalidDebtorAgentCountry", "Debtor agent country code is missing or invalid"), - AG06("InvalidCreditorAgentCountry", "Creditor agent country code is missing or invalid"), - AG07("UnsuccesfulDirectDebit", "Debtor account cannot be debited for a generic reason."), - AG08("InvalidAccessRights", "Transaction failed due to invalid or missing user or access right"), - AG09("PaymentNotReceived", "Original payment never received."), - AG10("AgentSuspended", "Agent of message is suspended from the Real Time Payment system."), - AG11("CreditorAgentSuspended", "Creditor Agent of message is suspended from the Real Time Payment system."), - AG12("NotAllowedBookTransfer", "Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed."), - AG13("ForbiddenReturnPayment", "Returned payments derived from previously returned transactions are not allowed."), - AGNT("IncorrectAgent", "Agent in the payment workflow is incorrect"), - ALAC("AlreadyAcceptedRTP", "Request-to-pay has already been accepted by the Debtor."), - AM01("ZeroAmount", "Specified message amount is equal to zero"), - AM02("NotAllowedAmount", "Specific transaction/message amount is greater than allowed maximum"), - AM03("NotAllowedCurrency", "Specified message amount is an non processable currency outside of existing agreement"), - AM04("InsufficientFunds", "Amount of funds available to cover specified message amount is insufficient."), - AM05("Duplication", "Duplication"), - AM06("TooLowAmount", "Specified transaction amount is less than agreed minimum."), - AM07("BlockedAmount", "Amount specified in message has been blocked by regulatory authorities."), - AM09("WrongAmount", "Amount received is not the amount agreed or expected"), - AM10("InvalidControlSum", "Sum of instructed amounts does not equal the control sum."), - AM11("InvalidTransactionCurrency", "Transaction currency is invalid or missing"), - AM12("InvalidAmount", "Amount is invalid or missing"), - AM13("AmountExceedsClearingSystemLimit", "Transaction amount exceeds limits set by clearing system"), - AM14("AmountExceedsAgreedLimit", "Transaction amount exceeds limits agreed between bank and client"), - AM15("AmountBelowClearingSystemMinimum", "Transaction amount below minimum set by clearing system"), - AM16("InvalidGroupControlSum", "Control Sum at the Group level is invalid"), - AM17("InvalidPaymentInfoControlSum", "Control Sum at the Payment Information level is invalid"), - AM18("InvalidNumberOfTransactions", "Number of transactions is invalid or missing."), - AM19("InvalidGroupNumberOfTransactions", "Number of transactions at the Group level is invalid or missing"), - AM20("InvalidPaymentInfoNumberOfTransactions", "Number of transactions at the Payment Information level is invalid"), - AM21("LimitExceeded", "Transaction amount exceeds limits agreed between bank and client."), - AM22("ZeroAmountNotApplied", "Unable to apply zero amount to designated account. For example, where the rules of a service allow the use of zero amount payments, however the back-office system is unable to apply the funds to the account. If the rules of a service prohibit the use of zero amount payments, then code AM01 is used to report the error condition."), - AM23("AmountExceedsSettlementLimit", "Transaction amount exceeds settlement limit."), - APAR("AlreadyPaidRTP", "Request To Pay has already been paid by the Debtor."), - ARFR("AlreadyRefusedRTP", "Request-to-pay has already been refused by the Debtor."), - ARJR("AlreadyRejectedRTP", "Request-to-pay has already been rejected."), - ATNS("AttachementsNotSupported", "Attachments to the request-to-pay are not supported."), - BE01("InconsistenWithEndCustomer", "Identification of end customer is not consistent with associated account number. (formerly CreditorConsistency)."), - BE04("MissingCreditorAddress", "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)."), - BE05("UnrecognisedInitiatingParty", "Party who initiated the message is not recognised by the end customer"), - BE06("UnknownEndCustomer", "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books"), - BE07("MissingDebtorAddress", "Specification of debtor's address, which is required for payment, is missing/not correct."), - BE08("MissingDebtorName", "Debtor name is missing"), - BE09("InvalidCountry", "Country code is missing or Invalid."), - BE10("InvalidDebtorCountry", "Debtor country code is missing or invalid"), - BE11("InvalidCreditorCountry", "Creditor country code is missing or invalid"), - BE12("InvalidCountryOfResidence", "Country code of residence is missing or Invalid."), - BE13("InvalidDebtorCountryOfResidence", "Country code of debtor's residence is missing or Invalid"), - BE14("InvalidCreditorCountryOfResidence", "Country code of creditor's residence is missing or Invalid"), - BE15("InvalidIdentificationCode", "Identification code missing or invalid."), - BE16("InvalidDebtorIdentificationCode", "Debtor or Ultimate Debtor identification code missing or invalid"), - BE17("InvalidCreditorIdentificationCode", "Creditor or Ultimate Creditor identification code missing or invalid"), - BE18("InvalidContactDetails", "Contact details missing or invalid"), - BE19("InvalidChargeBearerCode", "Charge bearer code for transaction type is invalid"), - BE20("InvalidNameLength", "Name length exceeds local rules for payment type."), - BE21("MissingName", "Name missing or invalid. Generic usage if cannot specifically identify debtor or creditor."), - BE22("MissingCreditorName", "Creditor name is missing"), - BE23("AccountProxyInvalid", "Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid."), - CERI("CheckERI", "Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI."), - CH03("RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture", "Value in Requested Execution Date or Requested Collection Date is too far in the future"), - CH04("RequestedExecutionDateOrRequestedCollectionDateTooFarInPast", "Value in Requested Execution Date or Requested Collection Date is too far in the past"), - CH07("ElementIsNotToBeUsedAtB-andC-Level", "Element is not to be used at B- and C-Level"), - CH09("MandateChangesNotAllowed", "Mandate changes are not allowed"), - CH10("InformationOnMandateChangesMissing", "Information on mandate changes are missing"), - CH11("CreditorIdentifierIncorrect", "Value in Creditor Identifier is incorrect"), - CH12("CreditorIdentifierNotUnambiguouslyAtTransaction-Level", "Creditor Identifier is ambiguous at Transaction Level"), - CH13("OriginalDebtorAccountIsNotToBeUsed", "Original Debtor Account is not to be used"), - CH14("OriginalDebtorAgentIsNotToBeUsed", "Original Debtor Agent is not to be used"), - CH15("ElementContentIncludesMoreThan140Characters", "Content Remittance Information/Structured includes more than 140 characters"), - CH16("ElementContentFormallyIncorrect", "Content is incorrect"), - CH17("ElementNotAdmitted", "Element is not allowed"), - CH19("ValuesWillBeSetToNextTARGETday", "Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day"), - CH20("DecimalPointsNotCompatibleWithCurrency", "Number of decimal points not compatible with the currency"), - CH21("RequiredCompulsoryElementMissing", "Mandatory element is missing"), - CH22("COREandB2BwithinOnemessage", "SDD CORE and B2B not permitted within one message"), - CHQC("ChequeSettledOnCreditorAccount", "Cheque has been presented in cheque clearing and settled on the creditor’s account."), - CN01("AuthorisationCancelled", "Authorisation is cancelled."), - CNOR("CreditorBankIsNotRegistered", "Creditor bank is not registered under this BIC in the CSM"), - CURR("IncorrectCurrency", "Currency of the payment is incorrect"), - CUST("RequestedByCustomer", "Cancellation requested by the Debtor"), - DC02("SettlementNotReceived", "Rejection of a payment due to covering FI settlement not being received."), - DNOR("DebtorBankIsNotRegistered", "Debtor bank is not registered under this BIC in the CSM"), - DS01("ElectronicSignaturesCorrect", "The electronic signature(s) is/are correct"), - DS02("OrderCancelled", "An authorized user has cancelled the order"), - DS03("OrderNotCancelled", "The user’s attempt to cancel the order was not successful"), - DS04("OrderRejected", "The order was rejected by the bank side (for reasons concerning content)"), - DS05("OrderForwardedForPostprocessing", "The order was correct and could be forwarded for postprocessing"), - DS06("TransferOrder", "The order was transferred to VEU"), - DS07("ProcessingOK", "All actions concerning the order could be done by the EBICS bank server"), - DS08("DecompressionError", "The decompression of the file was not successful"), - DS09("DecryptionError", "The decryption of the file was not successful"), - DS0A("DataSignRequested", "Data signature is required."), - DS0B("UnknownDataSignFormat", "Data signature for the format is not available or invalid."), - DS0C("SignerCertificateRevoked", "The signer certificate is revoked."), - DS0D("SignerCertificateNotValid", "The signer certificate is not valid (revoked or not active)."), - DS0E("IncorrectSignerCertificate", "The signer certificate is not present."), - DS0F("SignerCertificationAuthoritySignerNotValid", "The authority of the signer certification sending the certificate is unknown."), - DS0G("NotAllowedPayment", "Signer is not allowed to sign this operation type."), - DS0H("NotAllowedAccount", "Signer is not allowed to sign for this account."), - DS0K("NotAllowedNumberOfTransaction", "The number of transaction is over the number allowed for this signer."), - DS10("Signer1CertificateRevoked", "The certificate is revoked for the first signer."), - DS11("Signer1CertificateNotValid", "The certificate is not valid (revoked or not active) for the first signer."), - DS12("IncorrectSigner1Certificate", "The certificate is not present for the first signer."), - DS13("SignerCertificationAuthoritySigner1NotValid", "The authority of signer certification sending the certificate is unknown for the first signer."), - DS14("UserDoesNotExist", "The user is unknown on the server"), - DS15("IdenticalSignatureFound", "The same signature has already been sent to the bank"), - DS16("PublicKeyVersionIncorrect", "The public key version is not correct. This code is returned when a customer sends signature files to the financial institution after conversion from an older program version (old ES format) to a new program version (new ES format) without having carried out re-initialisation with regard to a public key change."), - DS17("DifferentOrderDataInSignatures", "Order data and signatures don’t match"), - DS18("RepeatOrder", "File cannot be tested, the complete order has to be repeated. This code is returned in the event of a malfunction during the signature check, e.g. not enough storage space."), - DS19("ElectronicSignatureRightsInsufficient", "The user’s rights (concerning his signature) are insufficient to execute the order"), - DS20("Signer2CertificateRevoked", "The certificate is revoked for the second signer."), - DS21("Signer2CertificateNotValid", "The certificate is not valid (revoked or not active) for the second signer."), - DS22("IncorrectSigner2Certificate", "The certificate is not present for the second signer."), - DS23("SignerCertificationAuthoritySigner2NotValid", "The authority of signer certification sending the certificate is unknown for the second signer."), - DS24("WaitingTimeExpired", "Waiting time expired due to incomplete order"), - DS25("OrderFileDeleted", "The order file was deleted by the bank server"), - DS26("UserSignedMultipleTimes", "The same user has signed multiple times"), - DS27("UserNotYetActivated", "The user is not yet activated (technically)"), - DT01("InvalidDate", "Invalid date (eg, wrong or missing settlement date)"), - DT02("InvalidCreationDate", "Invalid creation date and time in Group Header (eg, historic date)"), - DT03("InvalidNonProcessingDate", "Invalid non bank processing date (eg, weekend or local public holiday)"), - DT04("FutureDateNotSupported", "Future date not supported"), - DT05("InvalidCutOffDate", "Associated message, payment information block or transaction was received after agreed processing cut-off date, i.e., date in the past."), - DT06("ExecutionDateChanged", "Execution Date has been modified in order for transaction to be processed"), - DU01("DuplicateMessageID", "Message Identification is not unique."), - DU02("DuplicatePaymentInformationID", "Payment Information Block is not unique."), - DU03("DuplicateTransaction", "Transaction is not unique."), - DU04("DuplicateEndToEndID", "End To End ID is not unique."), - DU05("DuplicateInstructionID", "Instruction ID is not unique."), - DUPL("DuplicatePayment", "Payment is a duplicate of another payment"), - ED01("CorrespondentBankNotPossible", "Correspondent bank not possible."), - ED03("BalanceInfoRequest", "Balance of payments complementary info is requested"), - ED05("SettlementFailed", "Settlement of the transaction has failed."), - ED06("SettlementSystemNotAvailable", "Interbank settlement system not available."), - EDTL("ExpiryDateTooLong", "Expiry date time of the request-to-pay is too far in the future."), - EDTR("ExpiryDateTimeReached", "Expiry date time of the request-to-pay is already reached."), - ERIN("ERIOptionNotSupported", "Extended Remittance Information (ERI) option is not supported."), - FF01("InvalidFileFormat", "File Format incomplete or invalid"), - FF02("SyntaxError", "Syntax error reason is provided as narrative information in the additional reason information."), - FF03("InvalidPaymentTypeInformation", "Payment Type Information is missing or invalid."), - FF04("InvalidServiceLevelCode", "Service Level code is missing or invalid"), - FF05("InvalidLocalInstrumentCode", "Local Instrument code is missing or invalid"), - FF06("InvalidCategoryPurposeCode", "Category Purpose code is missing or invalid"), - FF07("InvalidPurpose", "Purpose is missing or invalid"), - FF08("InvalidEndToEndId", "End to End Id missing or invalid"), - FF09("InvalidChequeNumber", "Cheque number missing or invalid"), - FF10("BankSystemProcessingError", "File or transaction cannot be processed due to technical issues at the bank side"), - FF11("ClearingRequestAborted", "Clearing request rejected due it being subject to an abort operation."), - FF12("OriginalTransactionNotEligibleForRequestedReturn", "Original payment is not eligible to be returned given its current status."), - FF13("RequestForCancellationNotFound", "No record of request for cancellation found."), - FOCR("FollowingCancellationRequest", "Return following a cancellation request."), - FR01("Fraud", "Returned as a result of fraud."), - FRAD("FraudulentOrigin", "Cancellation requested following a transaction that was originated fraudulently. The use of the FraudulentOrigin code should be governed by jurisdictions."), - G000("PaymentTransferredAndTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is tracked. No further updates will follow from the Status Originator."), - G001("PaymentTransferredAndNotTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is not tracked. No further updates will follow from the Status Originator."), - G002("CreditDebitNotConfirmed", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day. Update will follow from the Status Originator."), - G003("CreditPendingDocuments", "In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents. The Status Originator has requested creditor to provide additional documentation. Update will follow from the Status Originator."), - G004("CreditPendingFunds", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover. Update will follow from the Status Originator."), - G005("DeliveredWithServiceLevel", "Payment has been delivered to creditor agent with service level."), - G006("DeliveredWIthoutServiceLevel", "Payment has been delivered to creditor agent without service level."), - ID01("CorrespondingOriginalFileStillNotSent", "Signature file was sent to the bank but the corresponding original file has not been sent yet."), - IEDT("IncorrectExpiryDateTime", "Expiry date time of the request-to-pay is incorrect."), - IRNR("InitialRTPNeverReceived", "No initial request-to-pay has been received."), - MD01("NoMandate", "No Mandate"), - MD02("MissingMandatoryInformationInMandate", "Mandate related information data required by the scheme is missing."), - MD05("CollectionNotDue", "Creditor or creditor's agent should not have collected the direct debit"), - MD06("RefundRequestByEndCustomer", "Return of funds requested by end customer"), - MD07("EndCustomerDeceased", "End customer is deceased."), - MS02("NotSpecifiedReasonCustomerGenerated", "Reason has not been specified by end customer"), - MS03("NotSpecifiedReasonAgentGenerated", "Reason has not been specified by agent."), - NARR("Narrative", "Reason is provided as narrative information in the additional reason information."), - NERI("NoERI", "Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI."), - NOAR("NonAgreedRTP", "No existing agreement for receiving request-to-pay messages."), - NOAS("NoAnswerFromCustomer", "No response from Beneficiary."), - NOCM("NotCompliantGeneric", "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing."), - NOPG("NoPaymentGuarantee", "Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided."), - NRCH("PayerOrPayerRTPSPNotReachable", "Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable."), - PINS("TypeOfPaymentInstrumentNotSupported", "Type of payment requested in the request-to-pay is not supported by the payer."), - RC01("BankIdentifierIncorrect", "Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)."), - RC02("InvalidBankIdentifier", "Bank identifier is invalid or missing."), - RC03("InvalidDebtorBankIdentifier", "Debtor bank identifier is invalid or missing"), - RC04("InvalidCreditorBankIdentifier", "Creditor bank identifier is invalid or missing"), - RC05("InvalidBICIdentifier", "BIC identifier is invalid or missing."), - RC06("InvalidDebtorBICIdentifier", "Debtor BIC identifier is invalid or missing"), - RC07("InvalidCreditorBICIdentifier", "Creditor BIC identifier is invalid or missing"), - RC08("InvalidClearingSystemMemberIdentifier", "ClearingSystemMemberidentifier is invalid or missing."), - RC09("InvalidDebtorClearingSystemMemberIdentifier", "Debtor ClearingSystemMember identifier is invalid or missing"), - RC10("InvalidCreditorClearingSystemMemberIdentifier", "Creditor ClearingSystemMember identifier is invalid or missing"), - RC11("InvalidIntermediaryAgent", "Intermediary Agent is invalid or missing"), - RC12("MissingCreditorSchemeId", "Creditor Scheme Id is invalid or missing"), - RCON("RMessageConflict", "Conflict with R-Message"), - RECI("ReceiverCustomerInformation", "Further information regarding the intended recipient."), - REPR("RTPReceivedCanBeProcessed", "Request-to-pay has been received and can be processed further."), - RF01("NotUniqueTransactionReference", "Transaction reference is not unique within the message."), - RR01("MissingDebtorAccountOrIdentification", "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing"), - RR02("MissingDebtorNameOrAddress", "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing."), - RR03("MissingCreditorNameOrAddress", "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing."), - RR04("RegulatoryReason", "Regulatory Reason"), - RR05("RegulatoryInformationInvalid", "Regulatory or Central Bank Reporting information missing, incomplete or invalid."), - RR06("TaxInformationInvalid", "Tax information missing, incomplete or invalid."), - RR07("RemittanceInformationInvalid", "Remittance information structure does not comply with rules for payment type."), - RR08("RemittanceInformationTruncated", "Remittance information truncated to comply with rules for payment type."), - RR09("InvalidStructuredCreditorReference", "Structured creditor reference invalid or missing."), - RR10("InvalidCharacterSet", "Character set supplied not valid for the country and payment type."), - RR11("InvalidDebtorAgentServiceID", "Invalid or missing identification of a bank proprietary service."), - RR12("InvalidPartyID", "Invalid or missing identification required within a particular country or payment type."), - RTNS("RTPNotSupportedForDebtor", "Debtor does not support request-to-pay transactions."), - RUTA("ReturnUponUnableToApply", "Return following investigation request and no remediation possible."), - S000("ValidRequestForCancellationAcknowledged", "Request for Cancellation is acknowledged following validation."), - S001("UETRFlaggedForCancellation", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation."), - S002("NetworkStopOfUETR", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network."), - S003("RequestForCancellationForwarded", "Request for Cancellation has been forwarded to the payment processing/last payment processing agent."), - S004("RequestForCancellationDeliveryAcknowledgement", "Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent."), - SL01("SpecificServiceOfferedByDebtorAgent", "Due to specific service offered by the Debtor Agent."), - SL02("SpecificServiceOfferedByCreditorAgent", "Due to specific service offered by the Creditor Agent."), - SL03("ServiceofClearingSystem", "Due to a specific service offered by the clearing system."), - SL11("CreditorNotOnWhitelistOfDebtor", "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet). In the Whitelist the Debtor may list all allowed Creditors to debit Debtor bank account."), - SL12("CreditorOnBlacklistOfDebtor", "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”. In the Blacklist the Debtor may list all Creditors not allowed to debit Debtor bank account."), - SL13("MaximumNumberOfDirectDebitTransactionsExceeded", "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent."), - SL14("MaximumDirectDebitTransactionAmountExceeded", "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent."), - SPII("RTPServiceProviderIdentifierIncorrect", "Identifier of the request-to-pay service provider is incorrect."), - TA01("TransmissonAborted", "The transmission of the file was not successful – it had to be aborted (for technical reasons)"), - TD01("NoDataAvailable", "There is no data available (for download)"), - TD02("FileNonReadable", "The file cannot be read (e.g. unknown format)"), - TD03("IncorrectFileStructure", "The file format is incomplete or invalid"), - TK01("TokenInvalid", "Token is invalid."), - TK02("SenderTokenNotFound", "Token used for the sender does not exist."), - TK03("ReceiverTokenNotFound", "Token used for the receiver does not exist."), - TK09("TokenMissing", "Token required for request is missing."), - TKCM("TokenCounterpartyMismatch", "Token found with counterparty mismatch."), - TKSG("TokenSingleUse", "Single Use Token already used."), - TKSP("TokenSuspended", "Token found with suspended status."), - TKVE("TokenValueLimitExceeded", "Token found with value limit rule violation."), - TKXP("TokenExpired", "Token expired."), - TM01("InvalidCutOffTime", "Associated message, payment information block, or transaction was received after agreed processing cut-off time."), - TS01("TransmissionSuccessful", "The (technical) transmission of the file was successful."), - TS04("TransferToSignByHand", "The order was transferred to pass by accompanying note signed by hand"), - UCRD("UnknownCreditor", "Unknown Creditor."), - UPAY("UnduePayment", "Payment is not justified."), -} - -enum class ExternalPaymentGroupStatusCode(val isoCode: String, val description: String) { - ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."), - ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."), - ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement on the debtor's account has been completed."), - ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution."), - ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"), - ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."), - PART("PartiallyAccepted", "A number of transactions have been accepted, whereas another number of transactions have not yet achieved"), - PDNG("Pending", "Payment initiation or individual transaction included in the payment initiation is pending. Further checks and status update will be performed."), - RCVD("Received", "Payment initiation has been received by the receiving agent"), - RJCT("Rejected", "Payment initiation or individual transaction included in the payment initiation has been rejected."), -} - -enum class ExternalPaymentTransactionStatusCode(val isoCode: String, val description: String) { - ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."), - ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."), - ACFC("AcceptedFundsChecked", "Preceding check of technical validation and customer profile was successful and an automatic funds check was positive."), - ACIS("AcceptedandChequeIssued", "Payment instruction to issue a cheque has been accepted, and the cheque has been issued but not yet been deposited or cleared."), - ACPD("AcceptedClearingProcessed", "Status of transaction released from the Debtor Agent and accepted by the clearing."), - ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement completed."), - ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment instruction has been accepted for execution."), - ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"), - ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."), - ACWP("AcceptedWithoutPosting", "Payment instruction included in the credit transfer is accepted without being posted to the creditor customer’s account."), - BLCK("Blocked", "Payment transaction previously reported with status 'ACWP' is blocked, for example, funds will neither be posted to the Creditor's account, nor be returned to the Debtor."), - CANC("Cancelled", "Payment initiation has been successfully cancelled after having received a request for cancellation."), - CPUC("CashPickedUpByCreditor", "Cash has been picked up by the Creditor."), - PATC("PartiallyAcceptedTechnicalCorrect", "Payment initiation needs multiple authentications, where some but not yet all have been performed. Syntactical and semantical validations are successful."), - PDNG("Pending", "Payment instruction is pending. Further checks and status update will be performed."), - PRES("Presented", "Request for Payment has been presented to the Debtor."), - RCVD("Received", "Payment instruction has been received."), - RJCT("Rejected", "Payment instruction has been rejected."), -} diff --git a/ebics/src/main/kotlin/Iso20022Constants.kt b/ebics/src/main/kotlin/Iso20022Constants.kt deleted file mode 100644 index 961f5f90..00000000 --- a/ebics/src/main/kotlin/Iso20022Constants.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.ebics - -enum class HacAction(val description: String) { - FILE_UPLOAD("File submitted to the bank"), - FILE_DOWNLOAD("File downloaded from the bank"), - ES_UPLOAD("Electronic signature submitted to the bank"), - ES_DOWNLOAD("Electronic signature downloaded from the bank"), - ES_VERIFICATION("Signature verification"), - VEU_FORWARDING("Forwarding to EDS"), - VEU_VERIFICATION("EDS signature verification"), - VEU_VERIFICATION_END("VEU_VERIFICATION_END"), - VEU_CANCEL_ORDER("Cancellation of EDS order"), - ADDITIONAL("Additional information"), - ORDER_HAC_FINAL_POS("HAC end of order (positive)"), - ORDER_HAC_FINAL_NEG("ORDER_HAC_FINAL_NEG") -} \ No newline at end of file diff --git a/ebics/src/main/kotlin/XMLUtil.kt b/ebics/src/main/kotlin/XMLUtil.kt deleted file mode 100644 index b602adc0..00000000 --- a/ebics/src/main/kotlin/XMLUtil.kt +++ /dev/null @@ -1,212 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.ebics - -import io.ktor.http.* -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.w3c.dom.Document -import org.w3c.dom.Node -import org.w3c.dom.NodeList -import org.w3c.dom.ls.LSInput -import org.w3c.dom.ls.LSResourceResolver -import org.xml.sax.ErrorHandler -import org.xml.sax.InputSource -import org.xml.sax.SAXException -import org.xml.sax.SAXParseException -import java.io.* -import java.security.PrivateKey -import java.security.PublicKey -import java.security.interfaces.RSAPrivateCrtKey -import javax.xml.XMLConstants -import javax.xml.crypto.* -import javax.xml.crypto.dom.DOMURIReference -import javax.xml.crypto.dsig.* -import javax.xml.crypto.dsig.dom.DOMSignContext -import javax.xml.crypto.dsig.dom.DOMValidateContext -import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec -import javax.xml.crypto.dsig.spec.TransformParameterSpec -import javax.xml.namespace.NamespaceContext -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.OutputKeys -import javax.xml.transform.Source -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult -import javax.xml.transform.stream.StreamSource -import javax.xml.validation.SchemaFactory -import javax.xml.validation.Validator -import javax.xml.xpath.XPath -import javax.xml.xpath.XPathConstants -import javax.xml.xpath.XPathFactory - -private val logger: Logger = LoggerFactory.getLogger("libeufin-xml") - -/** - * This URI dereferencer allows handling the resource reference used for - * XML signatures in EBICS. - */ -private class EbicsSigUriDereferencer : URIDereferencer { - override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { - if (myRef !is DOMURIReference) - throw Exception("invalid type") - if (myRef.uri != "#xpointer(//*[@authenticate='true'])") - throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") - val xp: XPath = XPathFactory.newInstance().newXPath() - val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").evaluate( - myRef.here.ownerDocument, XPathConstants.NODESET - ) - if (nodeSet !is NodeList) - throw Exception("invalid type") - if (nodeSet.length <= 0) { - throw Exception("no nodes to sign") - } - val nodeList = ArrayList() - for (i in 0 until nodeSet.length) { - val node = nodeSet.item(i) - nodeList.add(node) - } - return NodeSetData { nodeList.iterator() } - } -} - -/** - * Helpers for dealing with XML in EBICS. - */ -object XMLUtil { - fun convertDomToBytes(document: Document): ByteArray { - val w = ByteArrayOutputStream() - val transformer = TransformerFactory.newInstance().newTransformer() - transformer.setOutputProperty(OutputKeys.STANDALONE, "yes") - transformer.transform(DOMSource(document), StreamResult(w)) - return w.toByteArray() - } - - /** - * Convert a node to a string without the XML declaration or - * indentation. - */ - fun convertNodeToString(node: Node): String { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") - /* Make string writer. */ - val sw = StringWriter() - /* Extract string. */ - t.transform(DOMSource(node), StreamResult(sw)) - return sw.toString() - } - - /** Parse [xml] into a XML DOM */ - fun parseIntoDom(xml: InputStream): Document { - val factory = DocumentBuilderFactory.newInstance().apply { - isNamespaceAware = true - } - val builder = factory.newDocumentBuilder() - return xml.use { - builder.parse(InputSource(it)) - } - } - - /** - * Sign an EBICS document with the authentication and identity signature. - */ - fun signEbicsDocument( - doc: Document, - signingPriv: PrivateKey, - withEbics3: Boolean = false - ) { - val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" - val authSigNode = XPathFactory.newInstance().newXPath() - .evaluate("/*[1]/$ns:AuthSignature", doc, XPathConstants.NODE) - if (authSigNode !is Node) - throw java.lang.Exception("no AuthSignature") - val fac = XMLSignatureFactory.getInstance("DOM") - val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) - val ref: Reference = - fac.newReference( - "#xpointer(//*[@authenticate='true'])", - fac.newDigestMethod(DigestMethod.SHA256, null), - listOf(c14n), - null, - null - ) - val canon: CanonicalizationMethod = - fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) - val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null) - val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) - val sig: XMLSignature = fac.newXMLSignature(si, null) - val dsc = DOMSignContext(signingPriv, authSigNode) - dsc.defaultNamespacePrefix = "ds" - dsc.uriDereferencer = EbicsSigUriDereferencer() - dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) - sig.sign(dsc) - val innerSig = authSigNode.firstChild - while (innerSig.hasChildNodes()) { - authSigNode.appendChild(innerSig.firstChild) - } - authSigNode.removeChild(innerSig) - } - - fun verifyEbicsDocument( - doc: Document, - signingPub: PublicKey, - withEbics3: Boolean = false - ): Boolean { - val doc2: Document = doc.cloneNode(true) as Document - val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" - val authSigNode = XPathFactory.newInstance().newXPath() - .evaluate("/*[1]/$ns:AuthSignature", doc2, XPathConstants.NODE) - if (authSigNode !is Node) - throw java.lang.Exception("no AuthSignature") - val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") - authSigNode.parentNode.insertBefore(sigEl, authSigNode) - while (authSigNode.hasChildNodes()) { - sigEl.appendChild(authSigNode.firstChild) - } - authSigNode.parentNode.removeChild(authSigNode) - val fac = XMLSignatureFactory.getInstance("DOM") - val dvc = DOMValidateContext(signingPub, sigEl) - dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) - dvc.uriDereferencer = EbicsSigUriDereferencer() - val sig = fac.unmarshalXMLSignature(dvc) - // FIXME: check that parameters are okay! - val valResult = sig.validate(dvc) - sig.signedInfo.references[0].validate(dvc) - return valResult - } - - fun getNodeFromXpath(doc: Document, query: String): Node { - val xpath = XPathFactory.newInstance().newXPath() - val ret = xpath.evaluate(query, doc, XPathConstants.NODE) - ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") - return ret as Node - } - - fun getStringFromXpath(doc: Document, query: String): String { - val xpath = XPathFactory.newInstance().newXPath() - val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String - if (ret.isEmpty()) { - throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") - } - return ret - } -} \ No newline at end of file diff --git a/ebics/src/main/kotlin/XmlCombinators.kt b/ebics/src/main/kotlin/XmlCombinators.kt deleted file mode 100644 index 902e21ae..00000000 --- a/ebics/src/main/kotlin/XmlCombinators.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2020, 2024 Taler Systems S.A. - * - * 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.ebics - -import org.w3c.dom.* -import java.io.InputStream -import java.io.StringWriter -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter -import javax.xml.parsers.* -import javax.xml.stream.XMLOutputFactory -import javax.xml.stream.XMLStreamWriter - -interface XmlBuilder { - fun el(path: String, lambda: XmlBuilder.() -> Unit = {}) - fun el(path: String, content: String) { - el(path) { - text(content) - } - } - fun attr(namespace: String, name: String, value: String) - fun attr(name: String, value: String) - fun text(content: String) - - companion object { - fun toString(root: String, f: XmlBuilder.() -> Unit): String { - val factory = XMLOutputFactory.newFactory() - val stream = StringWriter() - var writer = factory.createXMLStreamWriter(stream) - /** - * NOTE: commenting out because it wasn't obvious how to output the - * "standalone = 'yes' directive". Manual forge was therefore preferred. - */ - stream.write("") - XmlStreamBuilder(writer).el(root) { - this.f() - } - writer.writeEndDocument() - return stream.buffer.toString() - } - - fun toDom(root: String, schema: String?, f: XmlBuilder.() -> Unit): Document { - val factory = DocumentBuilderFactory.newInstance(); - factory.isNamespaceAware = true - val builder = factory.newDocumentBuilder(); - val doc = builder.newDocument(); - doc.setXmlVersion("1.0") - doc.setXmlStandalone(true) - val root = doc.createElementNS(schema, root) - doc.appendChild(root); - XmlDOMBuilder(doc, schema, root).f() - doc.normalize() - return doc - } - } -} - -private class XmlStreamBuilder(private val w: XMLStreamWriter): XmlBuilder { - override fun el(path: String, lambda: XmlBuilder.() -> Unit) { - path.splitToSequence('/').forEach { - w.writeStartElement(it) - } - lambda() - path.splitToSequence('/').forEach { - w.writeEndElement() - } - } - - override fun attr(namespace: String, name: String, value: String) { - w.writeAttribute(namespace, name, value) - } - - override fun attr(name: String, value: String) { - w.writeAttribute(name, value) - } - - override fun text(content: String) { - w.writeCharacters(content) - } -} - -private class XmlDOMBuilder(private val doc: Document, private val schema: String?, private var node: Element): XmlBuilder { - override fun el(path: String, lambda: XmlBuilder.() -> Unit) { - val current = node - path.splitToSequence('/').forEach { - val new = doc.createElementNS(schema, it) - node.appendChild(new) - node = new - } - lambda() - node = current - } - - override fun attr(namespace: String, name: String, value: String) { - node.setAttributeNS(namespace, name, value) - } - - override fun attr(name: String, value: String) { - node.setAttribute(name, value) - } - - override fun text(content: String) { - node.appendChild(doc.createTextNode(content)); - } -} - -class DestructionError(m: String) : Exception(m) - -private fun Element.childrenByTag(tag: String): Sequence = sequence { - for (i in 0..childNodes.length) { - val el = childNodes.item(i) - if (el !is Element) { - continue - } - if (el.localName != tag) { - continue - } - yield(el) - } -} - -class XmlDestructor internal constructor(private val el: Element) { - fun each(path: String, f: XmlDestructor.() -> Unit) { - el.childrenByTag(path).forEach { - f(XmlDestructor(it)) - } - } - - fun map(path: String, f: XmlDestructor.() -> T): List { - return el.childrenByTag(path).map { - f(XmlDestructor(it)) - }.toList() - } - - fun one(path: String): XmlDestructor { - val children = el.childrenByTag(path).iterator() - if (!children.hasNext()) { - throw DestructionError("expected a single $path child, got none instead at $el") - } - val el = children.next() - if (children.hasNext()) { - throw DestructionError("expected a single $path child, got ${children.asSequence() + 1} instead at $el") - } - return XmlDestructor(el) - } - fun opt(path: String): XmlDestructor? { - val children = el.childrenByTag(path).iterator() - if (!children.hasNext()) { - return null - } - val el = children.next() - if (children.hasNext()) { - throw DestructionError("expected an optional $path child, got ${children.asSequence().count() + 1} instead at $el") - } - return XmlDestructor(el) - } - - fun one(path: String, f: XmlDestructor.() -> T): T = f(one(path)) - fun opt(path: String, f: XmlDestructor.() -> T): T? = opt(path)?.run(f) - - fun text(): String = el.textContent - fun bool(): Boolean = el.textContent.toBoolean() - fun date(): LocalDate = LocalDate.parse(text(), DateTimeFormatter.ISO_DATE) - fun dateTime(): LocalDateTime = LocalDateTime.parse(text(), DateTimeFormatter.ISO_DATE_TIME) - inline fun > enum(): T = java.lang.Enum.valueOf(T::class.java, text()) - - fun attr(index: String): String = el.getAttribute(index) - - companion object { - fun fromStream(xml: InputStream, root: String, f: XmlDestructor.() -> T): T { - val doc = XMLUtil.parseIntoDom(xml) - return fromDoc(doc, root, f) - } - - fun fromDoc(doc: Document, root: String, f: XmlDestructor.() -> T): T { - if (doc.documentElement.tagName != root) { - throw DestructionError("expected root '$root' got '${doc.documentElement.tagName}'") - } - val destr = XmlDestructor(doc.documentElement) - return f(destr) - } - } -} - -fun destructXml(xml: InputStream, root: String, f: XmlDestructor.() -> T): T - = XmlDestructor.fromStream(xml, root, f) diff --git a/ebics/src/main/resources/version.txt b/ebics/src/main/resources/version.txt deleted file mode 100644 index 359d0539..00000000 --- a/ebics/src/main/resources/version.txt +++ /dev/null @@ -1 +0,0 @@ -v0.9.4-git-8aeffb3f \ No newline at end of file diff --git a/ebics/src/test/kotlin/XmlCombinatorsTest.kt b/ebics/src/test/kotlin/XmlCombinatorsTest.kt deleted file mode 100644 index 7e7efb65..00000000 --- a/ebics/src/test/kotlin/XmlCombinatorsTest.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 - * - */ - -import org.junit.Test -import tech.libeufin.ebics.XmlBuilder -import tech.libeufin.ebics.XMLUtil -import kotlin.test.assertEquals - -class XmlCombinatorsTest { - fun testBuilder(expected: String, root: String, builder: XmlBuilder.() -> Unit) { - val toString = XmlBuilder.toString(root, builder) - val toDom = XmlBuilder.toDom(root, null, builder) - //assertEquals(expected, toString) TODO fix empty tag being closed only with toString - assertEquals(expected, XMLUtil.convertDomToBytes(toDom).toString(Charsets.UTF_8)) - } - - @Test - fun testWithModularity() { - fun module(base: XmlBuilder) { - base.el("module") - } - testBuilder( - "", - "root" - ) { - module(this) - } - } - - @Test - fun testWithIterable() { - testBuilder( - "111222333444555666777888999101010", - "iterable" - ) { - el("endOfDocument") { - for (i in 1..10) - el("e$i/e$i$i", "$i$i$i") - } - } - } - - @Test - fun testBasicXmlBuilding() { - testBuilder( - "", - "ebicsRequest" - ) { - attr("version", "H004") - el("a/b/c") { - attr("attribute-of", "c") - el("d/e/f") { - attr("nested", "true") - el("g/h") - } - } - el("one_more") - } - } -} diff --git a/ebics/src/test/kotlin/XmlUtilTest.kt b/ebics/src/test/kotlin/XmlUtilTest.kt deleted file mode 100644 index 1ec63538..00000000 --- a/ebics/src/test/kotlin/XmlUtilTest.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 - * - */ - -import org.junit.Assert.assertTrue -import org.junit.Test -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.decodeBase64 -import tech.libeufin.ebics.XMLUtil -import java.security.KeyPairGenerator -import javax.xml.transform.stream.StreamSource - -class XmlUtilTest { - - @Test - fun basicSigningTest() { - val doc = XMLUtil.parseIntoDom(""" - - - Hello World - - """.trimIndent().toByteArray().inputStream()) - val kpg = KeyPairGenerator.getInstance("RSA") - kpg.initialize(2048) - val pair = kpg.genKeyPair() - val otherPair = kpg.genKeyPair() - XMLUtil.signEbicsDocument(doc, pair.private) - kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) - kotlin.test.assertFalse(XMLUtil.verifyEbicsDocument(doc, otherPair.public)) - } - - @Test - fun multiAuthSigningTest() { - val doc = XMLUtil.parseIntoDom(""" - - - Hello World - Another one! - - """.trimIndent().toByteArray().inputStream()) - val kpg = KeyPairGenerator.getInstance("RSA") - kpg.initialize(2048) - val pair = kpg.genKeyPair() - XMLUtil.signEbicsDocument(doc, pair.private) - kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) - } - - @Test - fun testRefSignature() { - val classLoader = ClassLoader.getSystemClassLoader() - val docText = classLoader.getResourceAsStream("signature1/doc.xml") - val doc = XMLUtil.parseIntoDom(docText) - val keyStream = classLoader.getResourceAsStream("signature1/public_key.txt") - val keyBytes = keyStream.decodeBase64().readAllBytes() - val key = CryptoUtil.loadRsaPublicKey(keyBytes) - assertTrue(XMLUtil.verifyEbicsDocument(doc, key)) - } -} \ No newline at end of file diff --git a/ebics/src/test/resources/signature1/doc.xml b/ebics/src/test/resources/signature1/doc.xml deleted file mode 100644 index 271f8429..00000000 --- a/ebics/src/test/resources/signature1/doc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - qiFUoCn9kE0zSidyraO2Br/wn3/XyvWObJZ0aLIBXyA=LupLyRUJIuk0kCRwpFj4fpen2MI7Jw0BI944agwzXHfSDfq0Pp8h3sub6eSsKIAq7ekT3z+mlfMc - VFaKRi4B7kv4ja/URiYCKKbChQU2+kMGDvsncx9VcpcFrqAbWPmE9JXD2W2YW9OSkJ1tAZxZlZwS - A8KcvluV1wGEBuakHL2t3GqFPQEfKW4l8GYTjHh/w9jBve5d8tvMOjGtoyNemZGrVlzBxO9+hwbw - 8UFUCDA00dCjFDUHOnyAbBYsGzoaQyZprDn3iYDvlBz243zAN98PIKDclxlUEmkuF+JhrhCRjT9l - +JJxrELGHaDkFVadR4kaPdWPsbDaV0/2Fzc4Qg== - Hello World - \ No newline at end of file diff --git a/ebics/src/test/resources/signature1/public_key.txt b/ebics/src/test/resources/signature1/public_key.txt deleted file mode 100644 index 6d52df58..00000000 --- a/ebics/src/test/resources/signature1/public_key.txt +++ /dev/null @@ -1 +0,0 @@ -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqpUpetHZYdMjnaG544iSLZ5SnxlV4F/eQsIckG3mvMaXCQsY4rUTfJyle/fTZ0xGbjCUXCsbl1wkz8eB6chaX2LsHYDGiu/xNnU1nddAVB+5kkA5AIGncT9NVhdOgmpnZY/tae9qtZfCPAvbI0sGYQHea0pwyJ/hUnRJiMOjSRgIXALIvGVNqxe4U5ffLXFIUapTK2hOuhUH9BwDSK+mVR6gw0vDT05Z38sEpTeKUqJywL5cPSFIV+AN4ErSvsXNkTKUcbDxhGzOh/oTjTkz1kFFKe4ijPkSRkpK2sJMyAIretBKOK8SDICnsSrIh0YAcd6yTHQ3CeEjW4t0ZBULOQIDAQAB \ No newline at end of file diff --git a/nexus/build.gradle b/nexus/build.gradle index 58e7858f..56eda968 100644 --- a/nexus/build.gradle +++ b/nexus/build.gradle @@ -22,7 +22,6 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") implementation(project(":common")) - implementation(project(":ebics")) // Command line parsing implementation("com.github.ajalt.clikt:clikt:$clikt_version") diff --git a/nexus/codegen.py b/nexus/codegen.py new file mode 100644 index 00000000..634a9842 --- /dev/null +++ b/nexus/codegen.py @@ -0,0 +1,72 @@ +# Update EBICS constants file using latest external code sets files + +import polars as pl +import requests +from io import BytesIO +from zipfile import ZipFile + + +def iso20022codegen(): + # Get XLSX zip file from server + r = requests.get( + "https://www.iso20022.org/sites/default/files/media/file/ExternalCodeSets_XLSX.zip" + ) + assert r.status_code == 200 + + # Unzip the XLSX file + zip = ZipFile(BytesIO(r.content)) + files = zip.namelist() + assert len(files) == 1 + file = zip.open(files[0]) + + # Parse excel + df = pl.read_excel(file, sheet_name="AllCodeSets") + + def extractCodeSet(setName: str, className: str) -> str: + out = f"enum class {className}(val isoCode: String, val description: String) {{" + + for row in df.filter(pl.col("Code Set") == setName).sort("Code Value").rows(named=True): + (value, isoCode, description) = ( + row["Code Value"], + row["Code Name"], + row["Code Definition"].split("\n", 1)[0].strip(), + ) + out += f'\n\t{value}("{isoCode}", "{description}"),' + + out += "\n}" + return out + + # Write kotlin file + kt = f"""/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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 + * + */ + +// THIS FILE IS GENERATED, DO NOT EDIT + +package tech.libeufin.nexus + +{extractCodeSet("ExternalStatusReason1Code", "ExternalStatusReasonCode")} + +{extractCodeSet("ExternalPaymentGroupStatus1Code", "ExternalPaymentGroupStatusCode")} + +{extractCodeSet("ExternalPaymentTransactionStatus1Code", "ExternalPaymentTransactionStatusCode")} +""" + with open("src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt", "w") as file1: + file1.write(kt) + +iso20022codegen() diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt index d6ae13f6..a089012a 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -27,7 +27,6 @@ import io.ktor.client.* import io.ktor.client.plugins.* import kotlinx.coroutines.* import tech.libeufin.common.* -import tech.libeufin.ebics.* import tech.libeufin.nexus.ebics.* import java.io.IOException import java.io.InputStream @@ -307,7 +306,7 @@ private fun ingestDocuments( private suspend fun fetchDocuments( db: Database, ctx: FetchContext, - docs: List + docs: List ): Boolean { val lastExecutionTime: Instant? = ctx.pinnedStart return docs.all { doc -> @@ -334,7 +333,7 @@ private suspend fun fetchDocuments( } } -enum class Document { +enum class EbicsDocument { /// EBICS acknowledgement - CustomerAcknowledgement HAC pain.002 acknowledgement, /// Payment status - CustomerPaymentStatusReport pain.002 @@ -379,10 +378,10 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") { help = "This flag fetches only once from the bank and returns, " + "ignoring the 'frequency' configuration value" ).flag(default = false) - private val documents: Set by argument( + private val documents: Set by argument( help = "Which documents should be fetched? If none are specified, all supported documents will be fetched", - helpTags = Document.entries.map { Pair(it.name, it.shortDescription()) }.toMap() - ).enum().multiple().unique() + helpTags = EbicsDocument.entries.map { Pair(it.name, it.shortDescription()) }.toMap() + ).enum().multiple().unique() private val pinnedStart by option( help = "Constant YYYY-MM-DD date for the earliest document" + " to download (only consumed in --transient mode). The" + @@ -419,7 +418,7 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") { null, FileLogger(ebicsLog) ) - val docs = if (documents.isEmpty()) Document.entries else documents.toList() + val docs = if (documents.isEmpty()) EbicsDocument.entries else documents.toList() if (transient) { logger.info("Transient mode: fetching once and returning.") val pinnedStartVal = pinnedStart diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt index d0e1a7f3..58e43e2d 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt @@ -26,7 +26,6 @@ import io.ktor.client.* import io.ktor.client.plugins.* import tech.libeufin.common.* import tech.libeufin.common.crypto.* -import tech.libeufin.ebics.* import tech.libeufin.nexus.ebics.* import java.nio.file.* import java.time.Instant diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt index 946a1808..e2b673dd 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -19,7 +19,6 @@ package tech.libeufin.nexus import tech.libeufin.common.* -import tech.libeufin.ebics.* import java.io.InputStream import java.net.URLEncoder import java.time.* diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt new file mode 100644 index 00000000..bc2c7eae --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022CodeSets.kt @@ -0,0 +1,330 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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 + * + */ + +// THIS FILE IS GENERATED, DO NOT EDIT + +package tech.libeufin.nexus + +enum class ExternalStatusReasonCode(val isoCode: String, val description: String) { + AB01("AbortedClearingTimeout", "Clearing process aborted due to timeout."), + AB02("AbortedClearingFatalError", "Clearing process aborted due to a fatal error."), + AB03("AbortedSettlementTimeout", "Settlement aborted due to timeout."), + AB04("AbortedSettlementFatalError", "Settlement process aborted due to a fatal error."), + AB05("TimeoutCreditorAgent", "Transaction stopped due to timeout at the Creditor Agent."), + AB06("TimeoutInstructedAgent", "Transaction stopped due to timeout at the Instructed Agent."), + AB07("OfflineAgent", "Agent of message is not online."), + AB08("OfflineCreditorAgent", "Creditor Agent is not online."), + AB09("ErrorCreditorAgent", "Transaction stopped due to error at the Creditor Agent."), + AB10("ErrorInstructedAgent", "Transaction stopped due to error at the Instructed Agent."), + AB11("TimeoutDebtorAgent", "Transaction stopped due to timeout at the Debtor Agent."), + AC01("IncorrectAccountNumber", "Account number is invalid or missing."), + AC02("InvalidDebtorAccountNumber", "Debtor account number invalid or missing"), + AC03("InvalidCreditorAccountNumber", "Creditor account number invalid or missing"), + AC04("ClosedAccountNumber", "Account number specified has been closed on the bank of account's books."), + AC05("ClosedDebtorAccountNumber", "Debtor account number closed"), + AC06("BlockedAccount", "Account specified is blocked, prohibiting posting of transactions against it."), + AC07("ClosedCreditorAccountNumber", "Creditor account number closed"), + AC08("InvalidBranchCode", "Branch code is invalid or missing"), + AC09("InvalidAccountCurrency", "Account currency is invalid or missing"), + AC10("InvalidDebtorAccountCurrency", "Debtor account currency is invalid or missing"), + AC11("InvalidCreditorAccountCurrency", "Creditor account currency is invalid or missing"), + AC12("InvalidAccountType", "Account type missing or invalid."), + AC13("InvalidDebtorAccountType", "Debtor account type missing or invalid"), + AC14("InvalidCreditorAccountType", "Creditor account type missing or invalid"), + AC15("AccountDetailsChanged", "The account details for the counterparty have changed."), + AC16("CardNumberInvalid", "Credit or debit card number is invalid."), + AEXR("AlreadyExpiredRTP", "Request-to-pay Expiry Date and Time has already passed."), + AG01("TransactionForbidden", "Transaction forbidden on this type of account (formerly NoAgreement)"), + AG02("InvalidBankOperationCode", "Bank Operation code specified in the message is not valid for receiver"), + AG03("TransactionNotSupported", "Transaction type not supported/authorized on this account"), + AG04("InvalidAgentCountry", "Agent country code is missing or invalid."), + AG05("InvalidDebtorAgentCountry", "Debtor agent country code is missing or invalid"), + AG06("InvalidCreditorAgentCountry", "Creditor agent country code is missing or invalid"), + AG07("UnsuccesfulDirectDebit", "Debtor account cannot be debited for a generic reason."), + AG08("InvalidAccessRights", "Transaction failed due to invalid or missing user or access right"), + AG09("PaymentNotReceived", "Original payment never received."), + AG10("AgentSuspended", "Agent of message is suspended from the Real Time Payment system."), + AG11("CreditorAgentSuspended", "Creditor Agent of message is suspended from the Real Time Payment system."), + AG12("NotAllowedBookTransfer", "Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed."), + AG13("ForbiddenReturnPayment", "Returned payments derived from previously returned transactions are not allowed."), + AGNT("IncorrectAgent", "Agent in the payment workflow is incorrect"), + ALAC("AlreadyAcceptedRTP", "Request-to-pay has already been accepted by the Debtor."), + AM01("ZeroAmount", "Specified message amount is equal to zero"), + AM02("NotAllowedAmount", "Specific transaction/message amount is greater than allowed maximum"), + AM03("NotAllowedCurrency", "Specified message amount is an non processable currency outside of existing agreement"), + AM04("InsufficientFunds", "Amount of funds available to cover specified message amount is insufficient."), + AM05("Duplication", "Duplication"), + AM06("TooLowAmount", "Specified transaction amount is less than agreed minimum."), + AM07("BlockedAmount", "Amount specified in message has been blocked by regulatory authorities."), + AM09("WrongAmount", "Amount received is not the amount agreed or expected"), + AM10("InvalidControlSum", "Sum of instructed amounts does not equal the control sum."), + AM11("InvalidTransactionCurrency", "Transaction currency is invalid or missing"), + AM12("InvalidAmount", "Amount is invalid or missing"), + AM13("AmountExceedsClearingSystemLimit", "Transaction amount exceeds limits set by clearing system"), + AM14("AmountExceedsAgreedLimit", "Transaction amount exceeds limits agreed between bank and client"), + AM15("AmountBelowClearingSystemMinimum", "Transaction amount below minimum set by clearing system"), + AM16("InvalidGroupControlSum", "Control Sum at the Group level is invalid"), + AM17("InvalidPaymentInfoControlSum", "Control Sum at the Payment Information level is invalid"), + AM18("InvalidNumberOfTransactions", "Number of transactions is invalid or missing."), + AM19("InvalidGroupNumberOfTransactions", "Number of transactions at the Group level is invalid or missing"), + AM20("InvalidPaymentInfoNumberOfTransactions", "Number of transactions at the Payment Information level is invalid"), + AM21("LimitExceeded", "Transaction amount exceeds limits agreed between bank and client."), + AM22("ZeroAmountNotApplied", "Unable to apply zero amount to designated account. For example, where the rules of a service allow the use of zero amount payments, however the back-office system is unable to apply the funds to the account. If the rules of a service prohibit the use of zero amount payments, then code AM01 is used to report the error condition."), + AM23("AmountExceedsSettlementLimit", "Transaction amount exceeds settlement limit."), + APAR("AlreadyPaidRTP", "Request To Pay has already been paid by the Debtor."), + ARFR("AlreadyRefusedRTP", "Request-to-pay has already been refused by the Debtor."), + ARJR("AlreadyRejectedRTP", "Request-to-pay has already been rejected."), + ATNS("AttachementsNotSupported", "Attachments to the request-to-pay are not supported."), + BE01("InconsistenWithEndCustomer", "Identification of end customer is not consistent with associated account number. (formerly CreditorConsistency)."), + BE04("MissingCreditorAddress", "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)."), + BE05("UnrecognisedInitiatingParty", "Party who initiated the message is not recognised by the end customer"), + BE06("UnknownEndCustomer", "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books"), + BE07("MissingDebtorAddress", "Specification of debtor's address, which is required for payment, is missing/not correct."), + BE08("MissingDebtorName", "Debtor name is missing"), + BE09("InvalidCountry", "Country code is missing or Invalid."), + BE10("InvalidDebtorCountry", "Debtor country code is missing or invalid"), + BE11("InvalidCreditorCountry", "Creditor country code is missing or invalid"), + BE12("InvalidCountryOfResidence", "Country code of residence is missing or Invalid."), + BE13("InvalidDebtorCountryOfResidence", "Country code of debtor's residence is missing or Invalid"), + BE14("InvalidCreditorCountryOfResidence", "Country code of creditor's residence is missing or Invalid"), + BE15("InvalidIdentificationCode", "Identification code missing or invalid."), + BE16("InvalidDebtorIdentificationCode", "Debtor or Ultimate Debtor identification code missing or invalid"), + BE17("InvalidCreditorIdentificationCode", "Creditor or Ultimate Creditor identification code missing or invalid"), + BE18("InvalidContactDetails", "Contact details missing or invalid"), + BE19("InvalidChargeBearerCode", "Charge bearer code for transaction type is invalid"), + BE20("InvalidNameLength", "Name length exceeds local rules for payment type."), + BE21("MissingName", "Name missing or invalid. Generic usage if cannot specifically identify debtor or creditor."), + BE22("MissingCreditorName", "Creditor name is missing"), + BE23("AccountProxyInvalid", "Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid."), + CERI("CheckERI", "Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI."), + CH03("RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture", "Value in Requested Execution Date or Requested Collection Date is too far in the future"), + CH04("RequestedExecutionDateOrRequestedCollectionDateTooFarInPast", "Value in Requested Execution Date or Requested Collection Date is too far in the past"), + CH07("ElementIsNotToBeUsedAtB-andC-Level", "Element is not to be used at B- and C-Level"), + CH09("MandateChangesNotAllowed", "Mandate changes are not allowed"), + CH10("InformationOnMandateChangesMissing", "Information on mandate changes are missing"), + CH11("CreditorIdentifierIncorrect", "Value in Creditor Identifier is incorrect"), + CH12("CreditorIdentifierNotUnambiguouslyAtTransaction-Level", "Creditor Identifier is ambiguous at Transaction Level"), + CH13("OriginalDebtorAccountIsNotToBeUsed", "Original Debtor Account is not to be used"), + CH14("OriginalDebtorAgentIsNotToBeUsed", "Original Debtor Agent is not to be used"), + CH15("ElementContentIncludesMoreThan140Characters", "Content Remittance Information/Structured includes more than 140 characters"), + CH16("ElementContentFormallyIncorrect", "Content is incorrect"), + CH17("ElementNotAdmitted", "Element is not allowed"), + CH19("ValuesWillBeSetToNextTARGETday", "Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day"), + CH20("DecimalPointsNotCompatibleWithCurrency", "Number of decimal points not compatible with the currency"), + CH21("RequiredCompulsoryElementMissing", "Mandatory element is missing"), + CH22("COREandB2BwithinOnemessage", "SDD CORE and B2B not permitted within one message"), + CHQC("ChequeSettledOnCreditorAccount", "Cheque has been presented in cheque clearing and settled on the creditor’s account."), + CN01("AuthorisationCancelled", "Authorisation is cancelled."), + CNOR("CreditorBankIsNotRegistered", "Creditor bank is not registered under this BIC in the CSM"), + CURR("IncorrectCurrency", "Currency of the payment is incorrect"), + CUST("RequestedByCustomer", "Cancellation requested by the Debtor"), + DC02("SettlementNotReceived", "Rejection of a payment due to covering FI settlement not being received."), + DNOR("DebtorBankIsNotRegistered", "Debtor bank is not registered under this BIC in the CSM"), + DS01("ElectronicSignaturesCorrect", "The electronic signature(s) is/are correct"), + DS02("OrderCancelled", "An authorized user has cancelled the order"), + DS03("OrderNotCancelled", "The user’s attempt to cancel the order was not successful"), + DS04("OrderRejected", "The order was rejected by the bank side (for reasons concerning content)"), + DS05("OrderForwardedForPostprocessing", "The order was correct and could be forwarded for postprocessing"), + DS06("TransferOrder", "The order was transferred to VEU"), + DS07("ProcessingOK", "All actions concerning the order could be done by the EBICS bank server"), + DS08("DecompressionError", "The decompression of the file was not successful"), + DS09("DecryptionError", "The decryption of the file was not successful"), + DS0A("DataSignRequested", "Data signature is required."), + DS0B("UnknownDataSignFormat", "Data signature for the format is not available or invalid."), + DS0C("SignerCertificateRevoked", "The signer certificate is revoked."), + DS0D("SignerCertificateNotValid", "The signer certificate is not valid (revoked or not active)."), + DS0E("IncorrectSignerCertificate", "The signer certificate is not present."), + DS0F("SignerCertificationAuthoritySignerNotValid", "The authority of the signer certification sending the certificate is unknown."), + DS0G("NotAllowedPayment", "Signer is not allowed to sign this operation type."), + DS0H("NotAllowedAccount", "Signer is not allowed to sign for this account."), + DS0K("NotAllowedNumberOfTransaction", "The number of transaction is over the number allowed for this signer."), + DS10("Signer1CertificateRevoked", "The certificate is revoked for the first signer."), + DS11("Signer1CertificateNotValid", "The certificate is not valid (revoked or not active) for the first signer."), + DS12("IncorrectSigner1Certificate", "The certificate is not present for the first signer."), + DS13("SignerCertificationAuthoritySigner1NotValid", "The authority of signer certification sending the certificate is unknown for the first signer."), + DS14("UserDoesNotExist", "The user is unknown on the server"), + DS15("IdenticalSignatureFound", "The same signature has already been sent to the bank"), + DS16("PublicKeyVersionIncorrect", "The public key version is not correct. This code is returned when a customer sends signature files to the financial institution after conversion from an older program version (old ES format) to a new program version (new ES format) without having carried out re-initialisation with regard to a public key change."), + DS17("DifferentOrderDataInSignatures", "Order data and signatures don’t match"), + DS18("RepeatOrder", "File cannot be tested, the complete order has to be repeated. This code is returned in the event of a malfunction during the signature check, e.g. not enough storage space."), + DS19("ElectronicSignatureRightsInsufficient", "The user’s rights (concerning his signature) are insufficient to execute the order"), + DS20("Signer2CertificateRevoked", "The certificate is revoked for the second signer."), + DS21("Signer2CertificateNotValid", "The certificate is not valid (revoked or not active) for the second signer."), + DS22("IncorrectSigner2Certificate", "The certificate is not present for the second signer."), + DS23("SignerCertificationAuthoritySigner2NotValid", "The authority of signer certification sending the certificate is unknown for the second signer."), + DS24("WaitingTimeExpired", "Waiting time expired due to incomplete order"), + DS25("OrderFileDeleted", "The order file was deleted by the bank server"), + DS26("UserSignedMultipleTimes", "The same user has signed multiple times"), + DS27("UserNotYetActivated", "The user is not yet activated (technically)"), + DT01("InvalidDate", "Invalid date (eg, wrong or missing settlement date)"), + DT02("InvalidCreationDate", "Invalid creation date and time in Group Header (eg, historic date)"), + DT03("InvalidNonProcessingDate", "Invalid non bank processing date (eg, weekend or local public holiday)"), + DT04("FutureDateNotSupported", "Future date not supported"), + DT05("InvalidCutOffDate", "Associated message, payment information block or transaction was received after agreed processing cut-off date, i.e., date in the past."), + DT06("ExecutionDateChanged", "Execution Date has been modified in order for transaction to be processed"), + DU01("DuplicateMessageID", "Message Identification is not unique."), + DU02("DuplicatePaymentInformationID", "Payment Information Block is not unique."), + DU03("DuplicateTransaction", "Transaction is not unique."), + DU04("DuplicateEndToEndID", "End To End ID is not unique."), + DU05("DuplicateInstructionID", "Instruction ID is not unique."), + DUPL("DuplicatePayment", "Payment is a duplicate of another payment"), + ED01("CorrespondentBankNotPossible", "Correspondent bank not possible."), + ED03("BalanceInfoRequest", "Balance of payments complementary info is requested"), + ED05("SettlementFailed", "Settlement of the transaction has failed."), + ED06("SettlementSystemNotAvailable", "Interbank settlement system not available."), + EDTL("ExpiryDateTooLong", "Expiry date time of the request-to-pay is too far in the future."), + EDTR("ExpiryDateTimeReached", "Expiry date time of the request-to-pay is already reached."), + ERIN("ERIOptionNotSupported", "Extended Remittance Information (ERI) option is not supported."), + FF01("InvalidFileFormat", "File Format incomplete or invalid"), + FF02("SyntaxError", "Syntax error reason is provided as narrative information in the additional reason information."), + FF03("InvalidPaymentTypeInformation", "Payment Type Information is missing or invalid."), + FF04("InvalidServiceLevelCode", "Service Level code is missing or invalid"), + FF05("InvalidLocalInstrumentCode", "Local Instrument code is missing or invalid"), + FF06("InvalidCategoryPurposeCode", "Category Purpose code is missing or invalid"), + FF07("InvalidPurpose", "Purpose is missing or invalid"), + FF08("InvalidEndToEndId", "End to End Id missing or invalid"), + FF09("InvalidChequeNumber", "Cheque number missing or invalid"), + FF10("BankSystemProcessingError", "File or transaction cannot be processed due to technical issues at the bank side"), + FF11("ClearingRequestAborted", "Clearing request rejected due it being subject to an abort operation."), + FF12("OriginalTransactionNotEligibleForRequestedReturn", "Original payment is not eligible to be returned given its current status."), + FF13("RequestForCancellationNotFound", "No record of request for cancellation found."), + FOCR("FollowingCancellationRequest", "Return following a cancellation request."), + FR01("Fraud", "Returned as a result of fraud."), + FRAD("FraudulentOrigin", "Cancellation requested following a transaction that was originated fraudulently. The use of the FraudulentOrigin code should be governed by jurisdictions."), + G000("PaymentTransferredAndTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is tracked. No further updates will follow from the Status Originator."), + G001("PaymentTransferredAndNotTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is not tracked. No further updates will follow from the Status Originator."), + G002("CreditDebitNotConfirmed", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day. Update will follow from the Status Originator."), + G003("CreditPendingDocuments", "In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents. The Status Originator has requested creditor to provide additional documentation. Update will follow from the Status Originator."), + G004("CreditPendingFunds", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover. Update will follow from the Status Originator."), + G005("DeliveredWithServiceLevel", "Payment has been delivered to creditor agent with service level."), + G006("DeliveredWIthoutServiceLevel", "Payment has been delivered to creditor agent without service level."), + ID01("CorrespondingOriginalFileStillNotSent", "Signature file was sent to the bank but the corresponding original file has not been sent yet."), + IEDT("IncorrectExpiryDateTime", "Expiry date time of the request-to-pay is incorrect."), + IRNR("InitialRTPNeverReceived", "No initial request-to-pay has been received."), + MD01("NoMandate", "No Mandate"), + MD02("MissingMandatoryInformationInMandate", "Mandate related information data required by the scheme is missing."), + MD05("CollectionNotDue", "Creditor or creditor's agent should not have collected the direct debit"), + MD06("RefundRequestByEndCustomer", "Return of funds requested by end customer"), + MD07("EndCustomerDeceased", "End customer is deceased."), + MS02("NotSpecifiedReasonCustomerGenerated", "Reason has not been specified by end customer"), + MS03("NotSpecifiedReasonAgentGenerated", "Reason has not been specified by agent."), + NARR("Narrative", "Reason is provided as narrative information in the additional reason information."), + NERI("NoERI", "Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI."), + NOAR("NonAgreedRTP", "No existing agreement for receiving request-to-pay messages."), + NOAS("NoAnswerFromCustomer", "No response from Beneficiary."), + NOCM("NotCompliantGeneric", "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing."), + NOPG("NoPaymentGuarantee", "Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided."), + NRCH("PayerOrPayerRTPSPNotReachable", "Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable."), + PINS("TypeOfPaymentInstrumentNotSupported", "Type of payment requested in the request-to-pay is not supported by the payer."), + RC01("BankIdentifierIncorrect", "Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)."), + RC02("InvalidBankIdentifier", "Bank identifier is invalid or missing."), + RC03("InvalidDebtorBankIdentifier", "Debtor bank identifier is invalid or missing"), + RC04("InvalidCreditorBankIdentifier", "Creditor bank identifier is invalid or missing"), + RC05("InvalidBICIdentifier", "BIC identifier is invalid or missing."), + RC06("InvalidDebtorBICIdentifier", "Debtor BIC identifier is invalid or missing"), + RC07("InvalidCreditorBICIdentifier", "Creditor BIC identifier is invalid or missing"), + RC08("InvalidClearingSystemMemberIdentifier", "ClearingSystemMemberidentifier is invalid or missing."), + RC09("InvalidDebtorClearingSystemMemberIdentifier", "Debtor ClearingSystemMember identifier is invalid or missing"), + RC10("InvalidCreditorClearingSystemMemberIdentifier", "Creditor ClearingSystemMember identifier is invalid or missing"), + RC11("InvalidIntermediaryAgent", "Intermediary Agent is invalid or missing"), + RC12("MissingCreditorSchemeId", "Creditor Scheme Id is invalid or missing"), + RCON("RMessageConflict", "Conflict with R-Message"), + RECI("ReceiverCustomerInformation", "Further information regarding the intended recipient."), + REPR("RTPReceivedCanBeProcessed", "Request-to-pay has been received and can be processed further."), + RF01("NotUniqueTransactionReference", "Transaction reference is not unique within the message."), + RR01("MissingDebtorAccountOrIdentification", "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing"), + RR02("MissingDebtorNameOrAddress", "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing."), + RR03("MissingCreditorNameOrAddress", "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing."), + RR04("RegulatoryReason", "Regulatory Reason"), + RR05("RegulatoryInformationInvalid", "Regulatory or Central Bank Reporting information missing, incomplete or invalid."), + RR06("TaxInformationInvalid", "Tax information missing, incomplete or invalid."), + RR07("RemittanceInformationInvalid", "Remittance information structure does not comply with rules for payment type."), + RR08("RemittanceInformationTruncated", "Remittance information truncated to comply with rules for payment type."), + RR09("InvalidStructuredCreditorReference", "Structured creditor reference invalid or missing."), + RR10("InvalidCharacterSet", "Character set supplied not valid for the country and payment type."), + RR11("InvalidDebtorAgentServiceID", "Invalid or missing identification of a bank proprietary service."), + RR12("InvalidPartyID", "Invalid or missing identification required within a particular country or payment type."), + RTNS("RTPNotSupportedForDebtor", "Debtor does not support request-to-pay transactions."), + RUTA("ReturnUponUnableToApply", "Return following investigation request and no remediation possible."), + S000("ValidRequestForCancellationAcknowledged", "Request for Cancellation is acknowledged following validation."), + S001("UETRFlaggedForCancellation", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation."), + S002("NetworkStopOfUETR", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network."), + S003("RequestForCancellationForwarded", "Request for Cancellation has been forwarded to the payment processing/last payment processing agent."), + S004("RequestForCancellationDeliveryAcknowledgement", "Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent."), + SL01("SpecificServiceOfferedByDebtorAgent", "Due to specific service offered by the Debtor Agent."), + SL02("SpecificServiceOfferedByCreditorAgent", "Due to specific service offered by the Creditor Agent."), + SL03("ServiceofClearingSystem", "Due to a specific service offered by the clearing system."), + SL11("CreditorNotOnWhitelistOfDebtor", "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet). In the Whitelist the Debtor may list all allowed Creditors to debit Debtor bank account."), + SL12("CreditorOnBlacklistOfDebtor", "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”. In the Blacklist the Debtor may list all Creditors not allowed to debit Debtor bank account."), + SL13("MaximumNumberOfDirectDebitTransactionsExceeded", "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent."), + SL14("MaximumDirectDebitTransactionAmountExceeded", "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent."), + SPII("RTPServiceProviderIdentifierIncorrect", "Identifier of the request-to-pay service provider is incorrect."), + TA01("TransmissonAborted", "The transmission of the file was not successful – it had to be aborted (for technical reasons)"), + TD01("NoDataAvailable", "There is no data available (for download)"), + TD02("FileNonReadable", "The file cannot be read (e.g. unknown format)"), + TD03("IncorrectFileStructure", "The file format is incomplete or invalid"), + TK01("TokenInvalid", "Token is invalid."), + TK02("SenderTokenNotFound", "Token used for the sender does not exist."), + TK03("ReceiverTokenNotFound", "Token used for the receiver does not exist."), + TK09("TokenMissing", "Token required for request is missing."), + TKCM("TokenCounterpartyMismatch", "Token found with counterparty mismatch."), + TKSG("TokenSingleUse", "Single Use Token already used."), + TKSP("TokenSuspended", "Token found with suspended status."), + TKVE("TokenValueLimitExceeded", "Token found with value limit rule violation."), + TKXP("TokenExpired", "Token expired."), + TM01("InvalidCutOffTime", "Associated message, payment information block, or transaction was received after agreed processing cut-off time."), + TS01("TransmissionSuccessful", "The (technical) transmission of the file was successful."), + TS04("TransferToSignByHand", "The order was transferred to pass by accompanying note signed by hand"), + UCRD("UnknownCreditor", "Unknown Creditor."), + UPAY("UnduePayment", "Payment is not justified."), +} + +enum class ExternalPaymentGroupStatusCode(val isoCode: String, val description: String) { + ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."), + ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."), + ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement on the debtor's account has been completed."), + ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution."), + ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"), + ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."), + PART("PartiallyAccepted", "A number of transactions have been accepted, whereas another number of transactions have not yet achieved"), + PDNG("Pending", "Payment initiation or individual transaction included in the payment initiation is pending. Further checks and status update will be performed."), + RCVD("Received", "Payment initiation has been received by the receiving agent"), + RJCT("Rejected", "Payment initiation or individual transaction included in the payment initiation has been rejected."), +} + +enum class ExternalPaymentTransactionStatusCode(val isoCode: String, val description: String) { + ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."), + ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."), + ACFC("AcceptedFundsChecked", "Preceding check of technical validation and customer profile was successful and an automatic funds check was positive."), + ACIS("AcceptedandChequeIssued", "Payment instruction to issue a cheque has been accepted, and the cheque has been issued but not yet been deposited or cleared."), + ACPD("AcceptedClearingProcessed", "Status of transaction released from the Debtor Agent and accepted by the clearing."), + ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement completed."), + ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment instruction has been accepted for execution."), + ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"), + ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."), + ACWP("AcceptedWithoutPosting", "Payment instruction included in the credit transfer is accepted without being posted to the creditor customer’s account."), + BLCK("Blocked", "Payment transaction previously reported with status 'ACWP' is blocked, for example, funds will neither be posted to the Creditor's account, nor be returned to the Debtor."), + CANC("Cancelled", "Payment initiation has been successfully cancelled after having received a request for cancellation."), + CPUC("CashPickedUpByCreditor", "Cash has been picked up by the Creditor."), + PATC("PartiallyAcceptedTechnicalCorrect", "Payment initiation needs multiple authentications, where some but not yet all have been performed. Syntactical and semantical validations are successful."), + PDNG("Pending", "Payment instruction is pending. Further checks and status update will be performed."), + PRES("Presented", "Request for Payment has been presented to the Debtor."), + RCVD("Received", "Payment instruction has been received."), + RJCT("Rejected", "Payment instruction has been rejected."), +} diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt new file mode 100644 index 00000000..abc7bc80 --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022Constants.kt @@ -0,0 +1,35 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.nexus + +enum class HacAction(val description: String) { + FILE_UPLOAD("File submitted to the bank"), + FILE_DOWNLOAD("File downloaded from the bank"), + ES_UPLOAD("Electronic signature submitted to the bank"), + ES_DOWNLOAD("Electronic signature downloaded from the bank"), + ES_VERIFICATION("Signature verification"), + VEU_FORWARDING("Forwarding to EDS"), + VEU_VERIFICATION("EDS signature verification"), + VEU_VERIFICATION_END("VEU_VERIFICATION_END"), + VEU_CANCEL_ORDER("Cancellation of EDS order"), + ADDITIONAL("Additional information"), + ORDER_HAC_FINAL_POS("HAC end of order (positive)"), + ORDER_HAC_FINAL_NEG("ORDER_HAC_FINAL_NEG") +} \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt new file mode 100644 index 00000000..905bd223 --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XMLUtil.kt @@ -0,0 +1,209 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.nexus + +import tech.libeufin.nexus.ebics.* +import io.ktor.http.* +import org.w3c.dom.Document +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import org.w3c.dom.ls.LSInput +import org.w3c.dom.ls.LSResourceResolver +import org.xml.sax.ErrorHandler +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.SAXParseException +import java.io.* +import java.security.PrivateKey +import java.security.PublicKey +import java.security.interfaces.RSAPrivateCrtKey +import javax.xml.XMLConstants +import javax.xml.crypto.* +import javax.xml.crypto.dom.DOMURIReference +import javax.xml.crypto.dsig.* +import javax.xml.crypto.dsig.dom.DOMSignContext +import javax.xml.crypto.dsig.dom.DOMValidateContext +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec +import javax.xml.crypto.dsig.spec.TransformParameterSpec +import javax.xml.namespace.NamespaceContext +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.Source +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.transform.stream.StreamSource +import javax.xml.validation.SchemaFactory +import javax.xml.validation.Validator +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + +/** + * This URI dereferencer allows handling the resource reference used for + * XML signatures in EBICS. + */ +private class EbicsSigUriDereferencer : URIDereferencer { + override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { + if (myRef !is DOMURIReference) + throw Exception("invalid type") + if (myRef.uri != "#xpointer(//*[@authenticate='true'])") + throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") + val xp: XPath = XPathFactory.newInstance().newXPath() + val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").evaluate( + myRef.here.ownerDocument, XPathConstants.NODESET + ) + if (nodeSet !is NodeList) + throw Exception("invalid type") + if (nodeSet.length <= 0) { + throw Exception("no nodes to sign") + } + val nodeList = ArrayList() + for (i in 0 until nodeSet.length) { + val node = nodeSet.item(i) + nodeList.add(node) + } + return NodeSetData { nodeList.iterator() } + } +} + +/** + * Helpers for dealing with XML in EBICS. + */ +object XMLUtil { + fun convertDomToBytes(document: Document): ByteArray { + val w = ByteArrayOutputStream() + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.setOutputProperty(OutputKeys.STANDALONE, "yes") + transformer.transform(DOMSource(document), StreamResult(w)) + return w.toByteArray() + } + + /** + * Convert a node to a string without the XML declaration or + * indentation. + */ + fun convertNodeToString(node: Node): String { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes") + /* Make string writer. */ + val sw = StringWriter() + /* Extract string. */ + t.transform(DOMSource(node), StreamResult(sw)) + return sw.toString() + } + + /** Parse [xml] into a XML DOM */ + fun parseIntoDom(xml: InputStream): Document { + val factory = DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true + } + val builder = factory.newDocumentBuilder() + return xml.use { + builder.parse(InputSource(it)) + } + } + + /** + * Sign an EBICS document with the authentication and identity signature. + */ + fun signEbicsDocument( + doc: Document, + signingPriv: PrivateKey, + withEbics3: Boolean = false + ) { + val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" + val authSigNode = XPathFactory.newInstance().newXPath() + .evaluate("/*[1]/$ns:AuthSignature", doc, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val fac = XMLSignatureFactory.getInstance("DOM") + val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) + val ref: Reference = + fac.newReference( + "#xpointer(//*[@authenticate='true'])", + fac.newDigestMethod(DigestMethod.SHA256, null), + listOf(c14n), + null, + null + ) + val canon: CanonicalizationMethod = + fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) + val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null) + val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) + val sig: XMLSignature = fac.newXMLSignature(si, null) + val dsc = DOMSignContext(signingPriv, authSigNode) + dsc.defaultNamespacePrefix = "ds" + dsc.uriDereferencer = EbicsSigUriDereferencer() + dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + sig.sign(dsc) + val innerSig = authSigNode.firstChild + while (innerSig.hasChildNodes()) { + authSigNode.appendChild(innerSig.firstChild) + } + authSigNode.removeChild(innerSig) + } + + fun verifyEbicsDocument( + doc: Document, + signingPub: PublicKey, + withEbics3: Boolean = false + ): Boolean { + val doc2: Document = doc.cloneNode(true) as Document + val ns = if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" + val authSigNode = XPathFactory.newInstance().newXPath() + .evaluate("/*[1]/$ns:AuthSignature", doc2, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") + authSigNode.parentNode.insertBefore(sigEl, authSigNode) + while (authSigNode.hasChildNodes()) { + sigEl.appendChild(authSigNode.firstChild) + } + authSigNode.parentNode.removeChild(authSigNode) + val fac = XMLSignatureFactory.getInstance("DOM") + val dvc = DOMValidateContext(signingPub, sigEl) + dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + dvc.uriDereferencer = EbicsSigUriDereferencer() + val sig = fac.unmarshalXMLSignature(dvc) + // FIXME: check that parameters are okay! + val valResult = sig.validate(dvc) + sig.signedInfo.references[0].validate(dvc) + return valResult + } + + fun getNodeFromXpath(doc: Document, query: String): Node { + val xpath = XPathFactory.newInstance().newXPath() + val ret = xpath.evaluate(query, doc, XPathConstants.NODE) + ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") + return ret as Node + } + + fun getStringFromXpath(doc: Document, query: String): String { + val xpath = XPathFactory.newInstance().newXPath() + val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String + if (ret.isEmpty()) { + throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") + } + return ret + } +} \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt new file mode 100644 index 00000000..a0d806ec --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt @@ -0,0 +1,204 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2020, 2024 Taler Systems S.A. + * + * 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.nexus + +import org.w3c.dom.* +import java.io.InputStream +import java.io.StringWriter +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import javax.xml.parsers.* +import javax.xml.stream.XMLOutputFactory +import javax.xml.stream.XMLStreamWriter + +interface XmlBuilder { + fun el(path: String, lambda: XmlBuilder.() -> Unit = {}) + fun el(path: String, content: String) { + el(path) { + text(content) + } + } + fun attr(namespace: String, name: String, value: String) + fun attr(name: String, value: String) + fun text(content: String) + + companion object { + fun toString(root: String, f: XmlBuilder.() -> Unit): String { + val factory = XMLOutputFactory.newFactory() + val stream = StringWriter() + var writer = factory.createXMLStreamWriter(stream) + /** + * NOTE: commenting out because it wasn't obvious how to output the + * "standalone = 'yes' directive". Manual forge was therefore preferred. + */ + stream.write("") + XmlStreamBuilder(writer).el(root) { + this.f() + } + writer.writeEndDocument() + return stream.buffer.toString() + } + + fun toDom(root: String, schema: String?, f: XmlBuilder.() -> Unit): Document { + val factory = DocumentBuilderFactory.newInstance(); + factory.isNamespaceAware = true + val builder = factory.newDocumentBuilder(); + val doc = builder.newDocument(); + doc.setXmlVersion("1.0") + doc.setXmlStandalone(true) + val root = doc.createElementNS(schema, root) + doc.appendChild(root); + XmlDOMBuilder(doc, schema, root).f() + doc.normalize() + return doc + } + } +} + +private class XmlStreamBuilder(private val w: XMLStreamWriter): XmlBuilder { + override fun el(path: String, lambda: XmlBuilder.() -> Unit) { + path.splitToSequence('/').forEach { + w.writeStartElement(it) + } + lambda() + path.splitToSequence('/').forEach { + w.writeEndElement() + } + } + + override fun attr(namespace: String, name: String, value: String) { + w.writeAttribute(namespace, name, value) + } + + override fun attr(name: String, value: String) { + w.writeAttribute(name, value) + } + + override fun text(content: String) { + w.writeCharacters(content) + } +} + +private class XmlDOMBuilder(private val doc: Document, private val schema: String?, private var node: Element): XmlBuilder { + override fun el(path: String, lambda: XmlBuilder.() -> Unit) { + val current = node + path.splitToSequence('/').forEach { + val new = doc.createElementNS(schema, it) + node.appendChild(new) + node = new + } + lambda() + node = current + } + + override fun attr(namespace: String, name: String, value: String) { + node.setAttributeNS(namespace, name, value) + } + + override fun attr(name: String, value: String) { + node.setAttribute(name, value) + } + + override fun text(content: String) { + node.appendChild(doc.createTextNode(content)); + } +} + +class DestructionError(m: String) : Exception(m) + +private fun Element.childrenByTag(tag: String): Sequence = sequence { + for (i in 0..childNodes.length) { + val el = childNodes.item(i) + if (el !is Element) { + continue + } + if (el.localName != tag) { + continue + } + yield(el) + } +} + +class XmlDestructor internal constructor(private val el: Element) { + fun each(path: String, f: XmlDestructor.() -> Unit) { + el.childrenByTag(path).forEach { + f(XmlDestructor(it)) + } + } + + fun map(path: String, f: XmlDestructor.() -> T): List { + return el.childrenByTag(path).map { + f(XmlDestructor(it)) + }.toList() + } + + fun one(path: String): XmlDestructor { + val children = el.childrenByTag(path).iterator() + if (!children.hasNext()) { + throw DestructionError("expected a single $path child, got none instead at $el") + } + val el = children.next() + if (children.hasNext()) { + throw DestructionError("expected a single $path child, got ${children.asSequence() + 1} instead at $el") + } + return XmlDestructor(el) + } + fun opt(path: String): XmlDestructor? { + val children = el.childrenByTag(path).iterator() + if (!children.hasNext()) { + return null + } + val el = children.next() + if (children.hasNext()) { + throw DestructionError("expected an optional $path child, got ${children.asSequence().count() + 1} instead at $el") + } + return XmlDestructor(el) + } + + fun one(path: String, f: XmlDestructor.() -> T): T = f(one(path)) + fun opt(path: String, f: XmlDestructor.() -> T): T? = opt(path)?.run(f) + + fun text(): String = el.textContent + fun bool(): Boolean = el.textContent.toBoolean() + fun date(): LocalDate = LocalDate.parse(text(), DateTimeFormatter.ISO_DATE) + fun dateTime(): LocalDateTime = LocalDateTime.parse(text(), DateTimeFormatter.ISO_DATE_TIME) + inline fun > enum(): T = java.lang.Enum.valueOf(T::class.java, text()) + + fun attr(index: String): String = el.getAttribute(index) + + companion object { + fun fromStream(xml: InputStream, root: String, f: XmlDestructor.() -> T): T { + val doc = XMLUtil.parseIntoDom(xml) + return fromDoc(doc, root, f) + } + + fun fromDoc(doc: Document, root: String, f: XmlDestructor.() -> T): T { + if (doc.documentElement.tagName != root) { + throw DestructionError("expected root '$root' got '${doc.documentElement.tagName}'") + } + val destr = XmlDestructor(doc.documentElement) + return f(destr) + } + } +} + +fun destructXml(xml: InputStream, root: String, f: XmlDestructor.() -> T): T + = XmlDestructor.fromStream(xml, root, f) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt new file mode 100644 index 00000000..6cd031ac --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics.kt @@ -0,0 +1,288 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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 + * + */ + +/** + * This is the main "EBICS library interface". Functions here are stateless helpers + * used to implement both an EBICS server and EBICS client. + */ + +package tech.libeufin.nexus.ebics + +import io.ktor.http.* +import org.w3c.dom.Document +import tech.libeufin.common.crypto.CryptoUtil +import tech.libeufin.common.* +import tech.libeufin.nexus.* +import java.io.InputStream +import java.security.SecureRandom +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import javax.xml.datatype.DatatypeFactory +import javax.xml.datatype.XMLGregorianCalendar + +data class EbicsProtocolError( + val httpStatusCode: HttpStatusCode, + val reason: String, + /** + * This class is also used when Nexus finds itself + * in an inconsistent state, without interacting with the + * bank. In this case, the EBICS code below can be left + * null. + */ + val ebicsTechnicalCode: EbicsReturnCode? = null +) : Exception(reason) + +/** + * @param size in bits + */ +fun getNonce(size: Int): ByteArray { + val sr = SecureRandom() + val ret = ByteArray(size / 8) + sr.nextBytes(ret) + return ret +} + +data class PreparedUploadData( + val transactionKey: ByteArray, + val userSignatureDataEncrypted: ByteArray, + val dataDigest: ByteArray, + val encryptedPayloadChunks: List +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PreparedUploadData + + if (!transactionKey.contentEquals(other.transactionKey)) return false + if (!userSignatureDataEncrypted.contentEquals(other.userSignatureDataEncrypted)) return false + if (encryptedPayloadChunks != other.encryptedPayloadChunks) return false + + return true + } + + override fun hashCode(): Int { + var result = transactionKey.contentHashCode() + result = 31 * result + userSignatureDataEncrypted.contentHashCode() + result = 31 * result + encryptedPayloadChunks.hashCode() + return result + } +} + +data class DataEncryptionInfo( + val transactionKey: ByteArray, + val bankPubDigest: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DataEncryptionInfo + + if (!transactionKey.contentEquals(other.transactionKey)) return false + if (!bankPubDigest.contentEquals(other.bankPubDigest)) return false + + return true + } + + override fun hashCode(): Int { + var result = transactionKey.contentHashCode() + result = 31 * result + bankPubDigest.contentHashCode() + return result + } +} + + +// TODO import missing using a script +@Suppress("SpellCheckingInspection") +enum class EbicsReturnCode(val errorCode: String) { + EBICS_OK("000000"), + EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), + EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), + EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), + EBICS_AUTHENTICATION_FAILED("061001"), + EBICS_INVALID_REQUEST("061002"), + EBICS_INTERNAL_ERROR("061099"), + EBICS_TX_RECOVERY_SYNC("061101"), + EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), + EBICS_INVALID_ORDER_DATA_FORMAT("090004"), + EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"), + EBICS_INVALID_USER_OR_USER_STATE("091002"), + EBICS_USER_UNKNOWN("091003"), + EBICS_EBICS_INVALID_USER_STATE("091004"), + EBICS_INVALID_ORDER_IDENTIFIER("091005"), + EBICS_UNSUPPORTED_ORDER_TYPE("091006"), + EBICS_INVALID_XML("091010"), + EBICS_TX_MESSAGE_REPLAY("091103"), + EBICS_PROCESSING_ERROR("091116"), + EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), + EBICS_AMOUNT_CHECK_FAILED("091303"); + + companion object { + fun lookup(errorCode: String): EbicsReturnCode { + for (x in entries) { + if (x.errorCode == errorCode) { + return x + } + } + throw Exception( + "Unknown EBICS status code: $errorCode" + ) + } + } +} + + +fun signOrderEbics3( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): ByteArray { + return XmlBuilder.toString("UserSignatureData") { + attr("xmlns", "http://www.ebics.org/S002") + el("OrderSignatureData") { + el("SignatureVersion", "A006") + el("SignatureValue", CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ).encodeBase64()) + el("PartnerID", partnerId) + el("UserID", userId) + } + }.toByteArray() +} + +data class EbicsResponseContent( + val transactionID: String?, + val orderID: String?, + val dataEncryptionInfo: DataEncryptionInfo?, + val orderDataEncChunk: String?, + val technicalReturnCode: EbicsReturnCode, + val bankReturnCode: EbicsReturnCode, + val reportText: String, + val segmentNumber: Int?, + // Only present in init phase + val numSegments: Int? +) + +data class EbicsKeyManagementResponseContent( + val technicalReturnCode: EbicsReturnCode, + val bankReturnCode: EbicsReturnCode?, + val orderData: ByteArray? +) + + +class HpbResponseData( + val hostID: String, + val encryptionPubKey: RSAPublicKey, + val encryptionVersion: String, + val authenticationPubKey: RSAPublicKey, + val authenticationVersion: String +) + + +fun ebics3toInternalRepr(response: Document): EbicsResponseContent { + // TODO better ebics response type + return XmlDestructor.fromDoc(response, "ebicsResponse") { + var transactionID: String? = null + var numSegments: Int? = null + lateinit var technicalReturnCode: EbicsReturnCode + lateinit var bankReturnCode: EbicsReturnCode + lateinit var reportText: String + var orderID: String? = null + var segmentNumber: Int? = null + var orderDataEncChunk: String? = null + var dataEncryptionInfo: DataEncryptionInfo? = null + one("header") { + one("static") { + transactionID = opt("TransactionID")?.text() + numSegments = opt("NumSegments")?.text()?.toInt() + } + one("mutable") { + segmentNumber = opt("SegmentNumber")?.text()?.toInt() + orderID = opt("OrderID")?.text() + technicalReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text()) + reportText = one("ReportText").text() + } + } + one("body") { + opt("DataTransfer") { + orderDataEncChunk = one("OrderData").text() + dataEncryptionInfo = opt("DataEncryptionInfo") { + DataEncryptionInfo( + one("TransactionKey").text().decodeBase64(), + one("EncryptionPubKeyDigest").text().decodeBase64() + ) + } + } + bankReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text()) + } + EbicsResponseContent( + transactionID = transactionID, + orderID = orderID, + bankReturnCode = bankReturnCode, + technicalReturnCode = technicalReturnCode, + reportText = reportText, + orderDataEncChunk = orderDataEncChunk, + dataEncryptionInfo = dataEncryptionInfo, + numSegments = numSegments, + segmentNumber = segmentNumber + ) + } +} + +fun parseEbicsHpbOrder(orderDataRaw: InputStream): HpbResponseData { + return XmlDestructor.fromStream(orderDataRaw, "HPBResponseOrderData") { + val (authenticationPubKey, authenticationVersion) = one("AuthenticationPubKeyInfo") { + Pair( + one("PubKeyValue").one("RSAKeyValue") { + CryptoUtil.loadRsaPublicKeyFromComponents( + one("Modulus").text().decodeBase64(), + one("Exponent").text().decodeBase64(), + ) + }, + one("AuthenticationVersion").text() + ) + } + val (encryptionPubKey, encryptionVersion) = one("EncryptionPubKeyInfo") { + Pair( + one("PubKeyValue").one("RSAKeyValue") { + CryptoUtil.loadRsaPublicKeyFromComponents( + one("Modulus").text().decodeBase64(), + one("Exponent").text().decodeBase64(), + ) + }, + one("EncryptionVersion").text() + ) + + } + val hostID: String = one("HostID").text() + HpbResponseData( + hostID = hostID, + encryptionPubKey = encryptionPubKey, + encryptionVersion = encryptionVersion, + authenticationPubKey = authenticationPubKey, + authenticationVersion = authenticationVersion + ) + } +} \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt index 1fdb4b26..0879d3c3 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt @@ -26,7 +26,7 @@ package tech.libeufin.nexus.ebics import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.common.* -import tech.libeufin.ebics.* +import tech.libeufin.nexus.* import tech.libeufin.nexus.BankPublicKeysFile import tech.libeufin.nexus.ClientPrivateKeysFile import tech.libeufin.nexus.EbicsSetupConfig diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt index e7df9c83..993c209c 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt @@ -19,13 +19,9 @@ package tech.libeufin.nexus.ebics import io.ktor.client.* -import tech.libeufin.ebics.* +import tech.libeufin.nexus.* import tech.libeufin.common.* import tech.libeufin.common.crypto.* -import tech.libeufin.nexus.BankPublicKeysFile -import tech.libeufin.nexus.ClientPrivateKeysFile -import tech.libeufin.nexus.EbicsSetupConfig -import tech.libeufin.nexus.logger import java.math.BigInteger import java.time.* import java.time.format.* diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt index 496006e2..228580d3 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -45,7 +45,6 @@ import io.ktor.http.* import io.ktor.utils.io.jvm.javaio.* import tech.libeufin.common.* import tech.libeufin.common.crypto.* -import tech.libeufin.ebics.* import tech.libeufin.nexus.* import java.io.ByteArrayOutputStream import java.io.InputStream diff --git a/nexus/src/test/kotlin/Ebics.kt b/nexus/src/test/kotlin/Ebics.kt index 7bd05fa7..e2a2ab84 100644 --- a/nexus/src/test/kotlin/Ebics.kt +++ b/nexus/src/test/kotlin/Ebics.kt @@ -20,7 +20,7 @@ import io.ktor.client.engine.mock.* import io.ktor.http.* import org.junit.Test -import tech.libeufin.ebics.XMLUtil +import tech.libeufin.nexus.* import tech.libeufin.nexus.ebics.* import kotlin.io.path.Path import kotlin.io.path.writeBytes diff --git a/nexus/src/test/kotlin/XmlCombinatorsTest.kt b/nexus/src/test/kotlin/XmlCombinatorsTest.kt new file mode 100644 index 00000000..b14920e3 --- /dev/null +++ b/nexus/src/test/kotlin/XmlCombinatorsTest.kt @@ -0,0 +1,76 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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 + * + */ + +import org.junit.Test +import tech.libeufin.nexus.XmlBuilder +import tech.libeufin.nexus.XMLUtil +import kotlin.test.assertEquals + +class XmlCombinatorsTest { + fun testBuilder(expected: String, root: String, builder: XmlBuilder.() -> Unit) { + val toString = XmlBuilder.toString(root, builder) + val toDom = XmlBuilder.toDom(root, null, builder) + //assertEquals(expected, toString) TODO fix empty tag being closed only with toString + assertEquals(expected, XMLUtil.convertDomToBytes(toDom).toString(Charsets.UTF_8)) + } + + @Test + fun testWithModularity() { + fun module(base: XmlBuilder) { + base.el("module") + } + testBuilder( + "", + "root" + ) { + module(this) + } + } + + @Test + fun testWithIterable() { + testBuilder( + "111222333444555666777888999101010", + "iterable" + ) { + el("endOfDocument") { + for (i in 1..10) + el("e$i/e$i$i", "$i$i$i") + } + } + } + + @Test + fun testBasicXmlBuilding() { + testBuilder( + "", + "ebicsRequest" + ) { + attr("version", "H004") + el("a/b/c") { + attr("attribute-of", "c") + el("d/e/f") { + attr("nested", "true") + el("g/h") + } + } + el("one_more") + } + } +} diff --git a/nexus/src/test/kotlin/XmlUtilTest.kt b/nexus/src/test/kotlin/XmlUtilTest.kt new file mode 100644 index 00000000..f847c928 --- /dev/null +++ b/nexus/src/test/kotlin/XmlUtilTest.kt @@ -0,0 +1,73 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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 + * + */ + +import org.junit.Assert.assertTrue +import org.junit.Test +import tech.libeufin.common.crypto.CryptoUtil +import tech.libeufin.common.decodeBase64 +import tech.libeufin.nexus.XMLUtil +import java.security.KeyPairGenerator +import javax.xml.transform.stream.StreamSource + +class XmlUtilTest { + + @Test + fun basicSigningTest() { + val doc = XMLUtil.parseIntoDom(""" + + + Hello World + + """.trimIndent().toByteArray().inputStream()) + val kpg = KeyPairGenerator.getInstance("RSA") + kpg.initialize(2048) + val pair = kpg.genKeyPair() + val otherPair = kpg.genKeyPair() + XMLUtil.signEbicsDocument(doc, pair.private) + kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) + kotlin.test.assertFalse(XMLUtil.verifyEbicsDocument(doc, otherPair.public)) + } + + @Test + fun multiAuthSigningTest() { + val doc = XMLUtil.parseIntoDom(""" + + + Hello World + Another one! + + """.trimIndent().toByteArray().inputStream()) + val kpg = KeyPairGenerator.getInstance("RSA") + kpg.initialize(2048) + val pair = kpg.genKeyPair() + XMLUtil.signEbicsDocument(doc, pair.private) + kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) + } + + @Test + fun testRefSignature() { + val classLoader = ClassLoader.getSystemClassLoader() + val docText = classLoader.getResourceAsStream("signature1/doc.xml") + val doc = XMLUtil.parseIntoDom(docText) + val keyStream = classLoader.getResourceAsStream("signature1/public_key.txt") + val keyBytes = keyStream.decodeBase64().readAllBytes() + val key = CryptoUtil.loadRsaPublicKey(keyBytes) + assertTrue(XMLUtil.verifyEbicsDocument(doc, key)) + } +} \ No newline at end of file diff --git a/nexus/src/test/resources/signature1/doc.xml b/nexus/src/test/resources/signature1/doc.xml new file mode 100644 index 00000000..271f8429 --- /dev/null +++ b/nexus/src/test/resources/signature1/doc.xml @@ -0,0 +1,9 @@ + + + qiFUoCn9kE0zSidyraO2Br/wn3/XyvWObJZ0aLIBXyA=LupLyRUJIuk0kCRwpFj4fpen2MI7Jw0BI944agwzXHfSDfq0Pp8h3sub6eSsKIAq7ekT3z+mlfMc + VFaKRi4B7kv4ja/URiYCKKbChQU2+kMGDvsncx9VcpcFrqAbWPmE9JXD2W2YW9OSkJ1tAZxZlZwS + A8KcvluV1wGEBuakHL2t3GqFPQEfKW4l8GYTjHh/w9jBve5d8tvMOjGtoyNemZGrVlzBxO9+hwbw + 8UFUCDA00dCjFDUHOnyAbBYsGzoaQyZprDn3iYDvlBz243zAN98PIKDclxlUEmkuF+JhrhCRjT9l + +JJxrELGHaDkFVadR4kaPdWPsbDaV0/2Fzc4Qg== + Hello World + \ No newline at end of file diff --git a/nexus/src/test/resources/signature1/public_key.txt b/nexus/src/test/resources/signature1/public_key.txt new file mode 100644 index 00000000..6d52df58 --- /dev/null +++ b/nexus/src/test/resources/signature1/public_key.txt @@ -0,0 +1 @@ +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqpUpetHZYdMjnaG544iSLZ5SnxlV4F/eQsIckG3mvMaXCQsY4rUTfJyle/fTZ0xGbjCUXCsbl1wkz8eB6chaX2LsHYDGiu/xNnU1nddAVB+5kkA5AIGncT9NVhdOgmpnZY/tae9qtZfCPAvbI0sGYQHea0pwyJ/hUnRJiMOjSRgIXALIvGVNqxe4U5ffLXFIUapTK2hOuhUH9BwDSK+mVR6gw0vDT05Z38sEpTeKUqJywL5cPSFIV+AN4ErSvsXNkTKUcbDxhGzOh/oTjTkz1kFFKe4ijPkSRkpK2sJMyAIretBKOK8SDICnsSrIh0YAcd6yTHQ3CeEjW4t0ZBULOQIDAQAB \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 48e088f8..310fbee3 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,5 +2,4 @@ rootProject.name = 'libeufin' include("bank") include("nexus") include("common") -include("testbench") -include("ebics") \ No newline at end of file +include("testbench") \ No newline at end of file -- cgit v1.2.3