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:
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 {