libeufin

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

commit 2ae33e7e5c60094a3c150ed77dbf38c33f1e0751
parent cccc4acefea30618cfa749880312413b7e32d57d
Author: Florian Dold <florian.dold@gmail.com>
Date:   Fri,  8 Nov 2019 20:08:34 +0100

start with HKD

Diffstat:
M.idea/gradle.xml | 1-
Msandbox/build.gradle | 1+
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt | 8++++++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 16++++++++++------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt | 2+-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 229+++++++++++++++++++++++++++++++++----------------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt | 11+++++++++++
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsResponse.kt | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsTypes.kt | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/HKDResponseOrderData.kt | 15+++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/HTDResponseOrderData.kt | 190+------------------------------------------------------------------------------
Msandbox/src/test/kotlin/EbicsMessagesTest.kt | 20++++++++++----------
12 files changed, 487 insertions(+), 344 deletions(-)

diff --git a/.idea/gradle.xml b/.idea/gradle.xml @@ -17,7 +17,6 @@ <option value="$PROJECT_DIR$/sandbox" /> </set> </option> - <option name="useAutoImport" value="true" /> <option name="useQualifiedModuleNames" value="true" /> </GradleProjectSettings> </option> diff --git a/sandbox/build.gradle b/sandbox/build.gradle @@ -39,6 +39,7 @@ dependencies { implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1" implementation 'org.apache.santuario:xmlsec:2.1.4' implementation group: 'org.bouncycastle', name: 'bcprov-jdk16', version: '1.45' + implementation group: 'org.xerial', name: 'sqlite-jdbc', version: '3.28.0' testImplementation group: 'junit', name: 'junit', version: '4.12' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.3.50' diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt @@ -151,14 +151,18 @@ object CryptoUtil { } fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { + return decryptEbicsE002(enc.encryptedTransactionKey, enc.encryptedData, privateKey) + } + + fun decryptEbicsE002(encryptedTransactionKey: ByteArray, encryptedData: ByteArray, privateKey: RSAPrivateCrtKey): ByteArray { val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) - val transactionKeyBytes = asymmetricCipher.doFinal(enc.encryptedTransactionKey) + val transactionKeyBytes = asymmetricCipher.doFinal(encryptedTransactionKey) val secretKeySpec = SecretKeySpec(transactionKeyBytes, "AES") val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) val ivParameterSpec = IvParameterSpec(ByteArray(16)) symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - val data = symmetricCipher.doFinal(enc.encryptedData) + val data = symmetricCipher.doFinal(encryptedData) return data } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -21,10 +21,11 @@ package tech.libeufin.sandbox import org.jetbrains.exposed.dao.* import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.transactions.TransactionManager import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction import java.sql.Blob +import java.sql.Connection const val CUSTOMER_NAME_MAX_LENGTH = 20 const val EBICS_HOST_ID_MAX_LENGTH = 10 @@ -229,13 +230,13 @@ class EbicsUploadTransactionEntity(id: EntityID<String>) : Entity<String>(id) { var subscriber by EbicsSubscriberEntity referencedOn EbicsUploadTransactionsTable.subscriber var numSegments by EbicsUploadTransactionsTable.numSegments var lastSeenSegment by EbicsUploadTransactionsTable.lastSeenSegment - var transactionKeyEnc by EbicsDownloadTransactionsTable.transactionKeyEnc + var transactionKeyEnc by EbicsUploadTransactionsTable.transactionKeyEnc } object EbicsUploadTransactionChunksTable : IdTable<String>() { override val id = - text("transactionID").entityId().references(EbicsUploadTransactionsTable.id, ReferenceOption.CASCADE) + text("transactionID").entityId() val chunkIndex = integer("chunkIndex") val chunkContent = blob("chunkContent") } @@ -250,16 +251,19 @@ class EbicsUploadTransactionChunkEntity(id : EntityID<String>): Entity<String>(i fun dbCreateTables() { - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") + Database.connect("jdbc:sqlite:libeufin-sandbox.sqlite3", "org.sqlite.JDBC") + TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE transaction { // addLogger(StdOutSqlLogger) - SchemaUtils.create( + SchemaUtils.createMissingTablesAndColumns( BankCustomersTable, EbicsSubscribersTable, EbicsHostsTable, - EbicsDownloadTransactionsTable + EbicsDownloadTransactionsTable, + EbicsUploadTransactionsTable, + EbicsUploadTransactionChunksTable ) } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt @@ -46,7 +46,7 @@ object EbicsOrderUtil { val rng = SecureRandom() val res = ByteArray(16) rng.nextBytes(res) - return res.toHexString() + return res.toHexString().toUpperCase() } /** diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -28,15 +28,20 @@ import io.ktor.response.respond import io.ktor.response.respondText import org.apache.xml.security.binding.xmldsig.RSAKeyValueType import org.apache.xml.security.binding.xmldsig.SignatureType +import org.jetbrains.exposed.sql.lowerCase import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.upperCase import org.w3c.dom.Document import tech.libeufin.schema.ebics_h004.* import tech.libeufin.schema.ebics_hev.HEVResponse import tech.libeufin.schema.ebics_hev.SystemReturnCodeType import tech.libeufin.schema.ebics_s001.SignaturePubKeyOrderData import java.math.BigInteger +import java.security.PrivateKey +import java.security.interfaces.RSAPrivateCrtKey import java.util.* import java.util.zip.DeflaterInputStream +import java.util.zip.InflaterInputStream import javax.sql.rowset.serial.SerialBlob @@ -225,7 +230,7 @@ private suspend fun ApplicationCall.handleEbicsHpb( */ private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo { return transaction { - val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestHostID }.firstOrNull() + val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestHostID.toUpperCase() }.firstOrNull() if (ebicsHost == null) { logger.warn("client requested unknown HostID") throw EbicsKeyManagementError("[EBICS_INVALID_HOST_ID]", "091011") @@ -254,13 +259,13 @@ private suspend fun ApplicationCall.receiveEbicsXml(): Document { fun handleEbicsHtd(): ByteArray { val htd = HTDResponseOrderData().apply { - this.partnerInfo = HTDResponseOrderData.PartnerInfo().apply { + this.partnerInfo = EbicsTypes.PartnerInfo().apply { this.accountInfoList = listOf( - HTDResponseOrderData.AccountInfo().apply { + EbicsTypes.AccountInfo().apply { this.id = "acctid1" this.accountHolder = "Mina Musterfrau" this.accountNumberList = listOf( - HTDResponseOrderData.GeneralAccountNumber().apply { + EbicsTypes.GeneralAccountNumber().apply { this.international = true this.value = "DE21500105174751659277" } @@ -268,17 +273,17 @@ fun handleEbicsHtd(): ByteArray { this.currency = "EUR" this.description = "ACCT" this.bankCodeList = listOf( - HTDResponseOrderData.GeneralBankCode().apply { + EbicsTypes.GeneralBankCode().apply { this.international = true this.value = "INGDDEFFXXX" } ) }, - HTDResponseOrderData.AccountInfo().apply { + EbicsTypes.AccountInfo().apply { this.id = "glsdemo" this.accountHolder = "Mina Musterfrau" this.accountNumberList = listOf( - HTDResponseOrderData.GeneralAccountNumber().apply { + EbicsTypes.GeneralAccountNumber().apply { this.international = true this.value = "DE91430609670123123123" } @@ -286,45 +291,45 @@ fun handleEbicsHtd(): ByteArray { this.currency = "EUR" this.description = "glsdemoacct" this.bankCodeList = listOf( - HTDResponseOrderData.GeneralBankCode().apply { + EbicsTypes.GeneralBankCode().apply { this.international = true this.value = "GENODEM1GLS" } ) } ) - this.addressInfo = HTDResponseOrderData.AddressInfo().apply { + this.addressInfo = EbicsTypes.AddressInfo().apply { this.name = "Foo" } - this.bankInfo = HTDResponseOrderData.BankInfo().apply { + this.bankInfo = EbicsTypes.BankInfo().apply { this.hostID = "host01" } this.orderInfoList = listOf( - HTDResponseOrderData.AuthOrderInfoType().apply { + EbicsTypes.AuthOrderInfoType().apply { this.description = "foo" this.orderType = "C53" this.transferType = "Download" }, - HTDResponseOrderData.AuthOrderInfoType().apply { + EbicsTypes.AuthOrderInfoType().apply { this.description = "foo" this.orderType = "C52" this.transferType = "Download" }, - HTDResponseOrderData.AuthOrderInfoType().apply { + EbicsTypes.AuthOrderInfoType().apply { this.description = "foo" this.orderType = "CCC" this.transferType = "Upload" } ) } - this.userInfo = HTDResponseOrderData.UserInfo().apply { + this.userInfo = EbicsTypes.UserInfo().apply { this.name = "Some User" - this.userID = HTDResponseOrderData.UserIDType().apply { + this.userID = EbicsTypes.UserIDType().apply { this.status = 5 this.value = "USER1" } this.permissionList = listOf( - HTDResponseOrderData.UserPermission().apply { + EbicsTypes.UserPermission().apply { this.orderTypes = "C54 C53 C52 CCC" } ) @@ -336,111 +341,22 @@ fun handleEbicsHtd(): ByteArray { } -fun createEbicsResponseForDownloadInitializationPhase( - transactionID: String, - numSegments: Int, - segmentSize: Int, - enc: CryptoUtil.EncryptionResult, - encodedData: String -): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = EbicsResponse.Header().apply { - this.authenticate = true - this._static = EbicsResponse.StaticHeaderType().apply { - this.transactionID = transactionID - this.numSegments = BigInteger.valueOf(numSegments.toLong()) - } - this.mutable = EbicsResponse.MutableHeaderType().apply { - this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - this.segmentNumber = EbicsResponse.SegmentNumber().apply { - this.lastSegment = (numSegments == 1) - this.value = BigInteger.valueOf(1) - } - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = EbicsResponse.Body().apply { - this.returnCode = EbicsResponse.ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - this.dataTransfer = EbicsResponse.DataTransferResponseType().apply { - this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { - this.authenticate = true - this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { - this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - this.version = "E002" - this.value = enc.pubKeyDigest - } - this.transactionKey = enc.encryptedTransactionKey - } - this.orderData = EbicsResponse.OrderData().apply { - this.value = encodedData.substring(0, Math.min(segmentSize, encodedData.length)) - } - } - } - } +fun signEbicsResponseX002(ebicsResponse: EbicsResponse, privateKey: RSAPrivateCrtKey): String { + val doc = XMLUtil.convertJaxbToDocument(ebicsResponse) + XMLUtil.signEbicsDocument(doc, privateKey) + val signedDoc = XMLUtil.convertDomToString(doc) + println("response: $signedDoc") + return signedDoc } -fun createEbicsResponseForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = EbicsResponse.Header().apply { - this.authenticate = true - this._static = EbicsResponse.StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = EbicsResponse.MutableHeaderType().apply { - this.transactionPhase = EbicsTypes.TransactionPhaseType.RECEIPT - if (positiveAck) { - this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_DONE] Received positive receipt" - this.returnCode = "011000" - } else { - this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_SKIPPED] Received negative receipt" - this.returnCode = "011001" - } - } - } - this.authSignature = SignatureType() - this.body = EbicsResponse.Body().apply { - this.returnCode = EbicsResponse.ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } -} +class EbicsTransactionDetails( -fun createEbicsResponseForUploadInitializationPhase(transactionID: String, orderID: String): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = EbicsResponse.Header().apply { - this.authenticate = true - this._static = EbicsResponse.StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = EbicsResponse.MutableHeaderType().apply { - this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - this.orderID = orderID - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = EbicsResponse.Body().apply { - this.returnCode = EbicsResponse.ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } +) + + +fun queryEbicsTransactionDetails(ebicsRequest: EbicsRequest): EbicsTransactionDetails { + throw NotImplementedError() } @@ -492,12 +408,14 @@ suspend fun ApplicationCall.ebicsweb() { val responseXmlStr = transaction { // Step 1 of 3: Get information about the host and subscriber - val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestedHostId }.firstOrNull() + val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestedHostId.toUpperCase() }.firstOrNull() val requestTransactionID = requestObject.header.static.transactionID var downloadTransaction: EbicsDownloadTransactionEntity? = null - var uploadTransaction: EbicsUploadTransactionEntity? = null + var uploadTransaction: EbicsUploadTransactionEntity? = + null val subscriber = if (requestTransactionID != null) { - downloadTransaction = EbicsDownloadTransactionEntity.findById(requestTransactionID) + println("finding subscriber by transactionID $requestTransactionID") + downloadTransaction = EbicsDownloadTransactionEntity.findById(requestTransactionID.toUpperCase()) if (downloadTransaction != null) { downloadTransaction.subscriber } else { @@ -517,6 +435,10 @@ suspend fun ApplicationCall.ebicsweb() { ebicsHost.authenticationPrivateKey .toByteArray() ) + val hostEncPriv = CryptoUtil.loadRsaPrivateKey( + ebicsHost.encryptionPrivateKey + .toByteArray() + ) val clientAuthPub = CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.toByteArray()) val clientEncPub = @@ -534,6 +456,7 @@ suspend fun ApplicationCall.ebicsweb() { val orderType = requestObject.header.static.orderDetails?.orderType ?: throw EbicsInvalidRequestError() if (staticHeader.numSegments == null) { + println("handling initialization for order type $orderType") val response = when (orderType) { "HTD" -> handleEbicsHtd() else -> throw EbicsInvalidXmlError() @@ -560,7 +483,7 @@ suspend fun ApplicationCall.ebicsweb() { this.numSegments = numSegments this.receiptReceived = false } - createEbicsResponseForDownloadInitializationPhase( + EbicsResponse.createForDownloadInitializationPhase( transactionID, numSegments, segmentSize, @@ -571,15 +494,30 @@ suspend fun ApplicationCall.ebicsweb() { val oidn = subscriber.nextOrderID++ if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError() val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn) - val signatureData = requestObject.body.dataTransfer?.signatureData - if (signatureData != null) { - println("signature data: ${signatureData.toString(Charsets.UTF_8)}") - } val numSegments = requestObject.header.static.numSegments ?: throw EbicsInvalidRequestError() val transactionKeyEnc = requestObject.body.dataTransfer?.dataEncryptionInfo?.transactionKey ?: throw EbicsInvalidRequestError() + val encPubKeyDigest = + requestObject.body.dataTransfer?.dataEncryptionInfo?.encryptionPubKeyDigest?.value + if (encPubKeyDigest == null) + throw EbicsInvalidRequestError() + val encSigData = requestObject.body.dataTransfer?.signatureData + if (encSigData == null) + throw EbicsInvalidRequestError() + val decryptedSignatureData = CryptoUtil.decryptEbicsE002( + CryptoUtil.EncryptionResult( + transactionKeyEnc, + encPubKeyDigest, + encSigData + ), hostEncPriv + ) + val plainSigData = InflaterInputStream(decryptedSignatureData.inputStream()).use { + it.readAllBytes() + } + println("signature data: ${plainSigData.toString(Charsets.UTF_8)}") + println("creating upload transaction for transactionID $transactionID") EbicsUploadTransactionEntity.new(transactionID) { this.host = ebicsHost this.subscriber = subscriber @@ -589,11 +527,39 @@ suspend fun ApplicationCall.ebicsweb() { this.numSegments = numSegments.toInt() this.transactionKeyEnc = SerialBlob(transactionKeyEnc) } - createEbicsResponseForUploadInitializationPhase(transactionID, orderID) + EbicsResponse.createForUploadInitializationPhase(transactionID, orderID) } } EbicsTypes.TransactionPhaseType.TRANSFER -> { - throw NotImplementedError() + requestTransactionID ?: throw EbicsInvalidRequestError() + val requestSegmentNumber = + requestObject.header.mutable.segmentNumber?.toInt() ?: throw EbicsInvalidRequestError() + if (uploadTransaction != null) { + if (requestSegmentNumber == 1 && uploadTransaction.numSegments == 1) { + val encOrderData = + requestObject.body.dataTransfer?.orderData ?: throw EbicsInvalidRequestError() + val zippedData = CryptoUtil.decryptEbicsE002( + uploadTransaction.transactionKeyEnc.toByteArray(), + encOrderData, + hostEncPriv + ) + val unzippedData = + InflaterInputStream(zippedData.inputStream()).use { it.readAllBytes() } + println("got upload data: ${unzippedData.toString(Charsets.UTF_8)}") + EbicsResponse.createForUploadTransferPhase( + requestTransactionID, + requestSegmentNumber, + true, + uploadTransaction.orderID + ) + } else { + throw NotImplementedError() + } + } else if (downloadTransaction != null) { + throw NotImplementedError() + } else { + throw AssertionError() + } } EbicsTypes.TransactionPhaseType.RECEIPT -> { requestTransactionID ?: throw EbicsInvalidRequestError() @@ -601,15 +567,10 @@ suspend fun ApplicationCall.ebicsweb() { throw EbicsInvalidRequestError() val receiptCode = requestObject.body.transferReceipt?.receiptCode ?: throw EbicsInvalidRequestError() - createEbicsResponseForDownloadReceiptPhase(requestTransactionID, receiptCode == 0) + EbicsResponse.createForDownloadReceiptPhase(requestTransactionID, receiptCode == 0) } } - val docText = XMLUtil.convertJaxbToString(ebicsResponse) - val doc = XMLUtil.parseStringIntoDom(docText) - XMLUtil.signEbicsDocument(doc, hostAuthPriv) - val signedDoc = XMLUtil.convertDomToString(doc) - println("response: $signedDoc") - docText + signEbicsResponseX002(ebicsResponse, hostAuthPriv) } respondText(responseXmlStr, ContentType.Application.Xml, HttpStatusCode.OK) } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/XMLUtil.kt @@ -203,6 +203,17 @@ class XMLUtil private constructor() { return sw.toString() } + inline fun <reified T> convertJaxbToDocument(obj: T): Document { + val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() + dbf.isNamespaceAware = true + val doc = dbf.newDocumentBuilder().newDocument() + val jc = JAXBContext.newInstance(T::class.java) + val m = jc.createMarshaller() + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + m.marshal(obj, doc) + return doc + } + /** * Convert a XML string to the JAXB representation. * diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsResponse.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsResponse.kt @@ -1,6 +1,7 @@ package tech.libeufin.schema.ebics_h004 import org.apache.xml.security.binding.xmldsig.SignatureType +import tech.libeufin.sandbox.CryptoUtil import java.math.BigInteger import javax.xml.bind.annotation.* import javax.xml.bind.annotation.adapters.CollapsedStringAdapter @@ -91,8 +92,8 @@ class EbicsResponse { @XmlValue lateinit var value: BigInteger - @XmlAttribute(name = "lastSegment", required = true) - var lastSegment: Boolean = false + @XmlAttribute(name = "lastSegment") + var lastSegment: Boolean? = null } @XmlAccessorType(XmlAccessType.NONE) @@ -132,4 +133,151 @@ class EbicsResponse { @get:XmlSchemaType(name = "positiveInteger") var numSegments: BigInteger? = null } + + companion object { + fun createForUploadInitializationPhase(transactionID: String, orderID: String): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + + fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.RECEIPT + if (positiveAck) { + this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_DONE] Received positive receipt" + this.returnCode = "011000" + } else { + this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_SKIPPED] Received negative receipt" + this.returnCode = "011001" + } + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + + fun createForUploadTransferPhase( + transactionID: String, + segmentNumber: Int, + lastSegment: Boolean, + orderID: String + ): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsResponse.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + if (lastSegment) { + this.lastSegment = true + } + } + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForDownloadInitializationPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + enc: CryptoUtil.EncryptionResult, + encodedData: String + ): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + this.segmentNumber = EbicsResponse.SegmentNumber().apply { + this.lastSegment = (numSegments == 1) + this.value = BigInteger.valueOf(1) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = EbicsResponse.DataTransferResponseType().apply { + this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + this.authenticate = true + this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { + this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + this.version = "E002" + this.value = enc.pubKeyDigest + } + this.transactionKey = enc.encryptedTransactionKey + } + this.orderData = EbicsResponse.OrderData().apply { + this.value = encodedData.substring(0, Math.min(segmentSize, encodedData.length)) + } + } + } + } + } + } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsTypes.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/EbicsTypes.kt @@ -202,4 +202,190 @@ class EbicsTypes private constructor() { @get:XmlElement(name = "Value", required = true) lateinit var value: String } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["addressInfo", "bankInfo", "accountInfoList", "orderInfoList"]) + class PartnerInfo { + @get:XmlElement(name = "AddressInfo", required = true) + lateinit var addressInfo: AddressInfo + + @get:XmlElement(name = "BankInfo", required = true) + lateinit var bankInfo: BankInfo + + @get:XmlElement(name = "AccountInfo", required = true) + var accountInfoList: List<AccountInfo>? = null + + @get:XmlElement(name = "OrderInfo") + lateinit var orderInfoList: List<AuthOrderInfoType> + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["orderType", "fileFormat", "transferType", "orderFormat", "description", "numSigRequired"] + ) + class AuthOrderInfoType { + @get:XmlElement(name = "OrderType") + lateinit var orderType: String + + @get:XmlElement(name = "FileFormat") + val fileFormat: EbicsTypes.FileFormatType? = null + + @get:XmlElement(name = "TransferType") + lateinit var transferType: String + + @get:XmlElement(name = "OrderFormat", required = false) + var orderFormat: String? = null + + @get:XmlElement(name = "Description") + lateinit var description: String + + @get:XmlElement(name = "NumSigRequired") + var numSigRequired: Int? = null + + } + + @XmlAccessorType(XmlAccessType.NONE) + class UserIDType { + @get:XmlValue + lateinit var value: String; + + @get:XmlAttribute(name = "Status") + var status: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["userID", "name", "permissionList"]) + class UserInfo { + @get:XmlElement(name = "UserID", required = true) + lateinit var userID: UserIDType + + @get:XmlElement(name = "Name") + var name: String? = null + + @get:XmlElement(name = "Permission", type = UserPermission::class) + var permissionList: List<UserPermission>? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderTypes", "fileFormat", "accountID", "maxAmount"]) + class UserPermission { + @get:XmlAttribute(name = "AuthorizationLevel") + var authorizationLevel: String? = null + + @get:XmlElement(name = "OrderTypes") + var orderTypes: String? = null + + @get:XmlElement(name = "FileFormat") + val fileFormat: EbicsTypes.FileFormatType? = null + + @get:XmlElement(name = "AccountID") + val accountID: String? = null + + @get:XmlElement(name = "MaxAmount") + val maxAmount: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["name", "street", "postCode", "city", "region", "country"]) + class AddressInfo { + @get:XmlElement(name = "Name") + var name: String? = null + + @get:XmlElement(name = "Street") + var street: String? = null + + @get:XmlElement(name = "PostCode") + var postCode: String? = null + + @get:XmlElement(name = "City") + var city: String? = null + + @get:XmlElement(name = "Region") + var region: String? = null + + @get:XmlElement(name = "Country") + var country: String? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + class BankInfo { + @get:XmlElement(name = "HostID") + lateinit var hostID: String + + @get:XmlElement(type = EbicsTypes.Parameter::class) + var parameters: List<EbicsTypes.Parameter>? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["accountNumberList", "bankCodeList", "accountHolder"]) + class AccountInfo { + @get:XmlAttribute(name = "Currency") + var currency: String? = null + + @get:XmlAttribute(name = "ID") + lateinit var id: String + + @get:XmlAttribute(name = "Description") + var description: String? = null + + @get:XmlElements( + XmlElement(name = "AccountNumber", type = GeneralAccountNumber::class), + XmlElement(name = "NationalAccountNumber", type = NationalAccountNumber::class) + ) + var accountNumberList: List<AbstractAccountNumber>? = null + + @get:XmlElements( + XmlElement(name = "BankCode", type = GeneralBankCode::class), + XmlElement(name = "NationalBankCode", type = NationalBankCode::class) + ) + var bankCodeList: List<AbstractBankCode>? = null + + @get:XmlElement(name = "AccountHolder") + var accountHolder: String? = null + } + + interface AbstractAccountNumber + + @XmlAccessorType(XmlAccessType.NONE) + class GeneralAccountNumber : AbstractAccountNumber { + @get:XmlAttribute(name = "international") + var international: Boolean = false + + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class NationalAccountNumber : AbstractAccountNumber { + @get:XmlAttribute(name = "format") + lateinit var format: String + + @get:XmlValue + lateinit var value: String + } + + interface AbstractBankCode + + @XmlAccessorType(XmlAccessType.NONE) + class GeneralBankCode : AbstractBankCode { + @get:XmlAttribute(name = "prefix") + var prefix: String? = null + + @get:XmlAttribute(name = "international") + var international: Boolean = false + + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class NationalBankCode : AbstractBankCode { + @get:XmlValue + lateinit var value: String + + @get:XmlAttribute(name = "format") + lateinit var format: String + } } \ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/HKDResponseOrderData.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/HKDResponseOrderData.kt @@ -0,0 +1,15 @@ +package tech.libeufin.schema.ebics_h004 + +import java.security.Permission +import javax.xml.bind.annotation.* + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["partnerInfo", "userInfo"]) +@XmlRootElement(name = "HTDResponseOrderData") +class HKDResponseOrderData { + @get:XmlElement(name = "PartnerInfo", required = true) + lateinit var partnerInfo: EbicsTypes.PartnerInfo + + @get:XmlElement(name = "UserInfo", type = EbicsTypes.UserInfo::class, required = true) + lateinit var userInfoList: List<EbicsTypes.UserInfo> +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/HTDResponseOrderData.kt b/sandbox/src/main/kotlin/tech/libeufin/schema/ebics_h004/HTDResponseOrderData.kt @@ -8,194 +8,8 @@ import javax.xml.bind.annotation.* @XmlRootElement(name = "HTDResponseOrderData") class HTDResponseOrderData { @get:XmlElement(name = "PartnerInfo", required = true) - lateinit var partnerInfo: PartnerInfo + lateinit var partnerInfo: EbicsTypes.PartnerInfo @get:XmlElement(name = "UserInfo", required = true) - lateinit var userInfo: UserInfo - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["addressInfo", "bankInfo", "accountInfoList", "orderInfoList"]) - class PartnerInfo { - @get:XmlElement(name = "AddressInfo", required = true) - lateinit var addressInfo: AddressInfo - - @get:XmlElement(name = "BankInfo", required = true) - lateinit var bankInfo: BankInfo - - @get:XmlElement(name = "AccountInfo", required = true) - var accountInfoList: List<AccountInfo>? = null - - @get:XmlElement(name = "OrderInfo") - lateinit var orderInfoList: List<AuthOrderInfoType> - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["orderType", "fileFormat", "transferType", "orderFormat", "description", "numSigRequired"] - ) - class AuthOrderInfoType { - @get:XmlElement(name = "OrderType") - lateinit var orderType: String - - @get:XmlElement(name = "FileFormat") - val fileFormat: EbicsTypes.FileFormatType? = null - - @get:XmlElement(name = "TransferType") - lateinit var transferType: String - - @get:XmlElement(name = "OrderFormat", required = false) - var orderFormat: String? = null - - @get:XmlElement(name = "Description") - lateinit var description: String - - @get:XmlElement(name = "NumSigRequired") - var numSigRequired: Int? = null - - } - - @XmlAccessorType(XmlAccessType.NONE) - class UserIDType { - @get:XmlValue - lateinit var value: String; - - @get:XmlAttribute(name = "Status") - var status: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["userID", "name", "permissionList"]) - class UserInfo { - @get:XmlElement(name = "UserID", required = true) - lateinit var userID: UserIDType - - @get:XmlElement(name = "Name") - var name: String? = null - - @get:XmlElement(name = "Permission", type = UserPermission::class) - var permissionList: List<UserPermission>? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderTypes", "fileFormat", "accountID", "maxAmount"]) - class UserPermission { - @get:XmlAttribute(name = "AuthorizationLevel") - var authorizationLevel: String? = null - - @get:XmlElement(name = "OrderTypes") - var orderTypes: String? = null - - @get:XmlElement(name = "FileFormat") - val fileFormat: EbicsTypes.FileFormatType? = null - - @get:XmlElement(name = "AccountID") - val accountID: String? = null - - @get:XmlElement(name = "MaxAmount") - val maxAmount: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["name", "street", "postCode", "city", "region", "country"]) - class AddressInfo { - @get:XmlElement(name = "Name") - var name: String? = null - - @get:XmlElement(name = "Street") - var street: String? = null - - @get:XmlElement(name = "PostCode") - var postCode: String? = null - - @get:XmlElement(name = "City") - var city: String? = null - - @get:XmlElement(name = "Region") - var region: String? = null - - @get:XmlElement(name = "Country") - var country: String? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - class BankInfo { - @get:XmlElement(name = "HostID") - lateinit var hostID: String - - @get:XmlElement(type = EbicsTypes.Parameter::class) - var parameters: List<EbicsTypes.Parameter>? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["accountNumberList", "bankCodeList", "accountHolder"]) - class AccountInfo { - @get:XmlAttribute(name = "Currency") - var currency: String? = null - - @get:XmlAttribute(name = "ID") - lateinit var id: String - - @get:XmlAttribute(name = "Description") - var description: String? = null - - @get:XmlElements( - XmlElement(name = "AccountNumber", type = GeneralAccountNumber::class), - XmlElement(name = "NationalAccountNumber", type = NationalAccountNumber::class) - ) - var accountNumberList: List<AbstractAccountNumber>? = null - - @get:XmlElements( - XmlElement(name = "BankCode", type = GeneralBankCode::class), - XmlElement(name = "NationalBankCode", type = NationalBankCode::class) - ) - var bankCodeList: List<AbstractBankCode>? = null - - @get:XmlElement(name = "AccountHolder") - var accountHolder: String? = null - } - - interface AbstractAccountNumber - - @XmlAccessorType(XmlAccessType.NONE) - class GeneralAccountNumber : AbstractAccountNumber { - @get:XmlAttribute(name = "international") - var international: Boolean = false - - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class NationalAccountNumber : AbstractAccountNumber { - @get:XmlAttribute(name = "format") - lateinit var format: String - - @get:XmlValue - lateinit var value: String - } - - interface AbstractBankCode - - @XmlAccessorType(XmlAccessType.NONE) - class GeneralBankCode : AbstractBankCode { - @get:XmlAttribute(name = "prefix") - var prefix: String? = null - - @get:XmlAttribute(name = "international") - var international: Boolean = false - - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class NationalBankCode : AbstractBankCode { - @get:XmlValue - lateinit var value: String - - @get:XmlAttribute(name = "format") - lateinit var format: String - } + lateinit var userInfo: EbicsTypes.UserInfo } diff --git a/sandbox/src/test/kotlin/EbicsMessagesTest.kt b/sandbox/src/test/kotlin/EbicsMessagesTest.kt @@ -177,13 +177,13 @@ class EbicsMessagesTest { @Test fun testHtd() { val htd = HTDResponseOrderData().apply { - this.partnerInfo = HTDResponseOrderData.PartnerInfo().apply { + this.partnerInfo = EbicsTypes.PartnerInfo().apply { this.accountInfoList = listOf( - HTDResponseOrderData.AccountInfo().apply { + EbicsTypes.AccountInfo().apply { this.id = "acctid1" this.accountHolder = "Mina Musterfrau" this.accountNumberList = listOf( - HTDResponseOrderData.GeneralAccountNumber().apply { + EbicsTypes.GeneralAccountNumber().apply { this.international = true this.value = "AT411100000237571500" } @@ -191,21 +191,21 @@ class EbicsMessagesTest { this.currency = "EUR" this.description = "some account" this.bankCodeList = listOf( - HTDResponseOrderData.GeneralBankCode().apply { + EbicsTypes.GeneralBankCode().apply { this.international = true this.value = "ABAGATWWXXX" } ) } ) - this.addressInfo = HTDResponseOrderData.AddressInfo().apply { + this.addressInfo = EbicsTypes.AddressInfo().apply { this.name = "Foo" } - this.bankInfo = HTDResponseOrderData.BankInfo().apply { + this.bankInfo = EbicsTypes.BankInfo().apply { this.hostID = "MYHOST" } this.orderInfoList = listOf( - HTDResponseOrderData.AuthOrderInfoType().apply { + EbicsTypes.AuthOrderInfoType().apply { this.description = "foo" this.orderType = "CCC" this.orderFormat = "foo" @@ -213,14 +213,14 @@ class EbicsMessagesTest { } ) } - this.userInfo = HTDResponseOrderData.UserInfo().apply { + this.userInfo = EbicsTypes.UserInfo().apply { this.name = "Some User" - this.userID = HTDResponseOrderData.UserIDType().apply { + this.userID = EbicsTypes.UserIDType().apply { this.status = 2 this.value = "myuserid" } this.permissionList = listOf( - HTDResponseOrderData.UserPermission().apply { + EbicsTypes.UserPermission().apply { this.orderTypes = "CCC ABC" } )