libeufin

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

commit 735135e718a8b6ebf35cd479851af71d0471ef24
parent d8efb15ceba322dbe5b66a6a7ba4cf4ddc74ebc2
Author: Florian Dold <florian.dold@gmail.com>
Date:   Wed,  5 Feb 2020 18:13:29 +0100

refactoring / code cleanup WIP

Diffstat:
Dnexus/src/main/kotlin/tech/libeufin/nexus/Containers.kt | 27---------------------------
Rnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt -> nexus/src/main/kotlin/tech/libeufin/nexus/Db.kt | 0
Anexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 124++-----------------------------------------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 510+++++++++++++++++++++++++++++++++----------------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 467+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msandbox/src/main/python/libeufin-cli | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mutil/src/main/kotlin/ebics_h004/EbicsRequest.kt | 12++++++++++++
Mutil/src/main/kotlin/time.kt | 4++--
Mutil/src/test/kotlin/SignatureDataTest.kt | 4++--
10 files changed, 870 insertions(+), 670 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Containers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Containers.kt @@ -1,26 +0,0 @@ -package tech.libeufin.nexus - -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey - -/** - * This class is a mere container that keeps data found - * in the database and that is further needed to sign / verify - * / make messages. And not all the values are needed all - * the time. - */ -data class EbicsContainer( - val partnerId: String, - val userId: String, - var bankAuthPub: RSAPublicKey?, - var bankEncPub: RSAPublicKey?, - // needed to send the message - val ebicsUrl: String, - // needed to craft further messages - val hostId: String, - // needed to decrypt data coming from the bank - val customerEncPriv: RSAPrivateCrtKey, - // needed to sign documents - val customerAuthPriv: RSAPrivateCrtKey, - val customerSignPriv: RSAPrivateCrtKey -) -\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Db.kt diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt @@ -0,0 +1,248 @@ +package tech.libeufin.nexus + +import io.ktor.application.call +import io.ktor.client.HttpClient +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.response.respondText +import tech.libeufin.util.CryptoUtil +import tech.libeufin.util.EbicsOrderUtil +import tech.libeufin.util.ebics_h004.EbicsRequest +import tech.libeufin.util.ebics_h004.EbicsResponse +import tech.libeufin.util.ebics_h004.EbicsTypes +import tech.libeufin.util.getGregorianCalendarNow +import java.lang.StringBuilder +import java.math.BigInteger +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.util.* +import java.util.zip.DeflaterInputStream +import javax.xml.datatype.XMLGregorianCalendar + +/** + * This class is a mere container that keeps data found + * in the database and that is further needed to sign / verify + * / make messages. And not all the values are needed all + * the time. + */ +data class EbicsSubscriberDetails( + val partnerId: String, + val userId: String, + var bankAuthPub: RSAPublicKey?, + var bankEncPub: RSAPublicKey?, + // needed to send the message + val ebicsUrl: String, + // needed to craft further messages + val hostId: String, + // needed to decrypt data coming from the bank + val customerEncPriv: RSAPrivateCrtKey, + // needed to sign documents + val customerAuthPriv: RSAPrivateCrtKey, + val customerSignPriv: RSAPrivateCrtKey +) + + +fun createDownloadInitializationPhase( + subscriberData: EbicsSubscriberDetails, + orderType: String, + nonce: ByteArray, + date: XMLGregorianCalendar +): EbicsRequest { + return EbicsRequest.createForDownloadInitializationPhase( + subscriberData.userId, + subscriberData.partnerId, + subscriberData.hostId, + nonce, + date, + subscriberData.bankEncPub ?: throw BankKeyMissing( + HttpStatusCode.PreconditionFailed + ), + subscriberData.bankAuthPub ?: throw BankKeyMissing( + HttpStatusCode.PreconditionFailed + ), + orderType + ) +} + + +fun createUploadInitializationPhase( + subscriberData: EbicsSubscriberDetails, + orderType: String, + cryptoBundle: CryptoUtil.EncryptionResult +): EbicsRequest { + return EbicsRequest.createForUploadInitializationPhase( + cryptoBundle, + subscriberData.hostId, + getNonce(128), + subscriberData.partnerId, + subscriberData.userId, + getGregorianCalendarNow(), + subscriberData.bankAuthPub!!, + subscriberData.bankEncPub!!, + BigInteger.ONE, + orderType + ) +} + + +/** + * Wrapper around the lower decryption routine, that takes a EBICS response + * object containing a encrypted payload, and return the plain version of it + * (including decompression). + */ +fun decryptAndDecompressResponse(chunks: List<String>, transactionKey: ByteArray, privateKey: RSAPrivateCrtKey, pubDigest: ByteArray): ByteArray { + val buf = StringBuilder() + chunks.forEach { buf.append(it) } + val decoded = Base64.getDecoder().decode(buf.toString()) + val er = CryptoUtil.EncryptionResult( + transactionKey, + pubDigest, + decoded + ) + val dataCompr = CryptoUtil.decryptEbicsE002( + er, + privateKey + ) + return EbicsOrderUtil.decodeOrderData(dataCompr) +} + + +/** + * Get the private key that matches the given public key digest. + */ +fun getDecryptionKey(subscriberDetails: EbicsSubscriberDetails, pubDigest: ByteArray): RSAPrivateCrtKey { + val authPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerAuthPriv) + val encPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv) + val authPubDigest = CryptoUtil.getEbicsPublicKeyHash(authPub) + val encPubDigest = CryptoUtil.getEbicsPublicKeyHash(encPub) + if (pubDigest.contentEquals(authPubDigest)) { + return subscriberDetails.customerAuthPriv + } + if (pubDigest.contentEquals(encPubDigest)) { + return subscriberDetails.customerEncPriv + } + throw Exception("no matching private key to decrypt response") +} + + +/** + * Do an EBICS download transaction. This includes the initialization phase, transaction phase + * and receipt phase. + */ +suspend fun doEbicsDownloadTransaction( + client: HttpClient, + subscriberDetails: EbicsSubscriberDetails, + orderType: String +): ByteArray { + val initDownloadRequest = createDownloadInitializationPhase( + subscriberDetails, + orderType, + getNonce(128), + getGregorianCalendarNow() + ) + val payloadChunks = LinkedList<String>(); + val initResponse = client.postToBankSigned<EbicsRequest, EbicsResponse>( + subscriberDetails.ebicsUrl, + initDownloadRequest, + subscriberDetails.customerAuthPriv + ) + if (initResponse.value.body.returnCode.value != "000000") { + throw EbicsError(initResponse.value.body.returnCode.value) + } + val initDataTransfer = initResponse.value.body.dataTransfer + ?: throw ProtocolViolationError("initial response for download transaction does not contain data transfer") + val dataEncryptionInfo = initDataTransfer.dataEncryptionInfo + ?: throw ProtocolViolationError("initial response for download transaction does not contain date encryption info") + val initOrderData = initDataTransfer.orderData.value + // FIXME: Also verify that algorithm matches! + val decryptionKey = getDecryptionKey(subscriberDetails, dataEncryptionInfo.encryptionPubKeyDigest.value) + payloadChunks.add(initOrderData) + val respPayload = decryptAndDecompressResponse( + payloadChunks, + dataEncryptionInfo.transactionKey, + decryptionKey, + dataEncryptionInfo.encryptionPubKeyDigest.value + ) + val ackRequest = EbicsRequest.createForDownloadReceiptPhase( + initResponse.value.header._static.transactionID ?: throw BankInvalidResponse( + HttpStatusCode.ExpectationFailed + ), + subscriberDetails.hostId + ) + val ackResponse = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( + subscriberDetails.ebicsUrl, + ackRequest, + subscriberDetails.bankAuthPub ?: throw BankKeyMissing( + HttpStatusCode.PreconditionFailed + ), + subscriberDetails.customerAuthPriv + ) + if (ackResponse.value.body.returnCode.value != "000000") { + throw EbicsError(ackResponse.value.body.returnCode.value) + } + return respPayload +} + + +suspend fun doEbicsUploadTransaction( + client: HttpClient, + subscriberDetails: EbicsSubscriberDetails, + orderType: String, + payload: ByteArray +) { + if (subscriberDetails.bankEncPub == null) { + throw InvalidSubscriberStateError("bank encryption key unknown, request HPB first") + } + val usd_encrypted = CryptoUtil.encryptEbicsE002( + EbicsOrderUtil.encodeOrderDataXml( + signOrder( + payload, + subscriberDetails.customerSignPriv, + subscriberDetails.partnerId, + subscriberDetails.userId + ) + ), + subscriberDetails.bankEncPub!! + ) + val response = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( + subscriberDetails.ebicsUrl, + createUploadInitializationPhase( + subscriberDetails, + orderType, + usd_encrypted + ), + subscriberDetails.bankAuthPub!!, + subscriberDetails.customerAuthPriv + ) + if (response.value.header.mutable.returnCode != "000000") { + throw EbicsError(response.value.header.mutable.returnCode) + } + if (response.value.body.returnCode.value != "000000") { + throw EbicsError(response.value.body.returnCode.value) + } + logger.debug("INIT phase passed!") + /* now send actual payload */ + val compressedInnerPayload = DeflaterInputStream( + payload.inputStream() + ).use { it.readAllBytes() } + val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( + compressedInnerPayload, + subscriberDetails.bankEncPub!!, + usd_encrypted.plainTransactionKey!! + ) + val tmp = EbicsRequest.createForUploadTransferPhase( + subscriberDetails.hostId, + response.value.header._static.transactionID!!, + BigInteger.ONE, + encryptedPayload.encryptedData + ) + val responseTransaction = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( + subscriberDetails.ebicsUrl, + tmp, + subscriberDetails.bankAuthPub!!, + subscriberDetails.customerAuthPriv + ) + if (responseTransaction.value.body.returnCode.value != "000000") { + throw EbicsError(response.value.body.returnCode.value) + } +} +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -17,126 +17,6 @@ import java.util.* import javax.xml.bind.JAXBElement import javax.xml.datatype.XMLGregorianCalendar - -/** - * Wrapper around the lower decryption routine, that takes a EBICS response - * object containing a encrypted payload, and return the plain version of it - * (including decompression). - */ -fun decryptAndDecompressResponse(response: EbicsResponse, privateKey: RSAPrivateCrtKey): ByteArray { - val er = CryptoUtil.EncryptionResult( - response.body.dataTransfer!!.dataEncryptionInfo!!.transactionKey, - (response.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) - .encryptionPubKeyDigest.value, - Base64.getDecoder().decode(response.body.dataTransfer!!.orderData.value) - ) - val dataCompr = CryptoUtil.decryptEbicsE002( - er, - privateKey - ) - return EbicsOrderUtil.decodeOrderData(dataCompr) -} - -fun createDownloadInitializationPhase( - subscriberData: EbicsContainer, - orderType: String, - nonce: ByteArray, - date: XMLGregorianCalendar -): EbicsRequest { - return EbicsRequest.createForDownloadInitializationPhase( - subscriberData.userId, - subscriberData.partnerId, - subscriberData.hostId, - nonce, - date, - subscriberData.bankEncPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberData.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - orderType - ) -} - -fun createDownloadInitializationPhase( - subscriberData: EbicsContainer, - orderType: String, - nonce: ByteArray, - date: XMLGregorianCalendar, - dateStart: XMLGregorianCalendar, - dateEnd: XMLGregorianCalendar -): EbicsRequest { - return EbicsRequest.createForDownloadInitializationPhase( - subscriberData.userId, - subscriberData.partnerId, - subscriberData.hostId, - nonce, - date, - subscriberData.bankEncPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberData.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - orderType, - dateStart, - dateEnd - ) -} - -fun createUploadInitializationPhase( - subscriberData: EbicsContainer, - orderType: String, - cryptoBundle: CryptoUtil.EncryptionResult -): EbicsRequest { - return EbicsRequest.createForUploadInitializationPhase( - cryptoBundle, - subscriberData.hostId, - getNonce(128), - subscriberData.partnerId, - subscriberData.userId, - getGregorianDate(), - subscriberData.bankAuthPub!!, - subscriberData.bankEncPub!!, - BigInteger.ONE, - orderType - ) -} - -/** - * Usually, queries must return lots of data from within a transaction - * block. For convenience, we wrap such data into a EbicsContainer, so - * that only one object is always returned from the transaction block. - */ -fun containerInit(subscriber: EbicsSubscriberEntity): EbicsContainer { - var bankAuthPubValue: RSAPublicKey? = null - if (subscriber.bankAuthenticationPublicKey != null) { - bankAuthPubValue = CryptoUtil.loadRsaPublicKey( - subscriber.bankAuthenticationPublicKey?.toByteArray()!! - ) - } - var bankEncPubValue: RSAPublicKey? = null - if (subscriber.bankEncryptionPublicKey != null) { - bankEncPubValue = CryptoUtil.loadRsaPublicKey( - subscriber.bankEncryptionPublicKey?.toByteArray()!! - ) - } - return EbicsContainer( - bankAuthPub = bankAuthPubValue, - bankEncPub = bankEncPubValue, - - ebicsUrl = subscriber.ebicsURL, - hostId = subscriber.hostID, - userId = subscriber.userID, - partnerId = subscriber.partnerID, - - customerSignPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()), - customerAuthPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()), - customerEncPriv = CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray()) - ) -} - /** * Inserts spaces every 2 characters, and a newline after 8 pairs. */ @@ -191,7 +71,7 @@ fun signOrder( * response already converted in JAXB. */ suspend inline fun HttpClient.postToBank(url: String, body: String): String { - LOGGER.debug("Posting: $body") + logger.debug("Posting: $body") val response = try { this.post<String>( urlString = url, @@ -217,7 +97,7 @@ suspend inline fun <reified T, reified S> HttpClient.postToBankSignedAndVerify( val doc = XMLUtil.convertJaxbToDocument(body) XMLUtil.signEbicsDocument(doc, priv) val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - LOGGER.debug("About to verify: ${response}") + logger.debug("About to verify: ${response}") val responseDocument = try { XMLUtil.parseStringIntoDom(response) } catch (e: Exception) { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -38,13 +38,10 @@ import io.ktor.response.respondText import io.ktor.routing.* import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty -import io.ktor.util.encodeBase64 -import org.jetbrains.exposed.dao.EntityID import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.addLogger import org.jetbrains.exposed.sql.transactions.transaction -import org.joda.time.DateTime import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level @@ -55,10 +52,10 @@ import javax.sql.rowset.serial.SerialBlob import tech.libeufin.util.toHexString import tech.libeufin.util.CryptoUtil import tech.libeufin.util.EbicsOrderUtil -import tech.libeufin.util.XMLUtil import tech.libeufin.util.ebics_hev.HEVRequest import tech.libeufin.util.ebics_hev.HEVResponse import java.math.BigInteger +import java.security.interfaces.RSAPublicKey import java.text.SimpleDateFormat import java.util.* import java.util.zip.DeflaterInputStream @@ -82,7 +79,7 @@ fun testData() { } } } catch (e: ExposedSQLException) { - LOGGER.info("Likely primary key collision for sample data: accepted") + logger.info("Likely primary key collision for sample data: accepted") } } @@ -90,26 +87,63 @@ data class NotAnIdError(val statusCode: HttpStatusCode) : Exception("String ID n data class BankKeyMissing(val statusCode: HttpStatusCode) : Exception("Impossible operation: bank keys are missing") data class SubscriberNotFoundError(val statusCode: HttpStatusCode) : Exception("Subscriber not found in database") data class UnreachableBankError(val statusCode: HttpStatusCode) : Exception("Could not reach the bank") -data class UnparsableResponse(val statusCode: HttpStatusCode, val rawResponse: String) : Exception("bank responded: ${rawResponse}") +data class UnparsableResponse(val statusCode: HttpStatusCode, val rawResponse: String) : + Exception("bank responded: ${rawResponse}") + +class ProtocolViolationError(message: String) : Exception("protocol violation: ${message}") +class InvalidSubscriberStateError(message: String) : Exception("invalid subscriber state: ${message}") data class EbicsError(val codeError: String) : Exception("Bank did not accepted EBICS request, error is: ${codeError}") data class BadSignature(val statusCode: HttpStatusCode) : Exception("Signature verification unsuccessful") data class BadBackup(val statusCode: HttpStatusCode) : Exception("Could not restore backed up keys") data class BankInvalidResponse(val statusCode: HttpStatusCode) : Exception("Missing data from bank response") -val LOGGER: Logger = LoggerFactory.getLogger("tech.libeufin.nexus") +val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus") + +fun getSubscriberDetailsFromId(id: String): EbicsSubscriberDetails { + return transaction { + val subscriber = EbicsSubscriberEntity.findById( + id + ) ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + var bankAuthPubValue: RSAPublicKey? = null + if (subscriber.bankAuthenticationPublicKey != null) { + bankAuthPubValue = CryptoUtil.loadRsaPublicKey( + subscriber.bankAuthenticationPublicKey?.toByteArray()!! + ) + } + var bankEncPubValue: RSAPublicKey? = null + if (subscriber.bankEncryptionPublicKey != null) { + bankEncPubValue = CryptoUtil.loadRsaPublicKey( + subscriber.bankEncryptionPublicKey?.toByteArray()!! + ) + } + EbicsSubscriberDetails( + bankAuthPub = bankAuthPubValue, + bankEncPub = bankEncPubValue, + + ebicsUrl = subscriber.ebicsURL, + hostId = subscriber.hostID, + userId = subscriber.userID, + partnerId = subscriber.partnerID, + + customerSignPriv = CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()), + customerAuthPriv = CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()), + customerEncPriv = CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray()) + ) + } +} fun main() { dbCreateTables() testData() - val client = HttpClient(){ + val client = HttpClient() { expectSuccess = false // this way, it does not throw exceptions on != 200 responses. } - val logger = LoggerFactory.getLogger("tech.libeufin.nexus") val server = embeddedServer(Netty, port = 5001) { install(CallLogging) { this.level = Level.DEBUG - this.logger = LOGGER - + this.logger = tech.libeufin.nexus.logger } install(ContentNegotiation) { moshi { @@ -137,18 +171,28 @@ fun main() { exception<BadBackup> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Bad backup, or passphrase incorrect\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) + call.respondText( + "Bad backup, or passphrase incorrect\n", + ContentType.Text.Plain, + HttpStatusCode.BadRequest + ) } exception<UnparsableResponse> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Could not parse bank response (${cause.message})\n", ContentType.Text.Plain, HttpStatusCode - .InternalServerError) + call.respondText( + "Could not parse bank response (${cause.message})\n", ContentType.Text.Plain, HttpStatusCode + .InternalServerError + ) } exception<UnreachableBankError> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Could not reach the bank\n", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + call.respondText( + "Could not reach the bank\n", + ContentType.Text.Plain, + HttpStatusCode.InternalServerError + ) } exception<SubscriberNotFoundError> { cause -> @@ -158,17 +202,29 @@ fun main() { exception<BadSignature> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Signature verification unsuccessful\n", ContentType.Text.Plain, HttpStatusCode.NotAcceptable) + call.respondText( + "Signature verification unsuccessful\n", + ContentType.Text.Plain, + HttpStatusCode.NotAcceptable + ) } exception<EbicsError> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Bank gave EBICS-error response\n", ContentType.Text.Plain, HttpStatusCode.NotAcceptable) + call.respondText( + "Bank gave EBICS-error response\n", + ContentType.Text.Plain, + HttpStatusCode.NotAcceptable + ) } exception<BankKeyMissing> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Impossible operation: get bank keys first\n", ContentType.Text.Plain, HttpStatusCode.NotAcceptable) + call.respondText( + "Impossible operation: get bank keys first\n", + ContentType.Text.Plain, + HttpStatusCode.NotAcceptable + ) } exception<javax.xml.bind.UnmarshalException> { cause -> @@ -180,12 +236,14 @@ fun main() { ) } } + intercept(ApplicationCallPipeline.Fallback) { if (this.call.response.status() == null) { call.respondText("Not found (no route matched).\n", ContentType.Text.Plain, HttpStatusCode.NotFound) return@intercept finish() } } + routing { get("/") { call.respondText("Hello by Nexus!\n") @@ -194,203 +252,124 @@ fun main() { post("/ebics/subscribers/{id}/sendPTK") { val id = expectId(call.parameters["id"]) - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById( - id - ) ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } - val response = client.postToBankSigned<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - createDownloadInitializationPhase( - subscriberData, - "PTK", - getNonce(128), - getGregorianDate() - ), - subscriberData.customerAuthPriv - ) - val payload: ByteArray = - decryptAndDecompressResponse( - response.value, - subscriberData.customerAuthPriv - ) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "PTK") call.respondText( - payload.toString(Charsets.UTF_8), + response.toString(Charsets.UTF_8), ContentType.Text.Plain, - HttpStatusCode.OK) - + HttpStatusCode.OK + ) return@post } post("/ebics/subscribers/{id}/sendHAC") { val id = expectId(call.parameters["id"]) - - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById( - id - ) ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } - val response = client.postToBankSigned<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - createDownloadInitializationPhase( - subscriberData, - "HAC", - getNonce(128), - getGregorianDate() - ), - subscriberData.customerAuthPriv - ) - val payload: ByteArray = - decryptAndDecompressResponse( - response.value, - subscriberData.customerAuthPriv - ) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HAC") call.respondText( - payload.toString(Charsets.UTF_8), + response.toString(Charsets.UTF_8), ContentType.Text.Plain, - HttpStatusCode.OK) - + HttpStatusCode.OK + ) return@post } - post("/ebics/subscribers/{id}/sendC52") { val id = expectId(call.parameters["id"]) - val body = call.receive<EbicsDateRange>() - - val startDate = DateTime.parse(body.start) - val endDate = DateTime.parse(body.end) - // will throw DateTimeParseException if strings are malformed. - - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById( - id - ) ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } - val response = client.postToBankSigned<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - createDownloadInitializationPhase( - subscriberData, - "C52", - getNonce(128), - getGregorianDate(), - getGregorianDate(startDate.year, startDate.monthOfYear - 1, startDate.dayOfMonth), - getGregorianDate(endDate.year, endDate.monthOfYear - 1, endDate.dayOfMonth) - ), - subscriberData.customerAuthPriv - ) - val payload: ByteArray = - decryptAndDecompressResponse( - response.value, - subscriberData.customerAuthPriv - ) - val ackRequest = EbicsRequest.createForDownloadReceiptPhase( - response.value.header._static.transactionID ?: throw BankInvalidResponse( - HttpStatusCode.ExpectationFailed - ), - subscriberData.hostId + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "C52") + call.respondText( + response.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK ) + return@post + } - val ackResponse = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - ackRequest, - subscriberData.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberData.customerAuthPriv + post("/ebics/subscribers/{id}/sendHtd") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HTD") + call.respondText( + response.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK ) - logger.debug("C52 final response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) - if (ackResponse.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } + return@post + } + post("/ebics/subscribers/{id}/sendHAA") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HAA") call.respondText( - payload.toString(Charsets.UTF_8), + response.toString(Charsets.UTF_8), ContentType.Text.Plain, - HttpStatusCode.OK) - + HttpStatusCode.OK + ) return@post } - get("/ebics/subscribers/{id}/sendHtd") { + + post("/ebics/subscribers/{id}/sendHVZ") { val id = expectId(call.parameters["id"]) - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById( - id - ) ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } - val response = client.postToBankSigned<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - createDownloadInitializationPhase( - subscriberData, - "HTD", - getNonce(128), - getGregorianDate() - ), - subscriberData.customerAuthPriv + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HVZ") + call.respondText( + response.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK ) - logger.debug("HTD response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) - if (response.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } - - val encPubKeyDigestViaBank = (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) - .encryptionPubKeyDigest.value; - println("encPubKeyDigestViaBank ${encPubKeyDigestViaBank.toHexString()}") + return@post + } - val er = CryptoUtil.EncryptionResult( - response.value.body.dataTransfer!!.dataEncryptionInfo!!.transactionKey, - (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) - .encryptionPubKeyDigest.value, - Base64.getDecoder().decode(response.value.body.dataTransfer!!.orderData.value) + post("/ebics/subscribers/{id}/sendHVU") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HVU") + call.respondText( + response.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK ) + return@post + } - val dataCompr = CryptoUtil.decryptEbicsE002( - er, - subscriberData.customerAuthPriv - ) - val data = EbicsOrderUtil.decodeOrderDataXml<HTDResponseOrderData>(dataCompr) - logger.debug("HTD payload is: ${XMLUtil.convertJaxbToString(data)}") - val ackRequest = EbicsRequest.createForDownloadReceiptPhase( - response.value.header._static.transactionID ?: throw BankInvalidResponse( - HttpStatusCode.ExpectationFailed - ), - subscriberData.hostId + post("/ebics/subscribers/{id}/sendHPD") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HPD") + call.respondText( + response.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK ) + return@post + } - val ackResponse = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - ackRequest, - subscriberData.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberData.customerAuthPriv + post("/ebics/subscribers/{id}/sendHKD") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "HKD") + call.respondText( + response.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK ) - logger.debug("HTD final response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) - if (ackResponse.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } + return@post + } + + post("/ebics/subscribers/{id}/sendTSD") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "TSD") call.respondText( - "Success! Details (temporarily) reported on the Nexus console.", + response.toString(Charsets.UTF_8), ContentType.Text.Plain, HttpStatusCode.OK ) + return@post } + get("/ebics/subscribers/{id}/keyletter") { val id = expectId(call.parameters["id"]) var usernameLine = "TODO" @@ -520,8 +499,9 @@ fun main() { HttpStatusCode.OK ) } + get("/ebics/subscribers") { - var ret = EbicsSubscribersResponse() + val ret = EbicsSubscribersResponse() transaction { EbicsSubscriberEntity.all().forEach { ret.ebicsSubscribers.add( @@ -539,6 +519,7 @@ fun main() { call.respond(ret) return@get } + get("/ebics/subscribers/{id}") { val id = expectId(call.parameters["id"]) val response = transaction { @@ -557,6 +538,7 @@ fun main() { call.respond(HttpStatusCode.OK, response) return@get } + get("/ebics/{id}/sendHev") { val id = expectId(call.parameters["id"]) val (ebicsUrl, hostID) = transaction { @@ -572,10 +554,12 @@ fun main() { call.respond( HttpStatusCode.OK, EbicsHevResponse(response.value.versionNumber!!.map { - ProtocolAndVersion(it.value, it.protocolVersion, hostID) }) + ProtocolAndVersion(it.value, it.protocolVersion, hostID) + }) ) return@get } + post("/ebics/{id}/subscribers") { val body = call.receive<EbicsSubscriberInfoRequest>() val pairA = CryptoUtil.generateRsaKeyPair(2048) @@ -606,16 +590,10 @@ fun main() { ) return@post } + post("/ebics/subscribers/{id}/sendIni") { val id = expectId(call.parameters["id"]) - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) - ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } + val subscriberData = getSubscriberDetailsFromId(id) val iniRequest = EbicsUnsecuredRequest.createIni( subscriberData.hostId, subscriberData.userId, @@ -632,6 +610,7 @@ fun main() { call.respondText("Bank accepted signature key\n", ContentType.Text.Plain, HttpStatusCode.OK) return@post } + post("/ebics/subscribers/{id}/restoreBackup") { val body = call.receive<EbicsKeysBackup>() val id = expectId(call.parameters["id"]) @@ -659,10 +638,10 @@ fun main() { ) } catch (e: Exception) { e.printStackTrace() - LOGGER.info("Restoring keys failed, probably due to wrong passphrase") + logger.info("Restoring keys failed, probably due to wrong passphrase") throw BadBackup(HttpStatusCode.BadRequest) } - LOGGER.info("Restoring keys, creating new user: $id") + logger.info("Restoring keys, creating new user: $id") try { transaction { EbicsSubscriberEntity.new(id = expectId(call.parameters["id"])) { @@ -704,7 +683,7 @@ fun main() { bytesToBase64(authPub.encoded), bytesToBase64(encPub.encoded), bytesToBase64(sigPub.encoded) - ) + ) } call.respond( HttpStatusCode.OK, @@ -725,18 +704,24 @@ fun main() { hostID = subscriber.hostID, partnerID = subscriber.partnerID, ebicsURL = subscriber.ebicsURL, - authBlob = bytesToBase64(CryptoUtil.encryptKey( - subscriber.authenticationPrivateKey.toByteArray(), - body.passphrase - )), - encBlob = bytesToBase64(CryptoUtil.encryptKey( - subscriber.encryptionPrivateKey.toByteArray(), - body.passphrase - )), - sigBlob = bytesToBase64(CryptoUtil.encryptKey( - subscriber.signaturePrivateKey.toByteArray(), - body.passphrase - )) + authBlob = bytesToBase64( + CryptoUtil.encryptKey( + subscriber.authenticationPrivateKey.toByteArray(), + body.passphrase + ) + ), + encBlob = bytesToBase64( + CryptoUtil.encryptKey( + subscriber.encryptionPrivateKey.toByteArray(), + body.passphrase + ) + ), + sigBlob = bytesToBase64( + CryptoUtil.encryptKey( + subscriber.signaturePrivateKey.toByteArray(), + body.passphrase + ) + ) ) } call.response.headers.append("Content-Disposition", "attachment") @@ -745,76 +730,14 @@ fun main() { response ) } - post("/ebics/subscribers/{id}/sendTst") { + + post("/ebics/subscribers/{id}/sendTSU") { val id = expectId(call.parameters["id"]) - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) - ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } + val subscriberData = getSubscriberDetailsFromId(id) val payload = "PAYLOAD" - if (subscriberData.bankEncPub == null) { - call.respondText( - "Bank encryption key not found, request HPB first!\n", - ContentType.Text.Plain, - HttpStatusCode.NotFound - ) - return@post - } - val usd_encrypted = CryptoUtil.encryptEbicsE002( - EbicsOrderUtil.encodeOrderDataXml( - - signOrder( - payload.toByteArray(), - subscriberData.customerSignPriv, - subscriberData.partnerId, - subscriberData.userId - ) - ), - subscriberData.bankEncPub!! - ) - val response = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - createUploadInitializationPhase( - subscriberData, - "TST", - usd_encrypted - ), - subscriberData.bankAuthPub!!, - subscriberData.customerAuthPriv - ) - if (response.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } - logger.debug("INIT phase passed!") - /* now send actual payload */ - val compressedInnerPayload = DeflaterInputStream( - payload.toByteArray().inputStream() - - ).use { it.readAllBytes() } - val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( - compressedInnerPayload, - subscriberData.bankEncPub!!, - usd_encrypted.plainTransactionKey!! - ) - val tmp = EbicsRequest.createForUploadTransferPhase( - subscriberData.hostId, - response.value.header._static.transactionID!!, - BigInteger.ONE, - encryptedPayload.encryptedData - ) - val responseTransaction = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - tmp, - subscriberData.bankAuthPub!!, - subscriberData.customerAuthPriv - ) - if (responseTransaction.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } + + doEbicsUploadTransaction(client, subscriberData, "TSU", payload.toByteArray(Charsets.UTF_8)) + call.respondText( "TST INITIALIZATION & TRANSACTION phases succeeded\n", ContentType.Text.Plain, @@ -824,34 +747,29 @@ fun main() { post("/ebics/subscribers/{id}/sync") { val id = expectId(call.parameters["id"]) - val bundle = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) - ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } + val subscriberDetails = getSubscriberDetailsFromId(id) val response = client.postToBankSigned<EbicsNpkdRequest, EbicsKeyManagementResponse>( - bundle.ebicsUrl, + subscriberDetails.ebicsUrl, EbicsNpkdRequest.createRequest( - bundle.hostId, - bundle.partnerId, - bundle.userId, + subscriberDetails.hostId, + subscriberDetails.partnerId, + subscriberDetails.userId, getNonce(128), - getGregorianDate() + getGregorianCalendarNow() ), - bundle.customerAuthPriv + subscriberDetails.customerAuthPriv ) if (response.value.body.returnCode.value != "000000") { throw EbicsError(response.value.body.returnCode.value) } - val encPubKeyDigestViaBank = (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) - .encryptionPubKeyDigest.value; - val customerEncPub = CryptoUtil.getRsaPublicFromPrivate(bundle.customerEncPriv); + val encPubKeyDigestViaBank = + (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) + .encryptionPubKeyDigest.value; + val customerEncPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv); val encPubKeyDigestViaNexus = CryptoUtil.getEbicsPublicKeyHash(customerEncPub) println("encPubKeyDigestViaBank: ${encPubKeyDigestViaBank.toHexString()}") println("encPubKeyDigestViaNexus: ${encPubKeyDigestViaNexus.toHexString()}") + val decryptionKey = getDecryptionKey(subscriberDetails, encPubKeyDigestViaBank) val er = CryptoUtil.EncryptionResult( response.value.body.dataTransfer!!.dataEncryptionInfo!!.transactionKey, encPubKeyDigestViaBank, @@ -859,7 +777,7 @@ fun main() { ) val dataCompr = CryptoUtil.decryptEbicsE002( er, - bundle.customerAuthPriv + decryptionKey ) val data = EbicsOrderUtil.decodeOrderDataXml<HPBResponseOrderData>(dataCompr) // put bank's keys into database. @@ -883,16 +801,10 @@ fun main() { call.respondText("Bank keys stored in database\n", ContentType.Text.Plain, HttpStatusCode.OK) return@post } + post("/ebics/subscribers/{id}/sendHia") { val id = expectId(call.parameters["id"]) - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) - ?: throw SubscriberNotFoundError( - HttpStatusCode.NotFound - ) - ) - } + val subscriberData = getSubscriberDetailsFromId(id) val responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( subscriberData.ebicsUrl, EbicsUnsecuredRequest.createHia( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -40,17 +40,18 @@ import tech.libeufin.util.EbicsOrderUtil import tech.libeufin.util.XMLUtil import tech.libeufin.util.* import tech.libeufin.util.XMLUtil.Companion.signEbicsResponse -import java.awt.List import java.math.BigDecimal import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey import java.util.* import java.util.zip.DeflaterInputStream import java.util.zip.InflaterInputStream import javax.sql.rowset.serial.SerialBlob -open class EbicsRequestError(val errorText: String, val errorCode: String) : - Exception("EBICS request management error: $errorText ($errorCode)") +open class EbicsRequestError(errorText: String, errorCode: String) : + Exception("EBICS request error: $errorText ($errorCode)") + class EbicsInvalidRequestError : EbicsRequestError( "[EBICS_INVALID_REQUEST] Invalid request", @@ -146,13 +147,13 @@ private fun iterHistory(customerId: Int, header: EbicsRequest.Header, base: XmlE (header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange!!.start.toString() } catch (e: Exception) { LOGGER.debug("Asked to iterate over history with NO start date; default to now") - getGregorianDate().toString() + getGregorianCalendarNow().toString() }, try { (header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange!!.end.toString() } catch (e: Exception) { LOGGER.debug("Asked to iterate over history with NO end date; default to now") - getGregorianDate().toString() + getGregorianCalendarNow().toString() } ) { @@ -254,9 +255,7 @@ private fun balance(base: XmlElementBuilder) { * @param type 52 or 53. */ private fun constructCamtResponse(type: Int, customerId: Int, header: EbicsRequest.Header): String { - val camt = constructXml(indent = true) { - namespace("foo", "bar") // FIXME: set right namespace! root("foo:BkToCstmrAcctRpt") { element("GrpHdr") { @@ -273,21 +272,18 @@ private fun constructCamtResponse(type: Int, customerId: Int, header: EbicsReque } } } - return camt } -private fun ApplicationCall.handleEbicsC52(header: EbicsRequest.Header): ByteArray { - val userId = header.static.userID!! +private fun handleEbicsTSD(requestContext: RequestContext): ByteArray { + return "Hello World".toByteArray() +} - val subscriber = transaction { - EbicsSubscriberEntity.find { - stringParam(userId) eq EbicsSubscribersTable.userId // will have to match partner and system IDs - } - }.firstOrNull() ?: throw Exception("Unknown subscriber") - return constructCamtResponse(52, subscriber.bankCustomer.id.value, header).toByteArray() +private fun handleEbicsC52(requestContext: RequestContext): ByteArray { + val subscriber = requestContext.subscriber + return constructCamtResponse(52, subscriber.bankCustomer.id.value, requestContext.requestObject.header).toByteArray() } private suspend fun ApplicationCall.handleEbicsHia(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) { @@ -629,6 +625,238 @@ fun handleEbicsHkd(): ByteArray { } +private data class RequestContext( + val ebicsHost: EbicsHostEntity, + val subscriber: EbicsSubscriberEntity, + val clientEncPub: RSAPublicKey, + val clientAuthPub: RSAPublicKey, + val clientSigPub: RSAPublicKey, + val hostEncPriv: RSAPrivateCrtKey, + val hostAuthPriv: RSAPrivateCrtKey, + val requestObject: EbicsRequest, + val uploadTransaction: EbicsUploadTransactionEntity?, + val downloadTransaction: EbicsDownloadTransactionEntity? +) + + +private fun handleEbicsDownloadTransactionInitialization(requestContext: RequestContext): EbicsResponse { + val orderType = + requestContext.requestObject.header.static.orderDetails?.orderType ?: throw EbicsInvalidRequestError() + println("handling initialization for order type $orderType") + val response = when (orderType) { + "HTD" -> handleEbicsHtd() + "HKD" -> handleEbicsHkd() + /* Temporarily handling C52/C53 with same logic */ + "C52" -> handleEbicsC52(requestContext) + "C53" -> handleEbicsC52(requestContext) + "TSD" -> handleEbicsTSD(requestContext) + else -> throw EbicsInvalidXmlError() + } + + val transactionID = EbicsOrderUtil.generateTransactionId() + + val compressedResponse = DeflaterInputStream(response.inputStream()).use { + it.readAllBytes() + } + + val enc = CryptoUtil.encryptEbicsE002(compressedResponse, requestContext.clientEncPub) + val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData) + + val segmentSize = 4096 + val totalSize = encodedResponse.length + val numSegments = ((totalSize + segmentSize - 1) / segmentSize) + + EbicsDownloadTransactionEntity.new(transactionID) { + this.subscriber = requestContext.subscriber + this.host = requestContext.ebicsHost + this.orderType = orderType + this.segmentSize = segmentSize + this.transactionKeyEnc = SerialBlob(enc.encryptedTransactionKey) + this.encodedResponse = encodedResponse + this.numSegments = numSegments + this.receiptReceived = false + } + return EbicsResponse.createForDownloadInitializationPhase( + transactionID, + numSegments, + segmentSize, + enc, + encodedResponse + ) +} + + +private fun handleEbicsUploadTransactionInitialization(requestContext: RequestContext): EbicsResponse { + val orderType = + requestContext.requestObject.header.static.orderDetails?.orderType ?: throw EbicsInvalidRequestError() + val transactionID = EbicsOrderUtil.generateTransactionId() + val oidn = requestContext.subscriber.nextOrderID++ + if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError() + val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn) + val numSegments = + requestContext.requestObject.header.static.numSegments ?: throw EbicsInvalidRequestError() + val transactionKeyEnc = + requestContext.requestObject.body.dataTransfer?.dataEncryptionInfo?.transactionKey + ?: throw EbicsInvalidRequestError() + val encPubKeyDigest = + requestContext.requestObject.body.dataTransfer?.dataEncryptionInfo?.encryptionPubKeyDigest?.value + ?: throw EbicsInvalidRequestError() + val encSigData = requestContext.requestObject.body.dataTransfer?.signatureData?.value + ?: throw EbicsInvalidRequestError() + val decryptedSignatureData = CryptoUtil.decryptEbicsE002( + CryptoUtil.EncryptionResult( + transactionKeyEnc, + encPubKeyDigest, + encSigData + ), requestContext.hostEncPriv + ) + val plainSigData = InflaterInputStream(decryptedSignatureData.inputStream()).use { + it.readAllBytes() + } + + println("creating upload transaction for transactionID $transactionID") + EbicsUploadTransactionEntity.new(transactionID) { + this.host = requestContext.ebicsHost + this.subscriber = requestContext.subscriber + this.lastSeenSegment = 0 + this.orderType = orderType + this.orderID = orderID + this.numSegments = numSegments.toInt() + this.transactionKeyEnc = SerialBlob(transactionKeyEnc) + } + val sigObj = XMLUtil.convertStringToJaxb<UserSignatureData>(plainSigData.toString(Charsets.UTF_8)) + println("got UserSignatureData: ${plainSigData.toString(Charsets.UTF_8)}") + for (sig in sigObj.value.orderSignatureList ?: listOf()) { + println("inserting order signature for orderID $orderID and orderType $orderType") + EbicsOrderSignatureEntity.new { + this.orderID = orderID + this.orderType = orderType + this.partnerID = sig.partnerID + this.userID = sig.userID + this.signatureAlgorithm = sig.signatureVersion + this.signatureValue = SerialBlob(sig.signatureValue) + } + } + + return EbicsResponse.createForUploadInitializationPhase(transactionID, orderID) +} + + +private fun handleEbicsUploadTransactionTransmission(requestContext: RequestContext): EbicsResponse { + val uploadTransaction = requestContext.uploadTransaction ?: throw EbicsInvalidRequestError() + val requestObject = requestContext.requestObject + val requestSegmentNumber = + requestContext.requestObject.header.mutable.segmentNumber?.value?.toInt() ?: throw EbicsInvalidRequestError() + val requestTransactionID = requestObject.header.static.transactionID ?: throw EbicsInvalidRequestError() + if (requestSegmentNumber == 1 && uploadTransaction.numSegments == 1) { + val encOrderData = + requestObject.body.dataTransfer?.orderData ?: throw EbicsInvalidRequestError() + val zippedData = CryptoUtil.decryptEbicsE002( + uploadTransaction.transactionKeyEnc.toByteArray(), + encOrderData, + requestContext.hostEncPriv + ) + val unzippedData = + InflaterInputStream(zippedData.inputStream()).use { it.readAllBytes() } + println("got upload data: ${unzippedData.toString(Charsets.UTF_8)}") + + val sigs = EbicsOrderSignatureEntity.find { + (EbicsOrderSignaturesTable.orderID eq uploadTransaction.orderID) and + (EbicsOrderSignaturesTable.orderType eq uploadTransaction.orderType) + } + + if (sigs.count() == 0) { + throw EbicsInvalidRequestError() + } + + for (sig in sigs) { + if (sig.signatureAlgorithm == "A006") { + + val signedData = CryptoUtil.digestEbicsOrderA006(unzippedData) + val res1 = CryptoUtil.verifyEbicsA006(sig.signatureValue.toByteArray(), signedData, requestContext.clientSigPub) + + if (!res1) { + throw EbicsInvalidRequestError() + } + + } else { + throw NotImplementedError() + } + } + + return EbicsResponse.createForUploadTransferPhase( + requestTransactionID, + requestSegmentNumber, + true, + uploadTransaction.orderID + ) + } else { + throw NotImplementedError() + } +} + +private fun makeReqestContext(requestObject: EbicsRequest): RequestContext { + val staticHeader = requestObject.header.static + val requestedHostId = staticHeader.hostID + 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 + val subscriber = if (requestTransactionID != null) { + println("finding subscriber by transactionID $requestTransactionID") + downloadTransaction = EbicsDownloadTransactionEntity.findById(requestTransactionID.toUpperCase()) + if (downloadTransaction != null) { + downloadTransaction.subscriber + } else { + uploadTransaction = EbicsUploadTransactionEntity.findById(requestTransactionID) + uploadTransaction?.subscriber + } + } else { + val partnerID = staticHeader.partnerID ?: throw EbicsInvalidRequestError() + val userID = staticHeader.userID ?: throw EbicsInvalidRequestError() + findEbicsSubscriber(partnerID, userID, staticHeader.systemID) + } + + if (ebicsHost == null) throw EbicsInvalidRequestError() + + /** + * NOTE: production logic must check against READY state (the + * one activated after the subscriber confirms their keys via post) + */ + if (subscriber == null || subscriber.state != SubscriberState.INITIALIZED) + throw EbicsSubscriberStateError() + + val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( + ebicsHost.authenticationPrivateKey + .toByteArray() + ) + val hostEncPriv = CryptoUtil.loadRsaPrivateKey( + ebicsHost.encryptionPrivateKey + .toByteArray() + ) + val clientAuthPub = + CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.toByteArray()) + val clientEncPub = + CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.toByteArray()) + val clientSigPub = + CryptoUtil.loadRsaPublicKey(subscriber.signatureKey!!.rsaPublicKey.toByteArray()) + + return RequestContext( + hostAuthPriv = hostAuthPriv, + hostEncPriv = hostEncPriv, + clientAuthPub = clientAuthPub, + clientEncPub = clientEncPub, + clientSigPub = clientSigPub, + ebicsHost = ebicsHost, + requestObject = requestObject, + subscriber = subscriber, + downloadTransaction = downloadTransaction, + uploadTransaction = uploadTransaction + ) +} + suspend fun ApplicationCall.ebicsweb() { val requestDocument = receiveEbicsXml() @@ -673,229 +901,46 @@ suspend fun ApplicationCall.ebicsweb() { "ebicsRequest" -> { println("ebicsRequest ${XMLUtil.convertDomToString(requestDocument)}") val requestObject = requestDocument.toObject<EbicsRequest>() - val staticHeader = requestObject.header.static - val requestedHostId = staticHeader.hostID val responseXmlStr = transaction { // Step 1 of 3: Get information about the host and subscriber - 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 - val subscriber = if (requestTransactionID != null) { - println("finding subscriber by transactionID $requestTransactionID") - downloadTransaction = EbicsDownloadTransactionEntity.findById(requestTransactionID.toUpperCase()) - if (downloadTransaction != null) { - downloadTransaction.subscriber - } else { - uploadTransaction = EbicsUploadTransactionEntity.findById(requestTransactionID) - uploadTransaction?.subscriber - } - } else { - val partnerID = staticHeader.partnerID ?: throw EbicsInvalidRequestError() - val userID = staticHeader.userID ?: throw EbicsInvalidRequestError() - findEbicsSubscriber(partnerID, userID, staticHeader.systemID) - } - - if (ebicsHost == null) throw EbicsInvalidRequestError() - - /** - * NOTE: production logic must check against READY state (the - * one activated after the subscriber confirms their keys via post) - */ - if (subscriber == null || subscriber.state != SubscriberState.INITIALIZED) - throw EbicsSubscriberStateError() - - val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( - ebicsHost.authenticationPrivateKey - .toByteArray() - ) - val hostEncPriv = CryptoUtil.loadRsaPrivateKey( - ebicsHost.encryptionPrivateKey - .toByteArray() - ) - val clientAuthPub = - CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.toByteArray()) - val clientEncPub = - CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.toByteArray()) - val clientSigPub = - CryptoUtil.loadRsaPublicKey(subscriber.signatureKey!!.rsaPublicKey.toByteArray()) + val requestContext = makeReqestContext(requestObject) // Step 2 of 3: Validate the signature - val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, clientAuthPub) + val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, requestContext.clientAuthPub) if (!verifyResult) { throw EbicsInvalidRequestError() } + // Step 3 of 3: Generate response val ebicsResponse: EbicsResponse = when (requestObject.header.mutable.transactionPhase) { EbicsTypes.TransactionPhaseType.INITIALISATION -> { - val transactionID = EbicsOrderUtil.generateTransactionId() - 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() - "HKD" -> handleEbicsHkd() - - /* Temporarily handling C52/C53 with same logic */ - "C52" -> handleEbicsC52(requestObject.header) - "C53" -> handleEbicsC52(requestObject.header) - else -> throw EbicsInvalidXmlError() - } - - val compressedResponse = DeflaterInputStream(response.inputStream()).use { - it.readAllBytes() - } - - val enc = CryptoUtil.encryptEbicsE002(compressedResponse, clientEncPub) - val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData) - - val segmentSize = 4096 - val totalSize = encodedResponse.length - val numSegments = ((totalSize + segmentSize - 1) / segmentSize) - - EbicsDownloadTransactionEntity.new(transactionID) { - this.subscriber = subscriber - this.host = ebicsHost - this.orderType = orderType - this.segmentSize = segmentSize - this.transactionKeyEnc = SerialBlob(enc.encryptedTransactionKey) - this.encodedResponse = encodedResponse - this.numSegments = numSegments - this.receiptReceived = false - } - EbicsResponse.createForDownloadInitializationPhase( - transactionID, - numSegments, - segmentSize, - enc, - encodedResponse - ) + if (requestObject.header.static.numSegments == null) { + handleEbicsDownloadTransactionInitialization(requestContext) } else { - val oidn = subscriber.nextOrderID++ - if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError() - val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn) - 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?.value - if (encSigData == null) - throw EbicsInvalidRequestError() - val decryptedSignatureData = CryptoUtil.decryptEbicsE002( - CryptoUtil.EncryptionResult( - transactionKeyEnc, - encPubKeyDigest, - encSigData - ), hostEncPriv - ) - val plainSigData = InflaterInputStream(decryptedSignatureData.inputStream()).use { - it.readAllBytes() - } - - println("creating upload transaction for transactionID $transactionID") - EbicsUploadTransactionEntity.new(transactionID) { - this.host = ebicsHost - this.subscriber = subscriber - this.lastSeenSegment = 0 - this.orderType = orderType - this.orderID = orderID - this.numSegments = numSegments.toInt() - this.transactionKeyEnc = SerialBlob(transactionKeyEnc) - } - val sigObj = XMLUtil.convertStringToJaxb<UserSignatureData>(plainSigData.toString(Charsets.UTF_8)) - println("got UserSignatureData: ${plainSigData.toString(Charsets.UTF_8)}") - for (sig in sigObj.value.orderSignatureList ?: listOf()) { - println("inserting order signature for orderID $orderID and orderType $orderType") - EbicsOrderSignatureEntity.new { - this.orderID = orderID - this.orderType = orderType - this.partnerID = sig.partnerID - this.userID = sig.userID - this.signatureAlgorithm = sig.signatureVersion - this.signatureValue = SerialBlob(sig.signatureValue) - } - } - - EbicsResponse.createForUploadInitializationPhase(transactionID, orderID) + handleEbicsUploadTransactionInitialization(requestContext) } } EbicsTypes.TransactionPhaseType.TRANSFER -> { - requestTransactionID ?: throw EbicsInvalidRequestError() - val requestSegmentNumber = - requestObject.header.mutable.segmentNumber?.value?.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)}") - - val sigs = EbicsOrderSignatureEntity.find { - (EbicsOrderSignaturesTable.orderID eq uploadTransaction.orderID) and - (EbicsOrderSignaturesTable.orderType eq uploadTransaction.orderType) - } - - if (sigs.count() == 0) { - throw EbicsInvalidRequestError() - } - - for (sig in sigs) { - if (sig.signatureAlgorithm == "A006") { - - val signedData = CryptoUtil.digestEbicsOrderA006(unzippedData) - val res1 = CryptoUtil.verifyEbicsA006(sig.signatureValue.toByteArray(), signedData, clientSigPub) - - if (!res1) { - throw EbicsInvalidRequestError() - } - - } else { - throw NotImplementedError() - } - } - - EbicsResponse.createForUploadTransferPhase( - requestTransactionID, - requestSegmentNumber, - true, - uploadTransaction.orderID - ) - } else { - throw NotImplementedError() - } - } else if (downloadTransaction != null) { + if (requestContext.uploadTransaction != null) { + handleEbicsUploadTransactionTransmission(requestContext) + } else if (requestContext.downloadTransaction != null) { throw NotImplementedError() } else { throw AssertionError() } } EbicsTypes.TransactionPhaseType.RECEIPT -> { - requestTransactionID ?: throw EbicsInvalidRequestError() - if (downloadTransaction == null) + val requestTransactionID = requestObject.header.static.transactionID ?: throw EbicsInvalidRequestError() + if (requestContext.downloadTransaction == null) throw EbicsInvalidRequestError() val receiptCode = requestObject.body.transferReceipt?.receiptCode ?: throw EbicsInvalidRequestError() EbicsResponse.createForDownloadReceiptPhase(requestTransactionID, receiptCode == 0) } } - signEbicsResponse(ebicsResponse, hostAuthPriv) + signEbicsResponse(ebicsResponse, requestContext.hostAuthPriv) } respondText(responseXmlStr, ContentType.Application.Xml, HttpStatusCode.OK) } diff --git a/sandbox/src/main/python/libeufin-cli b/sandbox/src/main/python/libeufin-cli @@ -19,7 +19,7 @@ def cli(): def admin(ctx): pass -@admin.command(help="Instruct the Bank to create a new EBICS host ID.") +@admin.command(help="Instruct the sandbox bank to create a new EBICS host ID.") @click.option( "--host-id", help="EBICS host ID", @@ -48,7 +48,7 @@ def add_host(obj, host_id, ebics_version, bank_base_url): print(resp.content.decode("utf-8")) -@admin.command(help="Instruct the Sandbox to create a new EBICS Subscriber") +@admin.command(help="Instruct the sandbox bank to create a new EBICS Subscriber") @click.pass_obj @click.option( "--user-id", @@ -210,7 +210,7 @@ def backup(obj, account_id, output_file, nexus_base_url): return if response.status_code != 200: - print("Unsuccessful status code gotten: {}".format(response.status_code)) + print("Received unsuccessful status code: {}".format(response.status_code)) return output = open(output_file, "w+") @@ -220,7 +220,7 @@ def backup(obj, account_id, output_file, nexus_base_url): print("Backup stored in {}".format(output_file)) -@ebics.command(help="Send TST message") +@ebics.command(help="Send test upload message (TSU)") @click.pass_obj @click.option( "--account-id", @@ -230,9 +230,138 @@ def backup(obj, account_id, output_file, nexus_base_url): @click.argument( "nexus-base-url" ) -def tst(obj, account_id, nexus_base_url): +def tsu(obj, account_id, nexus_base_url): - url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendTst".format(account_id)) + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendTSU".format(account_id)) + try: + resp = post(url) + except Exception: + print("Could not reach the nexus") + return + + print(resp.content.decode("utf-8")) + + +@ebics.command(help="Send test download message (TSD)") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.argument( + "nexus-base-url" +) +def tsd(obj, account_id, nexus_base_url): + + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendTSD".format(account_id)) + try: + resp = post(url) + except Exception: + print("Could not reach the nexus") + return + + print(resp.content.decode("utf-8")) + + +@ebics.command(help="Send HAA message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.argument( + "nexus-base-url" +) +def haa(obj, account_id, nexus_base_url): + + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHAA".format(account_id)) + try: + resp = post(url) + except Exception: + print("Could not reach the bank") + return + + print(resp.content.decode("utf-8")) + +@ebics.command(help="Send HVZ message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.argument( + "nexus-base-url" +) +def hvz(obj, account_id, nexus_base_url): + + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHVZ".format(account_id)) + try: + resp = post(url) + except Exception: + print("Could not reach the bank") + return + + print(resp.content.decode("utf-8")) + + +@ebics.command(help="Send HVU message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.argument( + "nexus-base-url" +) +def hvu(obj, account_id, nexus_base_url): + + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHVU".format(account_id)) + try: + resp = post(url) + except Exception: + print("Could not reach the bank") + return + + print(resp.content.decode("utf-8")) + +@ebics.command(help="Send HPD message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.argument( + "nexus-base-url" +) +def hpd(obj, account_id, nexus_base_url): + + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHPD".format(account_id)) + try: + resp = post(url) + except Exception: + print("Could not reach the bank") + return + + print(resp.content.decode("utf-8")) + + +@ebics.command(help="Send HKD message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.argument( + "nexus-base-url" +) +def hkd(obj, account_id, nexus_base_url): + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHKD".format(account_id)) try: resp = post(url) except Exception: @@ -364,7 +493,7 @@ def htd(ctx, account_id, prepare, nexus_base_url): ctx.invoke(sync) url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHtd".format(account_id)) try: - resp = get(url) + resp = post(url) except Exception: print("Could not reach the bank") return diff --git a/util/src/main/kotlin/ebics_h004/EbicsRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -6,6 +6,7 @@ import tech.libeufin.util.LOGGER import tech.libeufin.util.XMLUtil import java.math.BigInteger import java.security.interfaces.RSAPublicKey +import java.util.* import javax.xml.bind.annotation.* import javax.xml.bind.annotation.adapters.CollapsedStringAdapter import javax.xml.bind.annotation.adapters.HexBinaryAdapter @@ -180,6 +181,10 @@ class EbicsRequest { XmlElement( name = "StandardOrderParams", type = StandardOrderParams::class + ), + XmlElement( + name = "GenericOrderParams", + type = GenericOrderParams::class ) ) var orderParams: OrderParams? = null @@ -255,6 +260,13 @@ class EbicsRequest { } @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["parameterList"]) + class GenericOrderParams : OrderParams() { + @get:XmlElement(type = EbicsTypes.Parameter::class) + var parameterList: List<EbicsTypes.Parameter> = LinkedList() + } + + @XmlAccessorType(XmlAccessType.NONE) @XmlType(name = "", propOrder = ["start", "end"]) class DateRange { @get:XmlElement(name = "Start") diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt @@ -5,14 +5,14 @@ import javax.xml.datatype.DatatypeFactory import javax.xml.datatype.XMLGregorianCalendar /* now */ -fun getGregorianDate(): XMLGregorianCalendar { +fun getGregorianCalendarNow(): XMLGregorianCalendar { val gregorianCalendar = GregorianCalendar() val datatypeFactory = DatatypeFactory.newInstance() return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) } /* explicit point in time */ -fun getGregorianDate(year: Int, month: Int, day: Int): XMLGregorianCalendar { +fun getGregorianCalendar(year: Int, month: Int, day: Int): XMLGregorianCalendar { val gregorianCalendar = GregorianCalendar(year, month, day) val datatypeFactory = DatatypeFactory.newInstance() return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) diff --git a/util/src/test/kotlin/SignatureDataTest.kt b/util/src/test/kotlin/SignatureDataTest.kt @@ -4,7 +4,7 @@ import tech.libeufin.util.CryptoUtil import tech.libeufin.util.XMLUtil import tech.libeufin.util.ebics_h004.EbicsRequest import tech.libeufin.util.ebics_h004.EbicsTypes -import tech.libeufin.util.getGregorianDate +import tech.libeufin.util.getGregorianCalendarNow import java.math.BigInteger class SignatureDataTest { @@ -22,7 +22,7 @@ class SignatureDataTest { static = EbicsRequest.StaticHeaderType().apply { hostID = "some host ID" nonce = "nonce".toByteArray() - timestamp = getGregorianDate() + timestamp = getGregorianCalendarNow() partnerID = "some partner ID" userID = "some user ID" orderDetails = EbicsRequest.OrderDetails().apply {