summaryrefslogtreecommitdiff
path: root/nexus
diff options
context:
space:
mode:
authorMS <ms@taler.net>2021-05-03 11:15:20 +0200
committerMS <ms@taler.net>2021-05-03 11:15:20 +0200
commit688af0d49e77ca918c7f66c37bd047b4560bf779 (patch)
treedb98668e4f60be3dcfbbe09d673db7bb92d78f59 /nexus
parentb1cf569f0e3953a016b98dab2e231b2657d616d1 (diff)
downloadlibeufin-688af0d49e77ca918c7f66c37bd047b4560bf779.tar.gz
libeufin-688af0d49e77ca918c7f66c37bd047b4560bf779.tar.bz2
libeufin-688af0d49e77ca918c7f66c37bd047b4560bf779.zip
Continuing polymorphism
Diffstat (limited to 'nexus')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt36
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt34
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt749
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt70
4 files changed, 432 insertions, 457 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
index c54edc64..9cf5a435 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
@@ -19,9 +19,11 @@
package tech.libeufin.nexus
+import com.fasterxml.jackson.databind.JsonNode
import io.ktor.client.HttpClient
import io.ktor.http.HttpStatusCode
import tech.libeufin.nexus.ebics.*
+import tech.libeufin.nexus.server.FetchSpecJson
// 'const' allows only primitive types.
val bankConnectionRegistry: Map<String, BankConnectionProtocol> = mapOf(
@@ -29,7 +31,41 @@ val bankConnectionRegistry: Map<String, BankConnectionProtocol> = mapOf(
)
interface BankConnectionProtocol {
+ // Initialize the connection. Usually uploads keys to the bank.
suspend fun connect(client: HttpClient, connId: String)
+
+ // Downloads the list of bank accounts managed at the
+ // bank under one particular connection.
+ suspend fun fetchAccounts(client: HttpClient, connId: String)
+
+ // Create a new connection from backup data.
+ fun createConnectionFromBackup(connId: String, user: NexusUserEntity, passphrase: String?, backup: JsonNode)
+
+ // Create a new connection from a HTTP request.
+ fun createConnection(connId: String, user: NexusUserEntity, data: JsonNode)
+
+ // Merely a formatter of connection details coming from
+ // the database.
+ fun getConnectionDetails(conn: NexusBankConnectionEntity): JsonNode
+
+ // Returns the backup data.
+ fun exportBackup(bankConnectionId: String, passphrase: String): JsonNode
+
+ // Export a printable format of the connection details. Useful
+ // to provide authentication via the traditional mail system.
+ fun exportAnalogDetails(conn: NexusBankConnectionEntity): ByteArray
+
+ // Send to the bank a previously prepared payment instruction.
+ suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long)
+
+ // Downlaods transactions from the bank, according to the specification
+ // given in the arguments.
+ suspend fun fetchTransactions(
+ fetchSpec: FetchSpecJson,
+ client: HttpClient,
+ bankConnectionId: String,
+ accountId: String
+ )
}
fun getConnectionPlugin(connId: String): BankConnectionProtocol {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index 5b872ccb..ede581c3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -27,8 +27,6 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction
import org.w3c.dom.Document
import tech.libeufin.nexus.*
-import tech.libeufin.nexus.ebics.fetchEbicsBySpec
-import tech.libeufin.nexus.ebics.submitEbicsPaymentInitiation
import tech.libeufin.nexus.iso20022.CamtParsingError
import tech.libeufin.nexus.iso20022.CreditDebitIndicator
import tech.libeufin.nexus.iso20022.parseCamtMessage
@@ -52,7 +50,7 @@ fun requireBankAccount(call: ApplicationCall, parameterKey: String): NexusBankAc
return account
}
-
+// called twice: once from the background task, once from the ad-hoc endpoint.
suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) {
val r = transaction {
val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId)
@@ -67,10 +65,10 @@ suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId:
if (r.submitted) {
return
}
- when (r.type) {
- null -> throw NexusError(HttpStatusCode.NotFound, "no default bank connection")
- "ebics" -> submitEbicsPaymentInitiation(httpClient, paymentInitiationId)
- }
+ if (r.type == null)
+ throw NexusError(HttpStatusCode.NotFound, "no default bank connection")
+
+ getConnectionPlugin(r.type).submitPaymentInitiation(httpClient, paymentInitiationId)
}
/**
@@ -308,20 +306,14 @@ suspend fun fetchBankAccountTransactions(client: HttpClient, fetchSpec: FetchSpe
val connectionName = conn.connectionId
}
}
- when (res.connectionType) {
- "ebics" -> {
- fetchEbicsBySpec(
- fetchSpec,
- client,
- res.connectionName,
- accountId
- )
- }
- else -> throw NexusError(
- HttpStatusCode.BadRequest,
- "Connection type '${res.connectionType}' not implemented"
- )
- }
+
+ getConnectionPlugin(res.connectionType).fetchTransactions(
+ fetchSpec,
+ client,
+ res.connectionName,
+ accountId
+ )
+
val newTransactions = ingestBankMessagesIntoAccount(res.connectionName, accountId)
ingestTalerTransactions(accountId)
return newTransactions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
index 2bd74ca8..18f58489 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -70,101 +70,7 @@ private data class EbicsFetchSpec(
val orderParams: EbicsOrderParams
)
-suspend fun fetchEbicsBySpec(
- fetchSpec: FetchSpecJson,
- client: HttpClient,
- bankConnectionId: String,
- accountId: String
-) {
- val subscriberDetails = transaction { getEbicsSubscriberDetails(bankConnectionId) }
- val lastTimes = transaction {
- val acct = NexusBankAccountEntity.findByName(accountId)
- if (acct == null) {
- throw NexusError(
- HttpStatusCode.NotFound,
- "Account not found"
- )
- }
- object {
- val lastStatement = acct.lastStatementCreationTimestamp?.let {
- ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
- }
- val lastReport = acct.lastReportCreationTimestamp?.let {
- ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
- }
- }
- }
- val specs = mutableListOf<EbicsFetchSpec>()
-
- fun addForLevel(l: FetchLevel, p: EbicsOrderParams) {
- when (l) {
- FetchLevel.ALL -> {
- specs.add(EbicsFetchSpec("C52", p))
- specs.add(EbicsFetchSpec("C53", p))
- }
- FetchLevel.REPORT -> {
- specs.add(EbicsFetchSpec("C52", p))
- }
- FetchLevel.STATEMENT -> {
- specs.add(EbicsFetchSpec("C53", p))
- }
- }
- }
-
- when (fetchSpec) {
- is FetchSpecLatestJson -> {
- val p = EbicsStandardOrderParams()
- addForLevel(fetchSpec.level, p)
- }
- is FetchSpecAllJson -> {
- val p = EbicsStandardOrderParams(
- EbicsDateRange(
- ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC),
- ZonedDateTime.now(ZoneOffset.UTC)
- )
- )
- addForLevel(fetchSpec.level, p)
- }
- is FetchSpecSinceLastJson -> {
- val pRep = EbicsStandardOrderParams(
- EbicsDateRange(
- lastTimes.lastReport ?: ZonedDateTime.ofInstant(
- Instant.EPOCH,
- ZoneOffset.UTC
- ), ZonedDateTime.now(ZoneOffset.UTC)
- )
- )
- val pStmt = EbicsStandardOrderParams(
- EbicsDateRange(
- lastTimes.lastStatement ?: ZonedDateTime.ofInstant(
- Instant.EPOCH,
- ZoneOffset.UTC
- ), ZonedDateTime.now(ZoneOffset.UTC)
- )
- )
- when (fetchSpec.level) {
- FetchLevel.ALL -> {
- specs.add(EbicsFetchSpec("C52", pRep))
- specs.add(EbicsFetchSpec("C53", pStmt))
- }
- FetchLevel.REPORT -> {
- specs.add(EbicsFetchSpec("C52", pRep))
- }
- FetchLevel.STATEMENT -> {
- specs.add(EbicsFetchSpec("C53", pStmt))
- }
- }
- }
- }
- for (spec in specs) {
- try {
- fetchEbicsC5x(spec.orderType, client, bankConnectionId, spec.orderParams, subscriberDetails)
- } catch (e: Exception) {
- logger.warn("Ingestion failed for $spec")
- }
- }
-}
-
+// Moved eventually in a tucked "camt" file.
fun storeCamt(bankConnectionId: String, camt: String, historyType: String) {
val camt53doc = XMLUtil.parseStringIntoDom(camt)
val msgId = camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId")
@@ -229,67 +135,6 @@ private suspend fun fetchEbicsC5x(
}
}
-
-fun createEbicsBankConnectionFromBackup(
- bankConnectionName: String,
- user: NexusUserEntity,
- passphrase: String?,
- backup: JsonNode
-) {
- if (passphrase === null) {
- throw NexusError(HttpStatusCode.BadRequest, "EBICS backup needs passphrase")
- }
- val bankConn = NexusBankConnectionEntity.new {
- connectionId = bankConnectionName
- owner = user
- type = "ebics"
- }
- val ebicsBackup = jacksonObjectMapper().treeToValue(backup, EbicsKeysBackupJson::class.java)
- val (authKey, encKey, sigKey) = try {
- Triple(
- CryptoUtil.decryptKey(
- EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.authBlob)),
- passphrase
- ),
- CryptoUtil.decryptKey(
- EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.encBlob)),
- passphrase
- ),
- CryptoUtil.decryptKey(
- EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.sigBlob)),
- passphrase
- )
- )
- } catch (e: Exception) {
- e.printStackTrace()
- logger.info("Restoring keys failed, probably due to wrong passphrase")
- throw NexusError(
- HttpStatusCode.BadRequest,
- "Bad backup given"
- )
- }
- try {
- EbicsSubscriberEntity.new {
- ebicsURL = ebicsBackup.ebicsURL
- hostID = ebicsBackup.hostID
- partnerID = ebicsBackup.partnerID
- userID = ebicsBackup.userID
- signaturePrivateKey = ExposedBlob(sigKey.encoded)
- encryptionPrivateKey = ExposedBlob((encKey.encoded))
- authenticationPrivateKey = ExposedBlob((authKey.encoded))
- nexusBankConnection = bankConn
- ebicsIniState = EbicsInitState.UNKNOWN
- ebicsHiaState = EbicsInitState.UNKNOWN
- }
- } catch (e: Exception) {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "exception: $e"
- )
- }
- return
-}
-
private fun getEbicsSubscriberDetailsInternal(subscriber: EbicsSubscriberEntity): EbicsClientSubscriberDetails {
var bankAuthPubValue: RSAPublicKey? = null
if (subscriber.bankAuthenticationPublicKey != null) {
@@ -335,55 +180,6 @@ private fun getEbicsSubscriberDetails(bankConnectionId: String): EbicsClientSubs
return getEbicsSubscriberDetailsInternal(subscriber)
}
-suspend fun ebicsFetchAccounts(connId: String, client: HttpClient) {
- val subscriberDetails = transaction { getEbicsSubscriberDetails(connId) }
- val response = doEbicsDownloadTransaction(
- client, subscriberDetails, "HTD", EbicsStandardOrderParams()
- )
- when (response) {
- is EbicsDownloadBankErrorResult -> {
- throw NexusError(
- HttpStatusCode.BadGateway,
- response.returnCode.errorCode
- )
- }
- is EbicsDownloadSuccessResult -> {
- val payload = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(
- response.orderData.toString(Charsets.UTF_8)
- )
- transaction {
- payload.value.partnerInfo.accountInfoList?.forEach { accountInfo ->
- val conn = NexusBankConnectionEntity.findByName(connId) ?: throw NexusError(
- HttpStatusCode.NotFound,
- "bank connection not found"
- )
-
- val isDuplicate = OfferedBankAccountsTable.select {
- OfferedBankAccountsTable.bankConnection eq conn.id and (
- OfferedBankAccountsTable.offeredAccountId eq accountInfo.id)
- }.firstOrNull()
- if (isDuplicate != null) return@forEach
- OfferedBankAccountsTable.insert { newRow ->
- newRow[accountHolder] = accountInfo.accountHolder ?: "NOT GIVEN"
- newRow[iban] =
- accountInfo.accountNumberList?.filterIsInstance<EbicsTypes.GeneralAccountNumber>()
- ?.find { it.international }?.value
- ?: throw NexusError(HttpStatusCode.NotFound, reason = "bank gave no IBAN")
- newRow[bankCode] = accountInfo.bankCodeList?.filterIsInstance<EbicsTypes.GeneralBankCode>()
- ?.find { it.international }?.value
- ?: throw NexusError(
- HttpStatusCode.NotFound,
- reason = "bank gave no BIC"
- )
- newRow[bankConnection] = requireBankConnectionInternal(connId).id
- newRow[offeredAccountId] = accountInfo.id
- }
- }
- }
- }
- }
-}
-
fun Route.ebicsBankProtocolRoutes(client: HttpClient) {
post("test-host") {
val r = call.receiveJson<EbicsHostTestRequest>()
@@ -552,52 +348,6 @@ fun Route.ebicsBankConnectionRoutes(client: HttpClient) {
}
}
-fun exportEbicsKeyBackup(bankConnectionId: String, passphrase: String): Any {
- val subscriber = transaction { getEbicsSubscriberDetails(bankConnectionId) }
- return EbicsKeysBackupJson(
- type = "ebics",
- userID = subscriber.userId,
- hostID = subscriber.hostId,
- partnerID = subscriber.partnerId,
- ebicsURL = subscriber.ebicsUrl,
- authBlob = bytesToBase64(
- CryptoUtil.encryptKey(
- subscriber.customerAuthPriv.encoded,
- passphrase
- )
- ),
- encBlob = bytesToBase64(
- CryptoUtil.encryptKey(
- subscriber.customerEncPriv.encoded,
- passphrase
- )
- ),
- sigBlob = bytesToBase64(
- CryptoUtil.encryptKey(
- subscriber.customerSignPriv.encoded,
- passphrase
- )
- )
- )
-}
-
-
-fun getEbicsConnectionDetails(conn: NexusBankConnectionEntity): Any {
- val ebicsSubscriber = transaction { getEbicsSubscriberDetails(conn.connectionId) }
- val mapper = ObjectMapper()
- val details = mapper.createObjectNode()
- details.put("ebicsUrl", ebicsSubscriber.ebicsUrl)
- details.put("ebicsHostId", ebicsSubscriber.hostId)
- details.put("partnerId", ebicsSubscriber.partnerId)
- details.put("userId", ebicsSubscriber.userId)
- val node = mapper.createObjectNode()
- node.put("type", conn.type)
- node.put("owner", conn.owner.username)
- node.put("ready", true) // test with #6715 needed.
- node.set<JsonNode>("details", details)
- return node
-}
-
/**
* Do the Hpb request when we don't know whether our keys have been submitted or not.
*
@@ -640,19 +390,163 @@ fun formatHex(ba: ByteArray): String {
return out
}
-fun getEbicsKeyLetterPdf(conn: NexusBankConnectionEntity): ByteArray {
- val ebicsSubscriber = transaction { getEbicsSubscriberDetails(conn.connectionId) }
+class EbicsBankConnectionProtocol: BankConnectionProtocol {
- val po = ByteArrayOutputStream()
- val pdfWriter = PdfWriter(po)
- val pdfDoc = PdfDocument(pdfWriter)
- val date = LocalDateTime.now()
- val dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
+ override suspend fun fetchTransactions(
+ fetchSpec: FetchSpecJson,
+ client: HttpClient,
+ bankConnectionId: String,
+ accountId: String
+ ) {
+ val subscriberDetails = transaction { getEbicsSubscriberDetails(bankConnectionId) }
+ val lastTimes = transaction {
+ val acct = NexusBankAccountEntity.findByName(accountId)
+ if (acct == null) {
+ throw NexusError(
+ HttpStatusCode.NotFound,
+ "Account not found"
+ )
+ }
+ object {
+ val lastStatement = acct.lastStatementCreationTimestamp?.let {
+ ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
+ }
+ val lastReport = acct.lastReportCreationTimestamp?.let {
+ ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
+ }
+ }
+ }
+ val specs = mutableListOf<EbicsFetchSpec>()
- fun writeCommon(doc: Document) {
- doc.add(
- Paragraph(
- """
+ fun addForLevel(l: FetchLevel, p: EbicsOrderParams) {
+ when (l) {
+ FetchLevel.ALL -> {
+ specs.add(EbicsFetchSpec("C52", p))
+ specs.add(EbicsFetchSpec("C53", p))
+ }
+ FetchLevel.REPORT -> {
+ specs.add(EbicsFetchSpec("C52", p))
+ }
+ FetchLevel.STATEMENT -> {
+ specs.add(EbicsFetchSpec("C53", p))
+ }
+ }
+ }
+
+ when (fetchSpec) {
+ is FetchSpecLatestJson -> {
+ val p = EbicsStandardOrderParams()
+ addForLevel(fetchSpec.level, p)
+ }
+ is FetchSpecAllJson -> {
+ val p = EbicsStandardOrderParams(
+ EbicsDateRange(
+ ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC),
+ ZonedDateTime.now(ZoneOffset.UTC)
+ )
+ )
+ addForLevel(fetchSpec.level, p)
+ }
+ is FetchSpecSinceLastJson -> {
+ val pRep = EbicsStandardOrderParams(
+ EbicsDateRange(
+ lastTimes.lastReport ?: ZonedDateTime.ofInstant(
+ Instant.EPOCH,
+ ZoneOffset.UTC
+ ), ZonedDateTime.now(ZoneOffset.UTC)
+ )
+ )
+ val pStmt = EbicsStandardOrderParams(
+ EbicsDateRange(
+ lastTimes.lastStatement ?: ZonedDateTime.ofInstant(
+ Instant.EPOCH,
+ ZoneOffset.UTC
+ ), ZonedDateTime.now(ZoneOffset.UTC)
+ )
+ )
+ when (fetchSpec.level) {
+ FetchLevel.ALL -> {
+ specs.add(EbicsFetchSpec("C52", pRep))
+ specs.add(EbicsFetchSpec("C53", pStmt))
+ }
+ FetchLevel.REPORT -> {
+ specs.add(EbicsFetchSpec("C52", pRep))
+ }
+ FetchLevel.STATEMENT -> {
+ specs.add(EbicsFetchSpec("C53", pStmt))
+ }
+ }
+ }
+ }
+ for (spec in specs) {
+ try {
+ fetchEbicsC5x(spec.orderType, client, bankConnectionId, spec.orderParams, subscriberDetails)
+ } catch (e: Exception) {
+ logger.warn("Ingestion failed for $spec")
+ }
+ }
+ }
+
+ override suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) {
+ val r = transaction {
+ val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId)
+ ?: throw NexusError(HttpStatusCode.NotFound, "payment initiation not found")
+ val conn = paymentInitiation.bankAccount.defaultBankConnection
+ ?: throw NexusError(HttpStatusCode.NotFound, "no default bank connection available for submission")
+ val subscriberDetails = getEbicsSubscriberDetails(conn.connectionId)
+ val painMessage = createPain001document(
+ NexusPaymentInitiationData(
+ debtorIban = paymentInitiation.bankAccount.iban,
+ debtorBic = paymentInitiation.bankAccount.bankCode,
+ debtorName = paymentInitiation.bankAccount.accountHolder,
+ currency = paymentInitiation.currency,
+ amount = paymentInitiation.sum.toString(),
+ creditorIban = paymentInitiation.creditorIban,
+ creditorName = paymentInitiation.creditorName,
+ creditorBic = paymentInitiation.creditorBic,
+ paymentInformationId = paymentInitiation.paymentInformationId,
+ preparationTimestamp = paymentInitiation.preparationDate,
+ subject = paymentInitiation.subject,
+ instructionId = paymentInitiation.instructionId,
+ endToEndId = paymentInitiation.endToEndId,
+ messageId = paymentInitiation.messageId
+ )
+ )
+ if (!XMLUtil.validateFromString(painMessage)) throw NexusError(
+ HttpStatusCode.InternalServerError, "Pain.001 message is invalid."
+ )
+ object {
+ val subscriberDetails = subscriberDetails
+ val painMessage = painMessage
+ }
+ }
+ doEbicsUploadTransaction(
+ httpClient,
+ r.subscriberDetails,
+ "CCT",
+ r.painMessage.toByteArray(Charsets.UTF_8),
+ EbicsStandardOrderParams()
+ )
+ transaction {
+ val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId)
+ ?: throw NexusError(HttpStatusCode.NotFound, "payment initiation not found")
+ paymentInitiation.submitted = true
+ paymentInitiation.submissionDate = LocalDateTime.now().millis()
+ }
+ }
+
+ override fun exportAnalogDetails(conn: NexusBankConnectionEntity): ByteArray {
+ val ebicsSubscriber = transaction { getEbicsSubscriberDetails(conn.connectionId) }
+ val po = ByteArrayOutputStream()
+ val pdfWriter = PdfWriter(po)
+ val pdfDoc = PdfDocument(pdfWriter)
+ val date = LocalDateTime.now()
+ val dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
+
+ fun writeCommon(doc: Document) {
+ doc.add(
+ Paragraph(
+ """
Datum: $dateStr
Teilnehmer: ${conn.id.value}
Host-ID: ${ebicsSubscriber.hostId}
@@ -660,130 +554,235 @@ fun getEbicsKeyLetterPdf(conn: NexusBankConnectionEntity): ByteArray {
Partner-ID: ${ebicsSubscriber.partnerId}
ES version: A006
""".trimIndent()
+ )
)
- )
- }
-
- fun writeKey(doc: Document, priv: RSAPrivateCrtKey) {
- val pub = CryptoUtil.getRsaPublicFromPrivate(priv)
- val hash = CryptoUtil.getEbicsPublicKeyHash(pub)
- doc.add(Paragraph("Exponent:\n${formatHex(pub.publicExponent.toByteArray())}"))
- doc.add(Paragraph("Modulus:\n${formatHex(pub.modulus.toByteArray())}"))
- doc.add(Paragraph("SHA-256 hash:\n${formatHex(hash)}"))
- }
-
- fun writeSigLine(doc: Document) {
- doc.add(Paragraph("Ort / Datum: ________________"))
- doc.add(Paragraph("Firma / Name: ________________"))
- doc.add(Paragraph("Unterschrift: ________________"))
- }
-
- Document(pdfDoc).use {
- it.add(Paragraph("Signaturschlüssel").setFontSize(24f))
- writeCommon(it)
- it.add(Paragraph("Öffentlicher Schlüssel (Public key for the electronic signature)"))
- writeKey(it, ebicsSubscriber.customerSignPriv)
- it.add(Paragraph("\n"))
- writeSigLine(it)
- it.add(AreaBreak())
-
- it.add(Paragraph("Authentifikationsschlüssel").setFontSize(24f))
- writeCommon(it)
- it.add(Paragraph("Öffentlicher Schlüssel (Public key for the identification and authentication signature)"))
- writeKey(it, ebicsSubscriber.customerAuthPriv)
- it.add(Paragraph("\n"))
- writeSigLine(it)
- it.add(AreaBreak())
-
- it.add(Paragraph("Verschlüsselungsschlüssel").setFontSize(24f))
- writeCommon(it)
- it.add(Paragraph("Öffentlicher Schlüssel (Public encryption key)"))
- writeKey(it, ebicsSubscriber.customerSignPriv)
- it.add(Paragraph("\n"))
- writeSigLine(it)
- }
- pdfWriter.flush()
- return po.toByteArray()
-}
+ }
-suspend fun submitEbicsPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) {
- val r = transaction {
- val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId)
- ?: throw NexusError(HttpStatusCode.NotFound, "payment initiation not found")
- val conn = paymentInitiation.bankAccount.defaultBankConnection
- ?: throw NexusError(HttpStatusCode.NotFound, "no default bank connection available for submission")
- val subscriberDetails = getEbicsSubscriberDetails(conn.connectionId)
- val painMessage = createPain001document(
- NexusPaymentInitiationData(
- debtorIban = paymentInitiation.bankAccount.iban,
- debtorBic = paymentInitiation.bankAccount.bankCode,
- debtorName = paymentInitiation.bankAccount.accountHolder,
- currency = paymentInitiation.currency,
- amount = paymentInitiation.sum.toString(),
- creditorIban = paymentInitiation.creditorIban,
- creditorName = paymentInitiation.creditorName,
- creditorBic = paymentInitiation.creditorBic,
- paymentInformationId = paymentInitiation.paymentInformationId,
- preparationTimestamp = paymentInitiation.preparationDate,
- subject = paymentInitiation.subject,
- instructionId = paymentInitiation.instructionId,
- endToEndId = paymentInitiation.endToEndId,
- messageId = paymentInitiation.messageId
+ fun writeKey(doc: Document, priv: RSAPrivateCrtKey) {
+ val pub = CryptoUtil.getRsaPublicFromPrivate(priv)
+ val hash = CryptoUtil.getEbicsPublicKeyHash(pub)
+ doc.add(Paragraph("Exponent:\n${formatHex(pub.publicExponent.toByteArray())}"))
+ doc.add(Paragraph("Modulus:\n${formatHex(pub.modulus.toByteArray())}"))
+ doc.add(Paragraph("SHA-256 hash:\n${formatHex(hash)}"))
+ }
+
+ fun writeSigLine(doc: Document) {
+ doc.add(Paragraph("Ort / Datum: ________________"))
+ doc.add(Paragraph("Firma / Name: ________________"))
+ doc.add(Paragraph("Unterschrift: ________________"))
+ }
+
+ Document(pdfDoc).use {
+ it.add(Paragraph("Signaturschlüssel").setFontSize(24f))
+ writeCommon(it)
+ it.add(Paragraph("Öffentlicher Schlüssel (Public key for the electronic signature)"))
+ writeKey(it, ebicsSubscriber.customerSignPriv)
+ it.add(Paragraph("\n"))
+ writeSigLine(it)
+ it.add(AreaBreak())
+
+ it.add(Paragraph("Authentifikationsschlüssel").setFontSize(24f))
+ writeCommon(it)
+ it.add(Paragraph("Öffentlicher Schlüssel (Public key for the identification and authentication signature)"))
+ writeKey(it, ebicsSubscriber.customerAuthPriv)
+ it.add(Paragraph("\n"))
+ writeSigLine(it)
+ it.add(AreaBreak())
+
+ it.add(Paragraph("Verschlüsselungsschlüssel").setFontSize(24f))
+ writeCommon(it)
+ it.add(Paragraph("Öffentlicher Schlüssel (Public encryption key)"))
+ writeKey(it, ebicsSubscriber.customerSignPriv)
+ it.add(Paragraph("\n"))
+ writeSigLine(it)
+ }
+ pdfWriter.flush()
+ return po.toByteArray()
+ }
+ override fun exportBackup(bankConnectionId: String, passphrase: String): JsonNode {
+ val subscriber = transaction { getEbicsSubscriberDetails(bankConnectionId) }
+ val ret = EbicsKeysBackupJson(
+ type = "ebics",
+ userID = subscriber.userId,
+ hostID = subscriber.hostId,
+ partnerID = subscriber.partnerId,
+ ebicsURL = subscriber.ebicsUrl,
+ authBlob = bytesToBase64(
+ CryptoUtil.encryptKey(
+ subscriber.customerAuthPriv.encoded,
+ passphrase
+ )
+ ),
+ encBlob = bytesToBase64(
+ CryptoUtil.encryptKey(
+ subscriber.customerEncPriv.encoded,
+ passphrase
+ )
+ ),
+ sigBlob = bytesToBase64(
+ CryptoUtil.encryptKey(
+ subscriber.customerSignPriv.encoded,
+ passphrase
+ )
)
)
- if (!XMLUtil.validateFromString(painMessage)) throw NexusError(
- HttpStatusCode.InternalServerError, "Pain.001 message is invalid."
+ val mapper = ObjectMapper()
+ return mapper.valueToTree(ret)
+ }
+
+ override fun getConnectionDetails(conn: NexusBankConnectionEntity): JsonNode {
+ val ebicsSubscriber = transaction { getEbicsSubscriberDetails(conn.connectionId) }
+ val mapper = ObjectMapper()
+ val details = mapper.createObjectNode()
+ details.put("ebicsUrl", ebicsSubscriber.ebicsUrl)
+ details.put("ebicsHostId", ebicsSubscriber.hostId)
+ details.put("partnerId", ebicsSubscriber.partnerId)
+ details.put("userId", ebicsSubscriber.userId)
+ val node = mapper.createObjectNode()
+ node.put("type", conn.type)
+ node.put("owner", conn.owner.username)
+ node.put("ready", true) // test with #6715 needed.
+ node.set<JsonNode>("details", details)
+ return node
+ }
+
+ override fun createConnection(connId: String, user: NexusUserEntity, data: JsonNode) {
+ val bankConn = NexusBankConnectionEntity.new {
+ this.connectionId = connId
+ owner = user
+ type = "ebics"
+ }
+ val newTransportData = jacksonObjectMapper(
+ ).treeToValue(data, EbicsNewTransport::class.java) ?: throw NexusError(
+ HttpStatusCode.BadRequest, "Ebics details not found in request"
)
- object {
- val subscriberDetails = subscriberDetails
- val painMessage = painMessage
+ val pairA = CryptoUtil.generateRsaKeyPair(2048)
+ val pairB = CryptoUtil.generateRsaKeyPair(2048)
+ val pairC = CryptoUtil.generateRsaKeyPair(2048)
+ EbicsSubscriberEntity.new {
+ ebicsURL = newTransportData.ebicsURL
+ hostID = newTransportData.hostID
+ partnerID = newTransportData.partnerID
+ userID = newTransportData.userID
+ systemID = newTransportData.systemID
+ signaturePrivateKey = ExposedBlob((pairA.private.encoded))
+ encryptionPrivateKey = ExposedBlob((pairB.private.encoded))
+ authenticationPrivateKey = ExposedBlob((pairC.private.encoded))
+ nexusBankConnection = bankConn
+ ebicsIniState = EbicsInitState.NOT_SENT
+ ebicsHiaState = EbicsInitState.NOT_SENT
+ }
+ }
+
+ override fun createConnectionFromBackup(
+ connId: String,
+ user: NexusUserEntity,
+ passphrase: String?,
+ backup: JsonNode
+ ) {
+ if (passphrase === null) {
+ throw NexusError(HttpStatusCode.BadRequest, "EBICS backup needs passphrase")
+ }
+ val bankConn = NexusBankConnectionEntity.new {
+ connectionId = connId
+ owner = user
+ type = "ebics"
+ }
+ val ebicsBackup = jacksonObjectMapper().treeToValue(backup, EbicsKeysBackupJson::class.java)
+ val (authKey, encKey, sigKey) = try {
+ Triple(
+ CryptoUtil.decryptKey(
+ EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.authBlob)),
+ passphrase
+ ),
+ CryptoUtil.decryptKey(
+ EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.encBlob)),
+ passphrase
+ ),
+ CryptoUtil.decryptKey(
+ EncryptedPrivateKeyInfo(base64ToBytes(ebicsBackup.sigBlob)),
+ passphrase
+ )
+ )
+ } catch (e: Exception) {
+ e.printStackTrace()
+ logger.info("Restoring keys failed, probably due to wrong passphrase")
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "Bad backup given"
+ )
}
+ try {
+ EbicsSubscriberEntity.new {
+ ebicsURL = ebicsBackup.ebicsURL
+ hostID = ebicsBackup.hostID
+ partnerID = ebicsBackup.partnerID
+ userID = ebicsBackup.userID
+ signaturePrivateKey = ExposedBlob(sigKey.encoded)
+ encryptionPrivateKey = ExposedBlob((encKey.encoded))
+ authenticationPrivateKey = ExposedBlob((authKey.encoded))
+ nexusBankConnection = bankConn
+ ebicsIniState = EbicsInitState.UNKNOWN
+ ebicsHiaState = EbicsInitState.UNKNOWN
+ }
+ } catch (e: Exception) {
+ throw NexusError(
+ HttpStatusCode.BadRequest,
+ "exception: $e"
+ )
+ }
+ return
}
- doEbicsUploadTransaction(
- httpClient,
- r.subscriberDetails,
- "CCT",
- r.painMessage.toByteArray(Charsets.UTF_8),
- EbicsStandardOrderParams()
- )
- transaction {
- val paymentInitiation = PaymentInitiationEntity.findById(paymentInitiationId)
- ?: throw NexusError(HttpStatusCode.NotFound, "payment initiation not found")
- paymentInitiation.submitted = true
- paymentInitiation.submissionDate = LocalDateTime.now().millis()
- }
-}
+ override suspend fun fetchAccounts(client: HttpClient, connId: String) {
+ val subscriberDetails = transaction { getEbicsSubscriberDetails(connId) }
+ val response = doEbicsDownloadTransaction(
+ client, subscriberDetails, "HTD", EbicsStandardOrderParams()
+ )
+ when (response) {
+ is EbicsDownloadBankErrorResult -> {
+ throw NexusError(
+ HttpStatusCode.BadGateway,
+ response.returnCode.errorCode
+ )
+ }
+ is EbicsDownloadSuccessResult -> {
+ val payload = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(
+ response.orderData.toString(Charsets.UTF_8)
+ )
+ transaction {
+ payload.value.partnerInfo.accountInfoList?.forEach { accountInfo ->
+ val conn = NexusBankConnectionEntity.findByName(connId) ?: throw NexusError(
+ HttpStatusCode.NotFound,
+ "bank connection not found"
+ )
-fun createEbicsBankConnection(bankConnectionName: String, user: NexusUserEntity, data: JsonNode) {
- val bankConn = NexusBankConnectionEntity.new {
- this.connectionId = bankConnectionName
- owner = user
- type = "ebics"
- }
- val newTransportData = jacksonObjectMapper(
- ).treeToValue(data, EbicsNewTransport::class.java) ?: throw NexusError(
- HttpStatusCode.BadRequest, "Ebics details not found in request"
- )
- val pairA = CryptoUtil.generateRsaKeyPair(2048)
- val pairB = CryptoUtil.generateRsaKeyPair(2048)
- val pairC = CryptoUtil.generateRsaKeyPair(2048)
- EbicsSubscriberEntity.new {
- ebicsURL = newTransportData.ebicsURL
- hostID = newTransportData.hostID
- partnerID = newTransportData.partnerID
- userID = newTransportData.userID
- systemID = newTransportData.systemID
- signaturePrivateKey = ExposedBlob((pairA.private.encoded))
- encryptionPrivateKey = ExposedBlob((pairB.private.encoded))
- authenticationPrivateKey = ExposedBlob((pairC.private.encoded))
- nexusBankConnection = bankConn
- ebicsIniState = EbicsInitState.NOT_SENT
- ebicsHiaState = EbicsInitState.NOT_SENT
+ val isDuplicate = OfferedBankAccountsTable.select {
+ OfferedBankAccountsTable.bankConnection eq conn.id and (
+ OfferedBankAccountsTable.offeredAccountId eq accountInfo.id)
+ }.firstOrNull()
+ if (isDuplicate != null) return@forEach
+ OfferedBankAccountsTable.insert { newRow ->
+ newRow[accountHolder] = accountInfo.accountHolder ?: "NOT GIVEN"
+ newRow[iban] =
+ accountInfo.accountNumberList?.filterIsInstance<EbicsTypes.GeneralAccountNumber>()
+ ?.find { it.international }?.value
+ ?: throw NexusError(HttpStatusCode.NotFound, reason = "bank gave no IBAN")
+ newRow[bankCode] = accountInfo.bankCodeList?.filterIsInstance<EbicsTypes.GeneralBankCode>()
+ ?.find { it.international }?.value
+ ?: throw NexusError(
+ HttpStatusCode.NotFound,
+ reason = "bank gave no BIC"
+ )
+ newRow[bankConnection] = requireBankConnectionInternal(connId).id
+ newRow[offeredAccountId] = accountInfo.id
+ }
+ }
+ }
+ }
+ }
}
-}
-class EbicsBankConnectionProtocol: BankConnectionProtocol {
override suspend fun connect(client: HttpClient, connId: String) {
val subscriber = transaction { getEbicsSubscriberDetails(connId) }
if (subscriber.bankAuthPub != null && subscriber.bankEncPub != null) {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index e8e9895b..37ef78b6 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -736,31 +736,12 @@ fun serverMain(dbName: String, host: String, port: Int) {
if (type == null || !type.isTextual) {
throw NexusError(HttpStatusCode.BadRequest, "backup needs type")
}
- when (type.textValue()) {
- "ebics" -> {
- createEbicsBankConnectionFromBackup(body.name, user, body.passphrase, body.data)
- }
- else -> {
- throw NexusError(HttpStatusCode.BadRequest, "backup type not supported")
- }
- }
+ val plugin = getConnectionPlugin(type.textValue())
+ plugin.createConnectionFromBackup(body.name, user, body.passphrase, body.data)
}
is CreateBankConnectionFromNewRequestJson -> {
- when (body.type) {
- "ebics" -> {
- createEbicsBankConnection(body.name, user, body.data)
- }
- "loopback" -> {
- createLoopbackBankConnection(body.name, user, body.data)
-
- }
- else -> {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "connection type ${body.type} not supported"
- )
- }
- }
+ val plugin = getConnectionPlugin(body.type)
+ plugin.createConnection(body.name, user, body.data)
}
}
}
@@ -802,17 +783,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
requireSuperuser(call.request)
val resp = transaction {
val conn = requireBankConnection(call, "connectionName")
- when (conn.type) {
- "ebics" -> {
- getEbicsConnectionDetails(conn)
- }
- else -> {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but '${conn.type}')"
- )
- }
- }
+ getConnectionPlugin(conn.type).getConnectionDetails(conn)
}
call.respond(resp)
}
@@ -823,17 +794,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
val body = call.receive<BackupRequestJson>()
val response = run {
val conn = requireBankConnection(call, "connectionName")
- when (conn.type) {
- "ebics" -> {
- exportEbicsKeyBackup(conn.connectionId, body.passphrase)
- }
- else -> {
- throw NexusError(
- HttpStatusCode.BadRequest,
- "bank connection is not of type 'ebics' (but '${conn.type}')"
- )
- }
- }
+ getConnectionPlugin(conn.type).exportBackup(conn.connectionId, body.passphrase)
}
call.response.headers.append("Content-Disposition", "attachment")
call.respond(
@@ -859,13 +820,8 @@ fun serverMain(dbName: String, host: String, port: Int) {
authenticateRequest(call.request)
requireBankConnection(call, "connectionName")
}
- when (conn.type) {
- "ebics" -> {
- val pdfBytes = getEbicsKeyLetterPdf(conn)
- call.respondBytes(pdfBytes, ContentType("application", "pdf"))
- }
- else -> throw NexusError(HttpStatusCode.NotImplemented, "keyletter not supported for ${conn.type}")
- }
+ val pdfBytes = getConnectionPlugin(conn.type).exportAnalogDetails(conn)
+ call.respondBytes(pdfBytes, ContentType("application", "pdf"))
}
get("/bank-connections/{connectionName}/messages") {
@@ -994,15 +950,7 @@ fun serverMain(dbName: String, host: String, port: Int) {
authenticateRequest(call.request)
requireBankConnection(call, "connid")
}
- when (conn.type) {
- "ebics" -> {
- ebicsFetchAccounts(conn.connectionId, client)
- }
- else -> throw NexusError(
- HttpStatusCode.NotImplemented,
- "connection not supported for ${conn.type}"
- )
- }
+ getConnectionPlugin(conn.type).fetchAccounts(client, conn.connectionId)
call.respond(object {})
}