libeufin

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

commit 785563f75054cf97a366dab740a8cfd42f68e542
parent 2152dd260b5e0e188ca806c67858fe5b3f304ec7
Author: Marcello Stanisci <stanisci.m@gmail.com>
Date:   Tue, 28 Jan 2020 15:57:53 +0100

New package: tech.libeufin.nexus

Diffstat:
Dnexus/src/main/kotlin/Containers.kt | 41-----------------------------------------
Dnexus/src/main/kotlin/DB.kt | 47-----------------------------------------------
Dnexus/src/main/kotlin/Helpers.kt | 305------------------------------------------------------------------------------
Dnexus/src/main/kotlin/JSON.kt | 63---------------------------------------------------------------
Dnexus/src/main/kotlin/Main.kt | 804-------------------------------------------------------------------------------
Anexus/src/main/kotlin/tech/libeufin/nexus/Containers.kt | 41+++++++++++++++++++++++++++++++++++++++++
Anexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 48++++++++++++++++++++++++++++++++++++++++++++++++
Anexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 324+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 846+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/test/kotlin/LetterFormatTest.kt | 2++
Mnexus/src/test/kotlin/SignatureDataTest.kt | 1+
Msandbox/build.gradle | 2--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 1-
14 files changed, 1323 insertions(+), 1263 deletions(-)

diff --git a/nexus/src/main/kotlin/Containers.kt b/nexus/src/main/kotlin/Containers.kt @@ -1,40 +0,0 @@ -package tech.libeufin.nexus - -import javax.crypto.SecretKey -import org.w3c.dom.Document -import java.security.PrivateKey -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import javax.xml.bind.JAXBElement - - -/** - * 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/DB.kt b/nexus/src/main/kotlin/DB.kt @@ -1,46 +0,0 @@ -package tech.libeufin.nexus - -import org.jetbrains.exposed.dao.* -import org.jetbrains.exposed.sql.* -import org.jetbrains.exposed.sql.transactions.TransactionManager -import org.jetbrains.exposed.sql.transactions.transaction - - -object EbicsSubscribersTable : IntIdTable() { - val ebicsURL = text("ebicsURL") - val hostID = text("hostID") - val partnerID = text("partnerID") - val userID = text("userID") - val systemID = text("systemID").nullable() - val signaturePrivateKey = blob("signaturePrivateKey") - val encryptionPrivateKey = blob("encryptionPrivateKey") - val authenticationPrivateKey = blob("authenticationPrivateKey") - val bankEncryptionPublicKey = blob("bankEncryptionPublicKey").nullable() - val bankAuthenticationPublicKey = blob("bankAuthenticationPublicKey").nullable() -} - -class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { - companion object : IntEntityClass<EbicsSubscriberEntity>(EbicsSubscribersTable) - - var ebicsURL by EbicsSubscribersTable.ebicsURL - var hostID by EbicsSubscribersTable.hostID - var partnerID by EbicsSubscribersTable.partnerID - var userID by EbicsSubscribersTable.userID - var systemID by EbicsSubscribersTable.systemID - var signaturePrivateKey by EbicsSubscribersTable.signaturePrivateKey - var encryptionPrivateKey by EbicsSubscribersTable.encryptionPrivateKey - var authenticationPrivateKey by EbicsSubscribersTable.authenticationPrivateKey - var bankEncryptionPublicKey by EbicsSubscribersTable.bankEncryptionPublicKey - var bankAuthenticationPublicKey by EbicsSubscribersTable.bankAuthenticationPublicKey -} - -fun dbCreateTables() { - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - - transaction { - addLogger(StdOutSqlLogger) - SchemaUtils.create( - EbicsSubscribersTable - ) - } -} -\ No newline at end of file diff --git a/nexus/src/main/kotlin/Helpers.kt b/nexus/src/main/kotlin/Helpers.kt @@ -1,304 +0,0 @@ -package tech.libeufin.nexus - -import io.ktor.client.HttpClient -import io.ktor.client.request.post -import io.ktor.http.HttpStatusCode -import tech.libeufin.util.* -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.ebics_s001.UserSignatureData -import java.math.BigInteger -import java.security.PrivateKey -import java.security.SecureRandom -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.util.* -import javax.xml.bind.JAXBElement -import javax.xml.datatype.DatatypeFactory -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 - ) -} - -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.authenticationPrivateKey.toByteArray()) - ) - -} - -/** - * Inserts spaces every 2 characters, and a newline after 8 pairs. - */ -fun chunkString(input: String): String { - - val ret = StringBuilder() - var columns = 0 - - for (i in input.indices) { - - if ((i + 1).rem(2) == 0) { - - if (columns == 7) { - ret.append(input[i] + "\n") - columns = 0 - continue - } - - ret.append(input[i] + " ") - columns++ - continue - } - ret.append(input[i]) - } - - return ret.toString() - -} - -fun expectId(param: String?): Int { - - try { - return param!!.toInt() - } catch (e: Exception) { - throw NotAnIdError(HttpStatusCode.BadRequest) - } -} - -fun signOrder( - orderBlob: ByteArray, - signKey: RSAPrivateCrtKey, - partnerId: String, - userId: String -): UserSignatureData { - - val ES_signature = CryptoUtil.signEbicsA006( - CryptoUtil.digestEbicsOrderA006(orderBlob), - signKey - ) - val userSignatureData = UserSignatureData().apply { - orderSignatureList = listOf( - UserSignatureData.OrderSignatureData().apply { - signatureVersion = "A006" - signatureValue = ES_signature - partnerID = partnerId - userID = userId - } - ) - } - - return userSignatureData -} - - -/** - * @return null when the bank could not be reached, otherwise returns the - * response already converted in JAXB. - */ -suspend inline fun HttpClient.postToBank(url: String, body: String): String { - - LOGGER.debug("Posting: $body") - - val response = try { - this.post<String>( - urlString = url, - block = { - this.body = body - } - ) - } catch (e: Exception) { - throw UnreachableBankError(HttpStatusCode.InternalServerError) - } - - return response -} - -/** - * DO verify the bank's signature - */ -suspend inline fun <reified T, reified S> HttpClient.postToBankSignedAndVerify( - url: String, - body: T, - pub: RSAPublicKey, - priv: RSAPrivateCrtKey -): JAXBElement<S> { - - val doc = XMLUtil.convertJaxbToDocument(body) - XMLUtil.signEbicsDocument(doc, priv) - - val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - LOGGER.debug("About to verify: ${response}") - - val responseDocument = try { - - XMLUtil.parseStringIntoDom(response) - } catch (e: Exception) { - - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } - - if (!XMLUtil.verifyEbicsDocument(responseDocument, pub)) { - - throw BadSignature(HttpStatusCode.NotAcceptable) - } - - try { - - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } -} - -suspend inline fun <reified T, reified S> HttpClient.postToBankSigned( - url: String, - body: T, - priv: PrivateKey -): JAXBElement<S> { - - val doc = XMLUtil.convertJaxbToDocument(body) - XMLUtil.signEbicsDocument(doc, priv) - - val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } -} - -/** - * do NOT verify the bank's signature - */ -suspend inline fun <reified T, reified S> HttpClient.postToBankUnsigned( - url: String, - body: T -): JAXBElement<S> { - - val response: String = this.postToBank(url, XMLUtil.convertJaxbToString(body)) - - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse(HttpStatusCode.BadRequest, response) - } -} - -/** - * @param size in bits - */ -fun getNonce(size: Int): ByteArray { - val sr = SecureRandom() - val ret = ByteArray(size / 8) - sr.nextBytes(ret) - return ret -} -\ No newline at end of file diff --git a/nexus/src/main/kotlin/JSON.kt b/nexus/src/main/kotlin/JSON.kt @@ -1,62 +0,0 @@ -package tech.libeufin.nexus - -import com.google.gson.annotations.JsonAdapter -import com.squareup.moshi.JsonClass -import org.joda.time.DateTime - - -data class EbicsBackupRequest( - val passphrase: String -) - -data class EbicsDateRange( - /** - * ISO 8601 calendar dates: YEAR-MONTH(01-12)-DAY(1-31) - */ - val start: String, - val end: String -) - -/** - * This object is used twice: as a response to the backup request, - * and as a request to the backup restore. Note: in the second case - * the client must provide the passphrase. - */ -data class EbicsKeysBackup( - val authBlob: ByteArray, - val encBlob: ByteArray, - val sigBlob: ByteArray, - val passphrase: String? = null -) - -/** - * This object is POSTed by clients _after_ having created - * a EBICS subscriber at the sandbox. - */ -@JsonClass(generateAdapter = true) // USED? -data class EbicsSubscriberInfoRequest( - val ebicsURL: String, - val hostID: String, - val partnerID: String, - val userID: String, - val systemID: String? = null -) - -/** - * Contain the ID that identifies the new user in the Nexus system. - */ -data class EbicsSubscriberInfoResponse( - val accountID: Int, - val ebicsURL: String, - val hostID: String, - val partnerID: String, - val userID: String, - val systemID: String? = null -) - -/** - * Admin call that tells all the subscribers managed by Nexus. - */ -data class EbicsSubscribersResponse( - val ebicsSubscribers: MutableList<EbicsSubscriberInfoResponse> = mutableListOf() -) -\ No newline at end of file diff --git a/nexus/src/main/kotlin/Main.kt b/nexus/src/main/kotlin/Main.kt @@ -1,804 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2019 Stanisci and Dold. - - * LibEuFin is free software; you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation; either version 3, or - * (at your option) any later version. - - * LibEuFin is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General - * Public License for more details. - - * You should have received a copy of the GNU Affero General Public - * License along with LibEuFin; see the file COPYING. If not, see - * <http://www.gnu.org/licenses/> - */ - -package tech.libeufin.nexus - -import com.ryanharter.ktor.moshi.moshi -import com.squareup.moshi.JsonDataException -import io.ktor.application.ApplicationCallPipeline -import io.ktor.application.call -import io.ktor.application.install -import io.ktor.client.* -import io.ktor.features.CallLogging -import io.ktor.features.ContentNegotiation -import io.ktor.features.StatusPages -import io.ktor.gson.gson -import io.ktor.http.ContentType -import io.ktor.http.HttpStatusCode -import io.ktor.request.receive -import io.ktor.request.uri -import io.ktor.response.respond -import io.ktor.response.respondText -import io.ktor.routing.* -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -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 -import tech.libeufin.util.ebics_h004.* -import tech.libeufin.util.* -import java.text.DateFormat -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 java.math.BigInteger -import java.text.SimpleDateFormat -import java.util.* -import java.util.zip.DeflaterInputStream -import javax.crypto.EncryptedPrivateKeyInfo - - -fun testData() { - - val pairA = CryptoUtil.generateRsaKeyPair(2048) - val pairB = CryptoUtil.generateRsaKeyPair(2048) - val pairC = CryptoUtil.generateRsaKeyPair(2048) - - transaction { - EbicsSubscriberEntity.new { - ebicsURL = "http://localhost:5000/ebicsweb" - userID = "USER1" - partnerID = "PARTNER1" - hostID = "host01" - - signaturePrivateKey = SerialBlob(pairA.private.encoded) - encryptionPrivateKey = SerialBlob(pairB.private.encoded) - authenticationPrivateKey = SerialBlob(pairC.private.encoded) - } - } -} - -data class NotAnIdError(val statusCode: HttpStatusCode) : Exception("String ID not convertible in number") -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 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") - -fun main() { - dbCreateTables() - testData() // gets always id == 1 - 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 - - } - - install(ContentNegotiation) { - moshi { - } - gson { - setDateFormat(DateFormat.LONG) - setPrettyPrinting() - } - } - - install(StatusPages) { - exception<Throwable> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Internal server error.\n", ContentType.Text.Plain, HttpStatusCode.InternalServerError) - } - - exception<JsonDataException> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) - } - - exception<NotAnIdError> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) - } - - 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) - } - - - 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) - } - - 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) - } - - exception<SubscriberNotFoundError> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Subscriber not found\n", ContentType.Text.Plain, HttpStatusCode.NotFound) - } - - exception<BadSignature> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - 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) - } - - 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) - } - - exception<javax.xml.bind.UnmarshalException> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText( - "Could not convert string into JAXB (either from client or from bank)\n", - ContentType.Text.Plain, - HttpStatusCode.NotFound - ) - } - } - - 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") - return@get - } - - 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, startDate.dayOfMonth), - getGregorianDate(endDate.year, endDate.monthOfYear, endDate.dayOfMonth) - ), - subscriberData.customerAuthPriv - ) - - val payload: ByteArray = decryptAndDecompressResponse(response.value, subscriberData.customerEncPriv) - - call.respondText( - payload.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK) - - return@post - } - - post("/ebics/subscribers/{id}/restore-backup") { - // Creates a *new* customer with nexus-internal identifier "id" - // and imports the backup into it. - // This endpoint *fails* if a subscriber with the same nexus-internal id - // already exists. - } - - get("/ebics/subscribers/{id}/sendHtd") { - 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 - ) - - logger.debug("HTD response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) - - if (response.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } - - 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) - ) - - val dataCompr = CryptoUtil.decryptEbicsE002( - er, - subscriberData.customerEncPriv - ) - 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 - ) - - val ackResponse = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( - subscriberData.ebicsUrl, - ackRequest, - subscriberData.bankAuthPub ?: throw BankKeyMissing(HttpStatusCode.PreconditionFailed), - subscriberData.customerAuthPriv - ) - - logger.debug("HTD final response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) - - if (ackResponse.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } - - call.respondText( - "Success! Details (temporarily) reported on the Nexus console.", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - - get("/ebics/subscribers/{id}/keyletter") { - - val id = expectId(call.parameters["id"]) - - var usernameLine = "TODO" - var recipientLine = "TODO" - val customerIdLine = "TODO" - - var userIdLine = "" - var esExponentLine = "" - var esModulusLine = "" - var authExponentLine = "" - var authModulusLine = "" - var encExponentLine = "" - var encModulusLine = "" - var esKeyHashLine = "" - var encKeyHashLine = "" - var authKeyHashLine = "" - - val esVersionLine = "A006" - val authVersionLine = "X002" - val encVersionLine = "E002" - - val now = Date() - val dateFormat = SimpleDateFormat("DD.MM.YYYY") - val timeFormat = SimpleDateFormat("HH.mm.ss") - var dateLine = dateFormat.format(now) - var timeLine = timeFormat.format(now) - - - - - - transaction { - val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - - val signPubTmp = CryptoUtil.getRsaPublicFromPrivate( - CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()) - ) - val authPubTmp = CryptoUtil.getRsaPublicFromPrivate( - CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()) - ) - val encPubTmp = CryptoUtil.getRsaPublicFromPrivate( - CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray()) - ) - - userIdLine = subscriber.userID - - esExponentLine = signPubTmp.publicExponent.toByteArray().toHexString() - esModulusLine = signPubTmp.modulus.toByteArray().toHexString() - - encExponentLine = encPubTmp.publicExponent.toByteArray().toHexString() - encModulusLine = encPubTmp.modulus.toByteArray().toHexString() - - authExponentLine = authPubTmp.publicExponent.toByteArray().toHexString() - authModulusLine = authPubTmp.modulus.toByteArray().toHexString() - - esKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(signPubTmp).toHexString() - encKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(encPubTmp).toHexString() - authKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(authPubTmp).toHexString() - } - - val iniLetter = """ - |Name: ${usernameLine} - |Date: ${dateLine} - |Time: ${timeLine} - |Recipient: ${recipientLine} - |User ID: ${userIdLine} - |Customer ID: ${customerIdLine} - |ES version: ${esVersionLine} - - |Public key for the electronic signature: - - |Exponent: - |${chunkString(esExponentLine)} - - |Modulus: - |${chunkString(esModulusLine)} - - |SHA-256 hash: - |${chunkString(esKeyHashLine)} - - |I hereby confirm the above public keys for my electronic signature. - - |__________ - |Place/date - - |__________ - |Signature - """.trimMargin() - - val hiaLetter = """ - |Name: ${usernameLine} - |Date: ${dateLine} - |Time: ${timeLine} - |Recipient: ${recipientLine} - |User ID: ${userIdLine} - |Customer ID: ${customerIdLine} - |Identification and authentication signature version: ${authVersionLine} - |Encryption version: ${encVersionLine} - - |Public key for the identification and authentication signature: - - |Exponent: - |${chunkString(authExponentLine)} - - |Modulus: - |${chunkString(authModulusLine)} - - |SHA-256 hash: - |${chunkString(authKeyHashLine)} - - |Public encryption key: - - |Exponent: - |${chunkString(encExponentLine)} - - |Modulus: - |${chunkString(encModulusLine)} - - |SHA-256 hash: - |${chunkString(encKeyHashLine)} - - - |I hereby confirm the above public keys for my electronic signature. - - |__________ - |Place/date - - |__________ - |Signature - """.trimMargin() - - call.respondText( - "####INI####:\n${iniLetter}\n\n\n####HIA####:\n${hiaLetter}", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - - - get("/ebics/subscribers") { - - var ret = EbicsSubscribersResponse() - transaction { - EbicsSubscriberEntity.all().forEach { - ret.ebicsSubscribers.add( - EbicsSubscriberInfoResponse( - accountID = it.id.value, - hostID = it.hostID, - partnerID = it.partnerID, - systemID = it.systemID, - ebicsURL = it.ebicsURL, - userID = it.userID - ) - ) - } - } - call.respond(ret) - return@get - } - - get("/ebics/subscribers/{id}") { - val id = expectId(call.parameters["id"]) - val response = transaction { - val tmp = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - EbicsSubscriberInfoResponse( - accountID = tmp.id.value, - hostID = tmp.hostID, - partnerID = tmp.partnerID, - systemID = tmp.systemID, - ebicsURL = tmp.ebicsURL, - userID = tmp.userID - ) - } - call.respond(HttpStatusCode.OK, response) - return@get - } - - post("/ebics/subscribers") { - - val body = call.receive<EbicsSubscriberInfoRequest>() - - val pairA = CryptoUtil.generateRsaKeyPair(2048) - val pairB = CryptoUtil.generateRsaKeyPair(2048) - val pairC = CryptoUtil.generateRsaKeyPair(2048) - - val id = transaction { - - EbicsSubscriberEntity.new { - ebicsURL = body.ebicsURL - hostID = body.hostID - partnerID = body.partnerID - userID = body.userID - systemID = body.systemID - signaturePrivateKey = SerialBlob(pairA.private.encoded) - encryptionPrivateKey = SerialBlob(pairB.private.encoded) - authenticationPrivateKey = SerialBlob(pairC.private.encoded) - - }.id.value - } - - call.respondText( - "Subscriber registered, ID: ${id}", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - return@post - } - - - post("/ebics/subscribers/{id}/sendIni") { - - val id = expectId(call.parameters["id"]) // caught above - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - ) - } - - val iniRequest = EbicsUnsecuredRequest.createIni( - subscriberData.hostId, - subscriberData.userId, - subscriberData.partnerId, - subscriberData.customerSignPriv - ) - - val responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( - subscriberData.ebicsUrl, - iniRequest - ) - - if (responseJaxb.value.body.returnCode.value != "000000") { - throw EbicsError(responseJaxb.value.body.returnCode.value) - } - - 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"]) - - val (authKey, encKey, sigKey) = try { - - Triple( - - CryptoUtil.decryptKey( - EncryptedPrivateKeyInfo(body.authBlob), body.passphrase!! - ), - - CryptoUtil.decryptKey( - EncryptedPrivateKeyInfo(body.encBlob), body.passphrase - ), - - CryptoUtil.decryptKey( - EncryptedPrivateKeyInfo(body.sigBlob), body.passphrase - ) - ) - - } catch (e: Exception) { - e.printStackTrace() - throw BadBackup(HttpStatusCode.BadRequest) - } - - transaction { - val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - - subscriber.encryptionPrivateKey = SerialBlob(encKey.encoded) - subscriber.authenticationPrivateKey = SerialBlob(authKey.encoded) - subscriber.signaturePrivateKey = SerialBlob(sigKey.encoded) - } - - call.respondText( - "Keys successfully restored", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - - } - - post("/ebics/subscribers/{id}/backup") { - - val id = expectId(call.parameters["id"]) - val body = call.receive<EbicsBackupRequest>() - - val content = transaction { - val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - - - EbicsKeysBackup( - - authBlob = CryptoUtil.encryptKey( - subscriber.authenticationPrivateKey.toByteArray(), - body.passphrase - ), - - encBlob = CryptoUtil.encryptKey( - subscriber.encryptionPrivateKey.toByteArray(), - body.passphrase), - - sigBlob = CryptoUtil.encryptKey( - subscriber.signaturePrivateKey.toByteArray(), - body.passphrase - ) - ) - } - - call.response.headers.append("Content-Disposition", "attachment") - call.respond( - HttpStatusCode.OK, - content - ) - - } - post("/ebics/subscribers/{id}/sendTst") { - - val id = expectId(call.parameters["id"]) - - val subscriberData = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - ) - } - 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.customerEncPriv - ) - - 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) - } - - call.respondText( - "TST INITIALIZATION & TRANSACTION phases succeeded\n", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - } - - post("/ebics/subscribers/{id}/sync") { - val id = expectId(call.parameters["id"]) - val bundle = transaction { - containerInit( - EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - ) - } - val response = client.postToBankSigned<EbicsNpkdRequest, EbicsKeyManagementResponse>( - bundle.ebicsUrl, - EbicsNpkdRequest.createRequest( - bundle.hostId, - bundle.partnerId, - bundle.userId, - getNonce(128), - getGregorianDate() - ), - bundle.customerAuthPriv - ) - - if (response.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } - - val er = CryptoUtil.EncryptionResult( - response.value.body.dataTransfer!!.dataEncryptionInfo!!.transactionKey, - (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) - .encryptionPubKeyDigest.value, - response.value.body.dataTransfer!!.orderData.value - ) - - val dataCompr = CryptoUtil.decryptEbicsE002( - er, - bundle.customerEncPriv - ) - val data = EbicsOrderUtil.decodeOrderDataXml<HPBResponseOrderData>(dataCompr) - - // put bank's keys into database. - transaction { - val subscriber = EbicsSubscriberEntity.findById(id) - - subscriber!!.bankAuthenticationPublicKey = SerialBlob( - - CryptoUtil.loadRsaPublicKeyFromComponents( - data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ).encoded - ) - - subscriber.bankEncryptionPublicKey = SerialBlob( - CryptoUtil.loadRsaPublicKeyFromComponents( - data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ).encoded - ) - } - - 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 responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( - subscriberData.ebicsUrl, - EbicsUnsecuredRequest.createHia( - subscriberData.hostId, - subscriberData.userId, - subscriberData.partnerId, - subscriberData.customerAuthPriv, - subscriberData.customerEncPriv - ) - ) - - if (responseJaxb.value.body.returnCode.value != "000000") { - throw EbicsError(responseJaxb.value.body.returnCode.value) - } - - call.respondText( - "Bank accepted authentication and encryption keys\n", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - - return@post - } - } - } - - logger.info("Up and running") - server.start(wait = true) -} diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Containers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Containers.kt @@ -0,0 +1,40 @@ +package tech.libeufin.nexus.tech.libeufin.nexus + +import javax.crypto.SecretKey +import org.w3c.dom.Document +import java.security.PrivateKey +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import javax.xml.bind.JAXBElement + + +/** + * 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 @@ -0,0 +1,47 @@ +package tech.libeufin.nexus.tech.libeufin.nexus + +import org.jetbrains.exposed.dao.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.transactions.transaction + + +object EbicsSubscribersTable : IntIdTable() { + val ebicsURL = text("ebicsURL") + val hostID = text("hostID") + val partnerID = text("partnerID") + val userID = text("userID") + val systemID = text("systemID").nullable() + val signaturePrivateKey = blob("signaturePrivateKey") + val encryptionPrivateKey = blob("encryptionPrivateKey") + val authenticationPrivateKey = blob("authenticationPrivateKey") + val bankEncryptionPublicKey = blob("bankEncryptionPublicKey").nullable() + val bankAuthenticationPublicKey = blob("bankAuthenticationPublicKey").nullable() +} + +class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { + companion object : IntEntityClass<EbicsSubscriberEntity>( + EbicsSubscribersTable + ) + + var ebicsURL by EbicsSubscribersTable.ebicsURL + var hostID by EbicsSubscribersTable.hostID + var partnerID by EbicsSubscribersTable.partnerID + var userID by EbicsSubscribersTable.userID + var systemID by EbicsSubscribersTable.systemID + var signaturePrivateKey by EbicsSubscribersTable.signaturePrivateKey + var encryptionPrivateKey by EbicsSubscribersTable.encryptionPrivateKey + var authenticationPrivateKey by EbicsSubscribersTable.authenticationPrivateKey + var bankEncryptionPublicKey by EbicsSubscribersTable.bankEncryptionPublicKey + var bankAuthenticationPublicKey by EbicsSubscribersTable.bankAuthenticationPublicKey +} + +fun dbCreateTables() { + Database.connect("jdbc:sqlite:libeufin-nexus.sqlite3", "org.sqlite.JDBC") + + transaction { + addLogger(StdOutSqlLogger) + SchemaUtils.create( + EbicsSubscribersTable + ) + } +} +\ 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 @@ -0,0 +1,323 @@ +package tech.libeufin.nexus.tech.libeufin.nexus + +import io.ktor.client.HttpClient +import io.ktor.client.request.post +import io.ktor.http.HttpStatusCode +import tech.libeufin.util.* +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.ebics_s001.UserSignatureData +import java.math.BigInteger +import java.security.PrivateKey +import java.security.SecureRandom +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +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 + ) +} + +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.authenticationPrivateKey.toByteArray()) + ) + +} + +/** + * Inserts spaces every 2 characters, and a newline after 8 pairs. + */ +fun chunkString(input: String): String { + + val ret = StringBuilder() + var columns = 0 + + for (i in input.indices) { + + if ((i + 1).rem(2) == 0) { + + if (columns == 7) { + ret.append(input[i] + "\n") + columns = 0 + continue + } + + ret.append(input[i] + " ") + columns++ + continue + } + ret.append(input[i]) + } + + return ret.toString() + +} + +fun expectId(param: String?): Int { + + try { + return param!!.toInt() + } catch (e: Exception) { + throw NotAnIdError(HttpStatusCode.BadRequest) + } +} + +fun signOrder( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): UserSignatureData { + + val ES_signature = CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ) + val userSignatureData = UserSignatureData().apply { + orderSignatureList = listOf( + UserSignatureData.OrderSignatureData().apply { + signatureVersion = "A006" + signatureValue = ES_signature + partnerID = partnerId + userID = userId + } + ) + } + + return userSignatureData +} + + +/** + * @return null when the bank could not be reached, otherwise returns the + * response already converted in JAXB. + */ +suspend inline fun HttpClient.postToBank(url: String, body: String): String { + + LOGGER.debug("Posting: $body") + + val response = try { + this.post<String>( + urlString = url, + block = { + this.body = body + } + ) + } catch (e: Exception) { + throw UnreachableBankError(HttpStatusCode.InternalServerError) + } + + return response +} + +/** + * DO verify the bank's signature + */ +suspend inline fun <reified T, reified S> HttpClient.postToBankSignedAndVerify( + url: String, + body: T, + pub: RSAPublicKey, + priv: RSAPrivateCrtKey +): JAXBElement<S> { + + val doc = XMLUtil.convertJaxbToDocument(body) + XMLUtil.signEbicsDocument(doc, priv) + + val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) + LOGGER.debug("About to verify: ${response}") + + val responseDocument = try { + + XMLUtil.parseStringIntoDom(response) + } catch (e: Exception) { + + throw UnparsableResponse( + HttpStatusCode.BadRequest, + response + ) + } + + if (!XMLUtil.verifyEbicsDocument(responseDocument, pub)) { + + throw BadSignature(HttpStatusCode.NotAcceptable) + } + + try { + + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + + throw UnparsableResponse( + HttpStatusCode.BadRequest, + response + ) + } +} + +suspend inline fun <reified T, reified S> HttpClient.postToBankSigned( + url: String, + body: T, + priv: PrivateKey +): JAXBElement<S> { + + val doc = XMLUtil.convertJaxbToDocument(body) + XMLUtil.signEbicsDocument(doc, priv) + + val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) + + try { + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw UnparsableResponse( + HttpStatusCode.BadRequest, + response + ) + } +} + +/** + * do NOT verify the bank's signature + */ +suspend inline fun <reified T, reified S> HttpClient.postToBankUnsigned( + url: String, + body: T +): JAXBElement<S> { + + val response: String = this.postToBank(url, XMLUtil.convertJaxbToString(body)) + + try { + return XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw UnparsableResponse( + HttpStatusCode.BadRequest, + response + ) + } +} + +/** + * @param size in bits + */ +fun getNonce(size: Int): ByteArray { + val sr = SecureRandom() + val ret = ByteArray(size / 8) + sr.nextBytes(ret) + return ret +} +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -0,0 +1,60 @@ +package tech.libeufin.nexus.tech.libeufin.nexus + +import com.squareup.moshi.JsonClass + + +data class EbicsBackupRequest( + val passphrase: String +) + +data class EbicsDateRange( + /** + * ISO 8601 calendar dates: YEAR-MONTH(01-12)-DAY(1-31) + */ + val start: String, + val end: String +) + +/** + * This object is used twice: as a response to the backup request, + * and as a request to the backup restore. Note: in the second case + * the client must provide the passphrase. + */ +data class EbicsKeysBackup( + val authBlob: ByteArray, + val encBlob: ByteArray, + val sigBlob: ByteArray, + val passphrase: String? = null +) + +/** + * This object is POSTed by clients _after_ having created + * a EBICS subscriber at the sandbox. + */ +@JsonClass(generateAdapter = true) // USED? +data class EbicsSubscriberInfoRequest( + val ebicsURL: String, + val hostID: String, + val partnerID: String, + val userID: String, + val systemID: String? = null +) + +/** + * Contain the ID that identifies the new user in the Nexus system. + */ +data class EbicsSubscriberInfoResponse( + val accountID: Int, + val ebicsURL: String, + val hostID: String, + val partnerID: String, + val userID: String, + val systemID: String? = null +) + +/** + * Admin call that tells all the subscribers managed by Nexus. + */ +data class EbicsSubscribersResponse( + val ebicsSubscribers: MutableList<EbicsSubscriberInfoResponse> = mutableListOf() +) +\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -0,0 +1,846 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.nexus.tech.libeufin.nexus + +import com.ryanharter.ktor.moshi.moshi +import com.squareup.moshi.JsonDataException +import io.ktor.application.ApplicationCallPipeline +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.client.* +import io.ktor.features.CallLogging +import io.ktor.features.ContentNegotiation +import io.ktor.features.StatusPages +import io.ktor.gson.gson +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.request.receive +import io.ktor.request.uri +import io.ktor.response.respond +import io.ktor.response.respondText +import io.ktor.routing.* +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +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 +import tech.libeufin.util.ebics_h004.* +import tech.libeufin.util.* +import java.text.DateFormat +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 java.math.BigInteger +import java.text.SimpleDateFormat +import java.util.* +import java.util.zip.DeflaterInputStream +import javax.crypto.EncryptedPrivateKeyInfo + + +fun testData() { + + val pairA = CryptoUtil.generateRsaKeyPair(2048) + val pairB = CryptoUtil.generateRsaKeyPair(2048) + val pairC = CryptoUtil.generateRsaKeyPair(2048) + + transaction { + EbicsSubscriberEntity.new { + ebicsURL = "http://localhost:5000/ebicsweb" + userID = "USER1" + partnerID = "PARTNER1" + hostID = "host01" + + signaturePrivateKey = SerialBlob(pairA.private.encoded) + encryptionPrivateKey = SerialBlob(pairB.private.encoded) + authenticationPrivateKey = SerialBlob(pairC.private.encoded) + } + } +} + +data class NotAnIdError(val statusCode: HttpStatusCode) : Exception("String ID not convertible in number") +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 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") + +fun main() { + dbCreateTables() + testData() // gets always id == 1 + 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 + + } + + install(ContentNegotiation) { + moshi { + } + gson { + setDateFormat(DateFormat.LONG) + setPrettyPrinting() + } + } + + install(StatusPages) { + exception<Throwable> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText("Internal server error.\n", ContentType.Text.Plain, HttpStatusCode.InternalServerError) + } + + exception<JsonDataException> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) + } + + exception<NotAnIdError> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) + } + + 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) + } + + + 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) + } + + 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) + } + + exception<SubscriberNotFoundError> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText("Subscriber not found\n", ContentType.Text.Plain, HttpStatusCode.NotFound) + } + + exception<BadSignature> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + 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) + } + + 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) + } + + exception<javax.xml.bind.UnmarshalException> { cause -> + logger.error("Exception while handling '${call.request.uri}'", cause) + call.respondText( + "Could not convert string into JAXB (either from client or from bank)\n", + ContentType.Text.Plain, + HttpStatusCode.NotFound + ) + } + } + + 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") + return@get + } + + 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, startDate.dayOfMonth), + getGregorianDate(endDate.year, endDate.monthOfYear, endDate.dayOfMonth) + ), + subscriberData.customerAuthPriv + ) + + val payload: ByteArray = + decryptAndDecompressResponse( + response.value, + subscriberData.customerEncPriv + ) + + call.respondText( + payload.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK) + + return@post + } + + post("/ebics/subscribers/{id}/restore-backup") { + // Creates a *new* customer with nexus-internal identifier "id" + // and imports the backup into it. + // This endpoint *fails* if a subscriber with the same nexus-internal id + // already exists. + } + + get("/ebics/subscribers/{id}/sendHtd") { + 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 + ) + + logger.debug("HTD response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) + + if (response.value.body.returnCode.value != "000000") { + throw EbicsError(response.value.body.returnCode.value) + } + + 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) + ) + + val dataCompr = CryptoUtil.decryptEbicsE002( + er, + subscriberData.customerEncPriv + ) + 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 + ) + + val ackResponse = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( + subscriberData.ebicsUrl, + ackRequest, + subscriberData.bankAuthPub ?: throw BankKeyMissing( + HttpStatusCode.PreconditionFailed + ), + subscriberData.customerAuthPriv + ) + + logger.debug("HTD final response: " + XMLUtil.convertJaxbToString<EbicsResponse>(response.value)) + + if (ackResponse.value.body.returnCode.value != "000000") { + throw EbicsError(response.value.body.returnCode.value) + } + + call.respondText( + "Success! Details (temporarily) reported on the Nexus console.", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + + get("/ebics/subscribers/{id}/keyletter") { + + val id = expectId(call.parameters["id"]) + + var usernameLine = "TODO" + var recipientLine = "TODO" + val customerIdLine = "TODO" + + var userIdLine = "" + var esExponentLine = "" + var esModulusLine = "" + var authExponentLine = "" + var authModulusLine = "" + var encExponentLine = "" + var encModulusLine = "" + var esKeyHashLine = "" + var encKeyHashLine = "" + var authKeyHashLine = "" + + val esVersionLine = "A006" + val authVersionLine = "X002" + val encVersionLine = "E002" + + val now = Date() + val dateFormat = SimpleDateFormat("DD.MM.YYYY") + val timeFormat = SimpleDateFormat("HH.mm.ss") + var dateLine = dateFormat.format(now) + var timeLine = timeFormat.format(now) + + + + + + transaction { + val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + + val signPubTmp = CryptoUtil.getRsaPublicFromPrivate( + CryptoUtil.loadRsaPrivateKey(subscriber.signaturePrivateKey.toByteArray()) + ) + val authPubTmp = CryptoUtil.getRsaPublicFromPrivate( + CryptoUtil.loadRsaPrivateKey(subscriber.authenticationPrivateKey.toByteArray()) + ) + val encPubTmp = CryptoUtil.getRsaPublicFromPrivate( + CryptoUtil.loadRsaPrivateKey(subscriber.encryptionPrivateKey.toByteArray()) + ) + + userIdLine = subscriber.userID + + esExponentLine = signPubTmp.publicExponent.toByteArray().toHexString() + esModulusLine = signPubTmp.modulus.toByteArray().toHexString() + + encExponentLine = encPubTmp.publicExponent.toByteArray().toHexString() + encModulusLine = encPubTmp.modulus.toByteArray().toHexString() + + authExponentLine = authPubTmp.publicExponent.toByteArray().toHexString() + authModulusLine = authPubTmp.modulus.toByteArray().toHexString() + + esKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(signPubTmp).toHexString() + encKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(encPubTmp).toHexString() + authKeyHashLine = CryptoUtil.getEbicsPublicKeyHash(authPubTmp).toHexString() + } + + val iniLetter = """ + |Name: ${usernameLine} + |Date: ${dateLine} + |Time: ${timeLine} + |Recipient: ${recipientLine} + |User ID: ${userIdLine} + |Customer ID: ${customerIdLine} + |ES version: ${esVersionLine} + + |Public key for the electronic signature: + + |Exponent: + |${chunkString(esExponentLine)} + + |Modulus: + |${chunkString(esModulusLine)} + + |SHA-256 hash: + |${chunkString(esKeyHashLine)} + + |I hereby confirm the above public keys for my electronic signature. + + |__________ + |Place/date + + |__________ + |Signature + """.trimMargin() + + val hiaLetter = """ + |Name: ${usernameLine} + |Date: ${dateLine} + |Time: ${timeLine} + |Recipient: ${recipientLine} + |User ID: ${userIdLine} + |Customer ID: ${customerIdLine} + |Identification and authentication signature version: ${authVersionLine} + |Encryption version: ${encVersionLine} + + |Public key for the identification and authentication signature: + + |Exponent: + |${chunkString(authExponentLine)} + + |Modulus: + |${chunkString(authModulusLine)} + + |SHA-256 hash: + |${chunkString(authKeyHashLine)} + + |Public encryption key: + + |Exponent: + |${chunkString(encExponentLine)} + + |Modulus: + |${chunkString(encModulusLine)} + + |SHA-256 hash: + |${chunkString(encKeyHashLine)} + + + |I hereby confirm the above public keys for my electronic signature. + + |__________ + |Place/date + + |__________ + |Signature + """.trimMargin() + + call.respondText( + "####INI####:\n${iniLetter}\n\n\n####HIA####:\n${hiaLetter}", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + + + get("/ebics/subscribers") { + + var ret = EbicsSubscribersResponse() + transaction { + EbicsSubscriberEntity.all().forEach { + ret.ebicsSubscribers.add( + EbicsSubscriberInfoResponse( + accountID = it.id.value, + hostID = it.hostID, + partnerID = it.partnerID, + systemID = it.systemID, + ebicsURL = it.ebicsURL, + userID = it.userID + ) + ) + } + } + call.respond(ret) + return@get + } + + get("/ebics/subscribers/{id}") { + val id = expectId(call.parameters["id"]) + val response = transaction { + val tmp = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + EbicsSubscriberInfoResponse( + accountID = tmp.id.value, + hostID = tmp.hostID, + partnerID = tmp.partnerID, + systemID = tmp.systemID, + ebicsURL = tmp.ebicsURL, + userID = tmp.userID + ) + } + call.respond(HttpStatusCode.OK, response) + return@get + } + + post("/ebics/subscribers") { + + val body = call.receive<EbicsSubscriberInfoRequest>() + + val pairA = CryptoUtil.generateRsaKeyPair(2048) + val pairB = CryptoUtil.generateRsaKeyPair(2048) + val pairC = CryptoUtil.generateRsaKeyPair(2048) + + val id = transaction { + + EbicsSubscriberEntity.new { + ebicsURL = body.ebicsURL + hostID = body.hostID + partnerID = body.partnerID + userID = body.userID + systemID = body.systemID + signaturePrivateKey = SerialBlob(pairA.private.encoded) + encryptionPrivateKey = SerialBlob(pairB.private.encoded) + authenticationPrivateKey = SerialBlob(pairC.private.encoded) + + }.id.value + } + + call.respondText( + "Subscriber registered, ID: ${id}", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + return@post + } + + + post("/ebics/subscribers/{id}/sendIni") { + + val id = + expectId(call.parameters["id"]) // caught above + val subscriberData = transaction { + containerInit( + EbicsSubscriberEntity.findById(id) + ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + ) + } + + val iniRequest = EbicsUnsecuredRequest.createIni( + subscriberData.hostId, + subscriberData.userId, + subscriberData.partnerId, + subscriberData.customerSignPriv + ) + + val responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( + subscriberData.ebicsUrl, + iniRequest + ) + + if (responseJaxb.value.body.returnCode.value != "000000") { + throw EbicsError(responseJaxb.value.body.returnCode.value) + } + + 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"]) + + val (authKey, encKey, sigKey) = try { + + Triple( + + CryptoUtil.decryptKey( + EncryptedPrivateKeyInfo(body.authBlob), body.passphrase!! + ), + + CryptoUtil.decryptKey( + EncryptedPrivateKeyInfo(body.encBlob), body.passphrase + ), + + CryptoUtil.decryptKey( + EncryptedPrivateKeyInfo(body.sigBlob), body.passphrase + ) + ) + + } catch (e: Exception) { + e.printStackTrace() + throw BadBackup(HttpStatusCode.BadRequest) + } + + transaction { + val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + + subscriber.encryptionPrivateKey = SerialBlob(encKey.encoded) + subscriber.authenticationPrivateKey = SerialBlob(authKey.encoded) + subscriber.signaturePrivateKey = SerialBlob(sigKey.encoded) + } + + call.respondText( + "Keys successfully restored", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + + } + + post("/ebics/subscribers/{id}/backup") { + + val id = expectId(call.parameters["id"]) + val body = call.receive<EbicsBackupRequest>() + + val content = transaction { + val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + + + EbicsKeysBackup( + + authBlob = CryptoUtil.encryptKey( + subscriber.authenticationPrivateKey.toByteArray(), + body.passphrase + ), + + encBlob = CryptoUtil.encryptKey( + subscriber.encryptionPrivateKey.toByteArray(), + body.passphrase + ), + + sigBlob = CryptoUtil.encryptKey( + subscriber.signaturePrivateKey.toByteArray(), + body.passphrase + ) + ) + } + + call.response.headers.append("Content-Disposition", "attachment") + call.respond( + HttpStatusCode.OK, + content + ) + + } + post("/ebics/subscribers/{id}/sendTst") { + + val id = expectId(call.parameters["id"]) + + val subscriberData = transaction { + containerInit( + EbicsSubscriberEntity.findById(id) + ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + ) + } + 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.customerEncPriv + ) + + 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) + } + + call.respondText( + "TST INITIALIZATION & TRANSACTION phases succeeded\n", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + + post("/ebics/subscribers/{id}/sync") { + val id = expectId(call.parameters["id"]) + val bundle = transaction { + containerInit( + EbicsSubscriberEntity.findById(id) + ?: throw SubscriberNotFoundError( + HttpStatusCode.NotFound + ) + ) + } + val response = client.postToBankSigned<EbicsNpkdRequest, EbicsKeyManagementResponse>( + bundle.ebicsUrl, + EbicsNpkdRequest.createRequest( + bundle.hostId, + bundle.partnerId, + bundle.userId, + getNonce(128), + getGregorianDate() + ), + bundle.customerAuthPriv + ) + + if (response.value.body.returnCode.value != "000000") { + throw EbicsError(response.value.body.returnCode.value) + } + + val er = CryptoUtil.EncryptionResult( + response.value.body.dataTransfer!!.dataEncryptionInfo!!.transactionKey, + (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) + .encryptionPubKeyDigest.value, + response.value.body.dataTransfer!!.orderData.value + ) + + val dataCompr = CryptoUtil.decryptEbicsE002( + er, + bundle.customerEncPriv + ) + val data = EbicsOrderUtil.decodeOrderDataXml<HPBResponseOrderData>(dataCompr) + + // put bank's keys into database. + transaction { + val subscriber = EbicsSubscriberEntity.findById(id) + + subscriber!!.bankAuthenticationPublicKey = SerialBlob( + + CryptoUtil.loadRsaPublicKeyFromComponents( + data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ).encoded + ) + + subscriber.bankEncryptionPublicKey = SerialBlob( + CryptoUtil.loadRsaPublicKeyFromComponents( + data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ).encoded + ) + } + + 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 responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( + subscriberData.ebicsUrl, + EbicsUnsecuredRequest.createHia( + subscriberData.hostId, + subscriberData.userId, + subscriberData.partnerId, + subscriberData.customerAuthPriv, + subscriberData.customerEncPriv + ) + ) + + if (responseJaxb.value.body.returnCode.value != "000000") { + throw EbicsError(responseJaxb.value.body.returnCode.value) + } + + call.respondText( + "Bank accepted authentication and encryption keys\n", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + + return@post + } + } + } + + logger.info("Up and running") + server.start(wait = true) +} diff --git a/nexus/src/test/kotlin/LetterFormatTest.kt b/nexus/src/test/kotlin/LetterFormatTest.kt @@ -1,6 +1,8 @@ package tech.libeufin.nexus import org.junit.Test +import tech.libeufin.nexus.tech.libeufin.nexus.chunkString +import tech.libeufin.nexus.tech.libeufin.nexus.getNonce import tech.libeufin.util.toHexString class LetterFormatTest { diff --git a/nexus/src/test/kotlin/SignatureDataTest.kt b/nexus/src/test/kotlin/SignatureDataTest.kt @@ -3,6 +3,7 @@ package tech.libeufin.nexus import tech.libeufin.util.XMLUtil import org.apache.xml.security.binding.xmldsig.SignatureType import org.junit.Test +import tech.libeufin.nexus.tech.libeufin.nexus.getNonce import tech.libeufin.util.CryptoUtil import tech.libeufin.util.ebics_h004.EbicsRequest import tech.libeufin.util.ebics_h004.EbicsTypes diff --git a/sandbox/build.gradle b/sandbox/build.gradle @@ -4,12 +4,10 @@ plugins { id 'application' } - sourceCompatibility = "11" targetCompatibility = "11" version '1.0-snapshot' - compileKotlin { kotlinOptions { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -365,7 +365,6 @@ class EbicsUploadTransactionChunkEntity(id : EntityID<String>): Entity<String>(i fun dbCreateTables() { Database.connect("jdbc:sqlite:libeufin-sandbox.sqlite3", "org.sqlite.JDBC") - // Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") TransactionManager.manager.defaultIsolationLevel = Connection.TRANSACTION_SERIALIZABLE transaction {