/*
* 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
*
*/
package tech.libeufin.sandbox
import io.ktor.application.ApplicationCall
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.request.receiveText
import io.ktor.response.respond
import io.ktor.response.respondText
import io.ktor.util.AttributeKey
import org.apache.xml.security.binding.xmldsig.RSAKeyValueType
import org.jetbrains.exposed.exceptions.ExposedSQLException
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.transaction
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.w3c.dom.Document
import tech.libeufin.util.*
import tech.libeufin.util.XMLUtil.Companion.signEbicsResponse
import tech.libeufin.util.ebics_h004.*
import tech.libeufin.util.ebics_hev.HEVResponse
import tech.libeufin.util.ebics_hev.SystemReturnCodeType
import tech.libeufin.util.ebics_s001.SignatureTypes
import tech.libeufin.util.ebics_s001.UserSignatureData
import java.math.BigDecimal
import java.security.interfaces.RSAPrivateCrtKey
import java.security.interfaces.RSAPublicKey
import java.time.Instant
import java.time.LocalDateTime
import java.util.*
import java.util.zip.DeflaterInputStream
import java.util.zip.InflaterInputStream
val EbicsHostIdAttribute = AttributeKey("RequestedEbicsHostID")
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox")
data class PainParseResult(
val creditorIban: String,
val creditorName: String,
val creditorBic: String?,
val debtorIban: String,
val debtorName: String,
val debtorBic: String?,
val subject: String,
val amount: Amount,
val currency: String,
val pmtInfId: String,
val msgId: String
)
open class EbicsRequestError(
val errorText: String,
val errorCode: String
) : Exception("EBICS request error: $errorText ($errorCode)")
class EbicsInvalidRequestError : EbicsRequestError(
"[EBICS_INVALID_REQUEST] Invalid request",
"060102"
)
class EbicsAccountAuthorisationFailed : EbicsRequestError(
"[EBICS_ACCOUNT_AUTHORISATION_FAILED] Subscriber's signature didn't verify",
"091302"
)
/**
* This error is thrown whenever the Subscriber's state is not suitable
* for the requested action. For example, the subscriber sends a EbicsRequest
* message without having first uploaded their keys (#5973).
*/
class EbicsSubscriberStateError : EbicsRequestError(
"[EBICS_INVALID_USER_OR_USER_STATE] Subscriber unknown or subscriber state inadmissible",
"091002"
)
open class EbicsKeyManagementError(val errorText: String, val errorCode: String) :
Exception("EBICS key management error: $errorText ($errorCode)")
private class EbicsInvalidXmlError : EbicsKeyManagementError(
"[EBICS_INVALID_XML]",
"091010"
)
private class EbicsUnsupportedOrderType : EbicsRequestError(
"[EBICS_UNSUPPORTED_ORDER_TYPE] Order type not supported",
"091005"
)
/**
* Used here also for "Internal server error". For example, when the
* sandbox itself generates a invalid XML response.
*/
class EbicsProcessingError(detail: String) : EbicsRequestError(
"[EBICS_PROCESSING_ERROR] $detail",
"091116"
)
private suspend fun ApplicationCall.respondEbicsKeyManagement(
errorText: String,
errorCode: String,
bankReturnCode: String,
dataTransfer: CryptoUtil.EncryptionResult? = null,
orderId: String? = null
) {
val responseXml = EbicsKeyManagementResponse().apply {
version = "H004"
header = EbicsKeyManagementResponse.Header().apply {
authenticate = true
mutable = EbicsKeyManagementResponse.MutableHeaderType().apply {
reportText = errorText
returnCode = errorCode
if (orderId != null) {
this.orderID = orderId
}
}
_static = EbicsKeyManagementResponse.EmptyStaticHeader()
}
body = EbicsKeyManagementResponse.Body().apply {
this.returnCode = EbicsKeyManagementResponse.ReturnCode().apply {
this.authenticate = true
this.value = bankReturnCode
}
if (dataTransfer != null) {
this.dataTransfer = EbicsKeyManagementResponse.DataTransfer().apply {
this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply {
this.authenticate = true
this.transactionKey = dataTransfer.encryptedTransactionKey
this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply {
this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256"
this.version = "E002"
this.value = dataTransfer.pubKeyDigest
}
}
this.orderData = EbicsKeyManagementResponse.OrderData().apply {
this.value = Base64.getEncoder().encodeToString(dataTransfer.encryptedData)
}
}
}
}
}
val text = XMLUtil.convertJaxbToString(responseXml)
logger.info("responding with:\n${text}")
if (!XMLUtil.validateFromString(text)) throw SandboxError(
HttpStatusCode.InternalServerError,
"Outgoint EBICS key management response is invalid"
)
respondText(text, ContentType.Application.Xml, HttpStatusCode.OK)
}
fun expectNonNull(x: T?): T {
if (x == null) {
throw EbicsProtocolError(HttpStatusCode.BadRequest, "expected non-null value")
}
return x;
}
private fun getRelatedParty(branch: XmlElementBuilder, payment: RawPayment) {
val otherParty = object {
var ibanPath = "CdtrAcct/Id/IBAN"
var namePath = "Cdtr/Nm"
var iban = payment.creditorIban
var name = payment.creditorName
var bicPath = "CdtrAgt/FinInstnId/BIC"
var bic = payment.creditorBic
}
if (payment.direction == "CRDT") {
otherParty.iban = payment.debtorIban
otherParty.ibanPath = "DbtrAcct/Id/IBAN"
otherParty.namePath = "Dbtr/Nm"
otherParty.name = payment.debtorName
otherParty.bic = payment.debtorBic
otherParty.bicPath = "DbtrAgt/FinInstnId/BIC"
}
branch.element("RltdPties") {
element(otherParty.namePath) {
text(otherParty.name)
}
element(otherParty.ibanPath) {
text(otherParty.iban)
}
}
val otherPartyBic = otherParty.bic
if (otherPartyBic != null) {
branch.element("RltdAgts") {
element(otherParty.bicPath) {
text(otherPartyBic)
}
}
}
}
fun buildCamtString(
type: Int,
subscriberIban: String,
freshHistory: MutableList,
balancePrcd: BigDecimal, // Balance up to freshHistory (excluded).
balanceClbd: BigDecimal
): SandboxCamt {
/**
* ID types required:
*
* - Message Id
* - Statement / Report Id
* - Electronic sequence number
* - Legal sequence number
* - Entry Id by the Servicer
* - Payment information Id
* - Proprietary code of the bank transaction
* - Id of the servicer (Issuer and Code)
*/
val creationTime = getUTCnow()
val dashedDate = creationTime.toDashedDate()
val zonedDateTime = creationTime.toZonedString()
val creationTimeMillis = creationTime.toInstant().toEpochMilli()
val messageId = "sandbox-${creationTimeMillis}"
val camtMessage = constructXml(indent = true) {
root("Document") {
attribute("xmlns", "urn:iso:std:iso:20022:tech:xsd:camt.0${type}.001.02")
attribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance")
attribute(
"xsi:schemaLocation",
"urn:iso:std:iso:20022:tech:xsd:camt.0${type}.001.02 camt.0${type}.001.02.xsd"
)
element(if (type == 53) "BkToCstmrStmt" else "BkToCstmrAcctRpt") {
element("GrpHdr") {
element("MsgId") {
text(messageId)
}
element("CreDtTm") {
text(zonedDateTime)
}
element("MsgPgntn") {
element("PgNb") {
text("001")
}
element("LastPgInd") {
text("true")
}
}
}
element(if (type == 52) "Rpt" else "Stmt") {
element("Id") {
text("0")
}
element("ElctrncSeqNb") {
text("0")
}
element("LglSeqNb") {
text("0")
}
element("CreDtTm") {
text(zonedDateTime)
}
element("Acct") {
// mandatory account identifier
element("Id/IBAN") {
text(subscriberIban)
}
element("Ccy") {
text("EUR")
}
element("Ownr/Nm") {
text("Debitor/Owner Name")
}
element("Svcr/FinInstnId") {
element("Nm") {
text("Libeufin Bank")
}
element("Othr") {
element("Id") {
text("0")
}
element("Issr") {
text("XY")
}
}
}
}
element("Bal") {
element("Tp/CdOrPrtry/Cd") {
/* Balance type, in a coded format. PRCD stands
for "Previously closed booked" and shows the
balance at the time _before_ all the entries
reported in this document were posted to the
involved bank account. */
text("PRCD")
}
element("Amt") {
attribute("Ccy", "EUR")
if (balancePrcd < BigDecimal.ZERO) {
text(balancePrcd.abs().toPlainString())
} else {
text(balancePrcd.toPlainString())
}
}
element("CdtDbtInd") {
text("CRDT")
}
element("Dt/Dt") {
// date of this balance
text(dashedDate)
}
}
element("Bal") {
element("Tp/CdOrPrtry/Cd") {
/* CLBD stands for "Closing booked balance", and it
is calculated by summing the PRCD with all the
entries reported in this document */
text("CLBD")
}
element("Amt") {
attribute("Ccy", "EUR")
// FIXME: the balance computation still not working properly
//text(balanceForAccount(subscriberIban).toString())
if (balanceClbd < BigDecimal.ZERO) {
text(balanceClbd.abs().toPlainString())
} else {
text(balanceClbd.toPlainString())
}
}
element("CdtDbtInd") {
// a temporary value to get the camt to validate.
// Should be fixed along #6269
if (balanceClbd < BigDecimal.ZERO) {
text("DBIT")
} else {
text("CRDT")
}
}
element("Dt/Dt") {
text(dashedDate)
}
}
freshHistory.forEach {
this.element("Ntry") {
element("Amt") {
attribute("Ccy", it.currency)
text(it.amount)
}
element("CdtDbtInd") {
text(
if (subscriberIban.equals(it.creditorIban))
"CRDT" else "DBIT"
)
}
element("Sts") {
/* Status of the entry (see 2.4.2.15.5 from the ISO20022 reference document.)
* From the original text:
* "Status of an entry on the books of the account servicer" */
text("BOOK")
}
element("BookgDt/Dt") {
text(dashedDate)
} // date of the booking
element("ValDt/Dt") {
text(dashedDate)
} // date of assets' actual (un)availability
element("AcctSvcrRef") {
text(it.uid)
}
element("BkTxCd") {
/* "Set of elements used to fully identify the type of underlying
* transaction resulting in an entry". */
element("Domn") {
element("Cd") {
text("PMNT")
}
element("Fmly") {
element("Cd") {
text("ICDT")
}
element("SubFmlyCd") {
text("ESCT")
}
}
}
element("Prtry") {
element("Cd") {
text("0")
}
element("Issr") {
text("XY")
}
}
}
element("NtryDtls/TxDtls") {
element("Refs") {
element("MsgId") {
text(it.msgId ?: "NOTPROVIDED")
}
element("PmtInfId") {
text(it.pmtInfId ?: "NOTPROVIDED")
}
element("EndToEndId") {
text("NOTPROVIDED")
}
}
element("AmtDtls/TxAmt/Amt") {
attribute("Ccy", "EUR")
text(it.amount)
}
element("BkTxCd") {
element("Domn") {
element("Cd") {
text("PMNT")
}
element("Fmly") {
element("Cd") {
text("ICDT")
}
element("SubFmlyCd") {
text("ESCT")
}
}
}
element("Prtry") {
element("Cd") {
text("0")
}
element("Issr") {
text("XY")
}
}
}
getRelatedParty(this, it)
element("RmtInf/Ustrd") {
text(it.subject)
}
}
}
}
}
}
}
}
return SandboxCamt(
camtMessage = camtMessage,
messageId = messageId,
creationTime = creationTimeMillis
)
}
fun getHistoryElementFromTransactionRow(
dbRow: BankAccountFreshTransactionEntity
): RawPayment {
return RawPayment(
subject = dbRow.transactionRef.subject,
creditorIban = dbRow.transactionRef.creditorIban,
creditorBic = dbRow.transactionRef.creditorBic,
creditorName = dbRow.transactionRef.creditorName,
debtorIban = dbRow.transactionRef.debtorIban,
debtorBic = dbRow.transactionRef.debtorBic,
debtorName = dbRow.transactionRef.debtorName,
date = importDateFromMillis(dbRow.transactionRef.date).toDashedDate(),
amount = dbRow.transactionRef.amount,
currency = dbRow.transactionRef.currency,
// The line below produces a value too long (>35 chars),
// and dbRow makes the document invalid!
// uid = "${dbRow.pmtInfId}-${it.msgId}"
uid = dbRow.transactionRef.accountServicerReference,
direction = dbRow.transactionRef.direction,
pmtInfId = dbRow.transactionRef.pmtInfId
)
}
fun getLastBalance(bankAccount: BankAccountEntity): BigDecimal {
val lastStatement = BankAccountStatementEntity.find {
BankAccountStatementsTable.bankAccount eq bankAccount.id
}.firstOrNull()
val lastBalance = if (lastStatement == null) {
BigDecimal.ZERO } else { BigDecimal(lastStatement.balanceClbd) }
return lastBalance
}
/**
* Builds CAMT response.
*
* @param type 52 or 53.
*/
private fun constructCamtResponse(
type: Int,
subscriber: EbicsSubscriberEntity,
// fixes #6243
dateRange: Pair?): List {
if (type != 53 && type != 52) throw EbicsUnsupportedOrderType()
val bankAccount = getBankAccountFromSubscriber(subscriber)
if (type == 52) {
val history = mutableListOf()
/**
* This block adds all the fresh transactions to the intermediate
* history list and returns the last balance that was reported in a
* C53 document. This latter will be the base balance to calculate
* the final balance after the fresh transactions.
*/
val lastBalance = transaction {
BankAccountFreshTransactionEntity.all().forEach {
if (it.transactionRef.account.label == bankAccount.label) {
history.add(getHistoryElementFromTransactionRow(it))
}
}
getLastBalance(bankAccount) // last reported balance
}
val freshBalance = balanceForAccount(
history = history,
baseBalance = lastBalance
)
return listOf(
buildCamtString(
type,
bankAccount.iban,
history,
balancePrcd = lastBalance,
balanceClbd = freshBalance
).camtMessage
)
}
SandboxAssert(type == 53, "Didn't catch unsupported Camt type")
logger.debug("Finding C$type records")
/**
* FIXME: when this function throws an exception, it makes a JSON response being responded.
* That is bad, because here we're inside a Ebics handler and only XML should
* be returned to the requester. This problem makes the (unhelpful) "bank didn't
* return XML" message appear in the Nexus logs.
*/
val ret = mutableListOf()
/**
* Retrieve all the records whose creation date lies into the
* time range given as a function's parameter.
*/
if (dateRange != null) {
logger.debug("Querying c$type with date range: $dateRange")
BankAccountStatementEntity.find {
BankAccountStatementsTable.creationTime.between(
dateRange.first,
dateRange.second) and(
BankAccountStatementsTable.bankAccount eq bankAccount.id)
}.forEach { ret.add(it.xmlMessage) }
} else {
/**
* No time range was given, hence pick the latest statement.
*/
logger.debug("No date range was given for c$type, respond with latest document")
BankAccountStatementEntity.find {
BankAccountStatementsTable.bankAccount eq bankAccount.id
}.lastOrNull().apply {
if (this != null) {
ret.add(this.xmlMessage)
}
}
}
if (ret.size == 0) throw EbicsRequestError(
"[EBICS_NO_DOWNLOAD_DATA_AVAILABLE] as Camt $type",
"090005"
)
return ret
}
/**
* TSD (test download) message.
*
* This is a non-standard EBICS order type use by LibEuFin to
* test download transactions.
*
* In the future, additional parameters (size, chunking, inject fault for retry) might
* be added to the order parameters.
*/
private fun handleEbicsTSD(): ByteArray {
return "Hello World\n".repeat(1024).toByteArray()
}
private fun handleEbicsPTK(): ByteArray {
return "Hello I am a dummy PTK response.".toByteArray()
}
private fun parsePain001(paymentRequest: String): PainParseResult {
val painDoc = XMLUtil.parseStringIntoDom(paymentRequest)
return destructXml(painDoc) {
requireRootElement("Document") {
requireUniqueChildNamed("CstmrCdtTrfInitn") {
val msgId = requireUniqueChildNamed("GrpHdr") {
requireUniqueChildNamed("MsgId") { focusElement.textContent }
}
requireUniqueChildNamed("PmtInf") {
val debtorName = requireUniqueChildNamed("Dbtr"){
requireUniqueChildNamed("Nm") {
focusElement.textContent
}
}
val debtorIban = requireUniqueChildNamed("DbtrAcct"){
requireUniqueChildNamed("Id") {
requireUniqueChildNamed("IBAN") {
focusElement.textContent
}
}
}
val debtorBic = requireUniqueChildNamed("DbtrAgt"){
requireUniqueChildNamed("FinInstnId") {
requireUniqueChildNamed("BIC") {
focusElement.textContent
}
}
}
val pmtInfId = requireUniqueChildNamed("PmtInfId") { focusElement.textContent }
val txDetails = requireUniqueChildNamed("CdtTrfTxInf") {
object {
val creditorIban = requireUniqueChildNamed("CdtrAcct") {
requireUniqueChildNamed("Id") {
requireUniqueChildNamed("IBAN") { focusElement.textContent }
}
}
val creditorName = requireUniqueChildNamed("Cdtr") {
requireUniqueChildNamed("Nm") {
focusElement.textContent
}
}
val creditorBic = maybeUniqueChildNamed("CdtrAgt") {
requireUniqueChildNamed("FinInstnId") {
requireUniqueChildNamed("BIC") {
focusElement.textContent
}
}
}
val amt = requireUniqueChildNamed("Amt") {
requireOnlyChild { focusElement }
}
val subject = requireUniqueChildNamed("RmtInf") {
requireUniqueChildNamed("Ustrd") { focusElement.textContent }
}
}
}
PainParseResult(
currency = txDetails.amt.getAttribute("Ccy"),
amount = Amount(txDetails.amt.textContent),
subject = txDetails.subject,
debtorIban = debtorIban,
debtorName = debtorName,
debtorBic = debtorBic,
creditorName = txDetails.creditorName,
creditorIban = txDetails.creditorIban,
creditorBic = txDetails.creditorBic,
pmtInfId = pmtInfId,
msgId = msgId
)
}
}
}
}
}
/**
* Process a payment request in the pain.001 format.
*/
private fun handleCct(paymentRequest: String) {
logger.debug("Handling CCT")
logger.debug("Pain.001: $paymentRequest")
val parseResult = parsePain001(paymentRequest)
transaction {
try {
BankAccountTransactionEntity.new {
account = getBankAccountFromIban(parseResult.debtorIban)
creditorIban = parseResult.creditorIban
creditorName = parseResult.creditorName
creditorBic = parseResult.creditorBic
debtorIban = parseResult.debtorIban
debtorName = parseResult.debtorName
debtorBic = parseResult.debtorBic
subject = parseResult.subject
amount = parseResult.amount.toString()
currency = parseResult.currency
date = getUTCnow().toInstant().toEpochMilli()
pmtInfId = parseResult.pmtInfId
accountServicerReference = "sandboxref-${getRandomString(16)}"
direction = "DBIT"
}
val maybeLocalCreditor = BankAccountEntity.find(
BankAccountsTable.iban eq parseResult.creditorIban
).firstOrNull()
if (maybeLocalCreditor != null) {
BankAccountTransactionEntity.new {
account = maybeLocalCreditor
creditorIban = parseResult.creditorIban
creditorName = parseResult.creditorName
creditorBic = parseResult.creditorBic
debtorIban = parseResult.debtorIban
debtorName = parseResult.debtorName
debtorBic = parseResult.debtorBic
subject = parseResult.subject
amount = parseResult.amount.toString()
currency = parseResult.currency
date = getUTCnow().toInstant().toEpochMilli()
pmtInfId = parseResult.pmtInfId
accountServicerReference = "sandboxref-${getRandomString(16)}"
direction = "CRDT"
}
}
} catch (e: ExposedSQLException) {
logger.warn("Could not insert new payment into the database: ${e}")
throw EbicsRequestError(
"[EBICS_PROCESSING_ERROR] ${e.sqlState}",
"091116"
)
}
}
}
/**
* This handler reports all the fresh transactions, belonging
* to the querying subscriber.
*/
private fun handleEbicsC52(requestContext: RequestContext): ByteArray {
logger.debug("Handling C52 request")
// Ignoring any dateRange parameter. (FIXME: clarify whether that is fine.)
val report = constructCamtResponse(52, requestContext.subscriber, dateRange = null)
SandboxAssert(
report.size == 1,
"C52 response does not contain one Camt.052 document"
)
if (!XMLUtil.validateFromString(report[0])) throw EbicsProcessingError(
"One statement was found invalid."
)
return report.map { it.toByteArray() }.zip()
}
private fun handleEbicsC53(requestContext: RequestContext): ByteArray {
logger.debug("Handling C53 request")
// Fetch date range.
val orderParams = requestContext.requestObject.header.static.orderDetails?.orderParams // as EbicsRequest.StandardOrderParams
val dateRange = if (orderParams != null) {
val standardOrderParams = orderParams as EbicsRequest.StandardOrderParams
val start = standardOrderParams.dateRange?.start?.toGregorianCalendar()?.timeInMillis
val end = standardOrderParams.dateRange?.end?.toGregorianCalendar()?.timeInMillis
if (start == null || end == null) {
// only accepting when both start/end are given.
null
} else {
Pair(start, end)
}
} else {
null
}
/**
* By multiple statements, this function is responsible to return
* a list of Strings: one for each statement.
*/
val camtStatements = constructCamtResponse(
53,
requestContext.subscriber,
dateRange
)
camtStatements.forEach {
if (!XMLUtil.validateFromString(it)) throw EbicsProcessingError(
"One statement was found invalid."
)
}
return camtStatements.map { it.toByteArray() }.zip()
}
private suspend fun ApplicationCall.handleEbicsHia(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) {
val plainOrderData = InflaterInputStream(orderData.inputStream()).use {
it.readAllBytes()
}
println("HIA order data: ${plainOrderData.toString(Charsets.UTF_8)}")
val keyObject = EbicsOrderUtil.decodeOrderDataXml(orderData)
val encPubXml = keyObject.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue
val authPubXml = keyObject.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue
val encPub = CryptoUtil.loadRsaPublicKeyFromComponents(encPubXml.modulus, encPubXml.exponent)
val authPub = CryptoUtil.loadRsaPublicKeyFromComponents(authPubXml.modulus, authPubXml.exponent)
val ok = transaction {
val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID)
if (ebicsSubscriber == null) {
logger.warn("ebics subscriber not found")
throw EbicsInvalidRequestError()
}
when (ebicsSubscriber.state) {
SubscriberState.NEW -> {}
SubscriberState.PARTIALLY_INITIALIZED_INI -> {}
SubscriberState.PARTIALLY_INITIALIZED_HIA, SubscriberState.INITIALIZED, SubscriberState.READY -> {
return@transaction false
}
}
ebicsSubscriber.authenticationKey = EbicsSubscriberPublicKeyEntity.new {
this.rsaPublicKey = ExposedBlob(authPub.encoded)
state = KeyState.NEW
}
ebicsSubscriber.encryptionKey = EbicsSubscriberPublicKeyEntity.new {
this.rsaPublicKey = ExposedBlob(encPub.encoded)
state = KeyState.NEW
}
ebicsSubscriber.state = when (ebicsSubscriber.state) {
SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_HIA
SubscriberState.PARTIALLY_INITIALIZED_INI -> SubscriberState.INITIALIZED
else -> throw Exception("internal invariant failed")
}
return@transaction true
}
if (ok) {
respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000")
} else {
respondEbicsKeyManagement("[EBICS_INVALID_USER_OR_USER_STATE]", "091002", "000000")
}
}
private suspend fun ApplicationCall.handleEbicsIni(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) {
val plainOrderData = InflaterInputStream(orderData.inputStream()).use {
it.readAllBytes()
}
println("INI order data: ${plainOrderData.toString(Charsets.UTF_8)}")
val keyObject = EbicsOrderUtil.decodeOrderDataXml(orderData)
val sigPubXml = keyObject.signaturePubKeyInfo.pubKeyValue.rsaKeyValue
val sigPub = CryptoUtil.loadRsaPublicKeyFromComponents(sigPubXml.modulus, sigPubXml.exponent)
val ok = transaction {
val ebicsSubscriber =
findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID)
if (ebicsSubscriber == null) {
logger.warn("ebics subscriber ('${header.static.partnerID}' / '${header.static.userID}' / '${header.static.systemID}') not found")
throw EbicsInvalidRequestError()
}
when (ebicsSubscriber.state) {
SubscriberState.NEW -> {}
SubscriberState.PARTIALLY_INITIALIZED_HIA -> {}
SubscriberState.PARTIALLY_INITIALIZED_INI, SubscriberState.INITIALIZED, SubscriberState.READY -> {
return@transaction false
}
}
ebicsSubscriber.signatureKey = EbicsSubscriberPublicKeyEntity.new {
this.rsaPublicKey = ExposedBlob(sigPub.encoded)
state = KeyState.NEW
}
ebicsSubscriber.state = when (ebicsSubscriber.state) {
SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_INI
SubscriberState.PARTIALLY_INITIALIZED_HIA -> SubscriberState.INITIALIZED
else -> throw Error("internal invariant failed")
}
return@transaction true
}
logger.info("Signature key inserted in database _and_ subscriber state changed accordingly")
if (ok) {
respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000")
} else {
respondEbicsKeyManagement("[EBICS_INVALID_USER_OR_USER_STATE]", "091002", "000000")
}
}
private suspend fun ApplicationCall.handleEbicsHpb(
ebicsHostInfo: EbicsHostPublicInfo,
requestDocument: Document,
header: EbicsNpkdRequest.Header
) {
val subscriberKeys = transaction {
val ebicsSubscriber =
findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID)
if (ebicsSubscriber == null) {
throw EbicsInvalidRequestError()
}
if (ebicsSubscriber.state != SubscriberState.INITIALIZED) {
throw EbicsSubscriberStateError()
}
val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey
val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey
val sigPubBlob = ebicsSubscriber.signatureKey!!.rsaPublicKey
SubscriberKeys(
CryptoUtil.loadRsaPublicKey(authPubBlob.bytes),
CryptoUtil.loadRsaPublicKey(encPubBlob.bytes),
CryptoUtil.loadRsaPublicKey(sigPubBlob.bytes)
)
}
val validationResult =
XMLUtil.verifyEbicsDocument(requestDocument, subscriberKeys.authenticationPublicKey)
logger.info("validationResult: $validationResult")
if (!validationResult) {
throw EbicsKeyManagementError("invalid signature", "90000")
}
val hpbRespondeData = HPBResponseOrderData().apply {
this.authenticationPubKeyInfo = EbicsTypes.AuthenticationPubKeyInfoType().apply {
this.authenticationVersion = "X002"
this.pubKeyValue = EbicsTypes.PubKeyValueType().apply {
this.rsaKeyValue = RSAKeyValueType().apply {
this.exponent = ebicsHostInfo.authenticationPublicKey.publicExponent.toByteArray()
this.modulus = ebicsHostInfo.authenticationPublicKey.modulus.toByteArray()
}
}
}
this.encryptionPubKeyInfo = EbicsTypes.EncryptionPubKeyInfoType().apply {
this.encryptionVersion = "E002"
this.pubKeyValue = EbicsTypes.PubKeyValueType().apply {
this.rsaKeyValue = RSAKeyValueType().apply {
this.exponent = ebicsHostInfo.encryptionPublicKey.publicExponent.toByteArray()
this.modulus = ebicsHostInfo.encryptionPublicKey.modulus.toByteArray()
}
}
}
this.hostID = ebicsHostInfo.hostID
}
val compressedOrderData = EbicsOrderUtil.encodeOrderDataXml(hpbRespondeData)
val encryptionResult = CryptoUtil.encryptEbicsE002(compressedOrderData, subscriberKeys.encryptionPublicKey)
respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000", encryptionResult, "OR01")
}
/**
* Find the ebics host corresponding to the one specified in the header.
*/
private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo {
return transaction {
val ebicsHost =
EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestHostID.uppercase(Locale.getDefault()) }.firstOrNull()
if (ebicsHost == null) {
logger.warn("client requested unknown HostID ${requestHostID}")
throw EbicsKeyManagementError("[EBICS_INVALID_HOST_ID]", "091011")
}
val encryptionPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.encryptionPrivateKey.bytes)
val authenticationPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.authenticationPrivateKey.bytes)
EbicsHostPublicInfo(
requestHostID,
CryptoUtil.getRsaPublicFromPrivate(encryptionPrivateKey),
CryptoUtil.getRsaPublicFromPrivate(authenticationPrivateKey)
)
}
}
private suspend fun ApplicationCall.receiveEbicsXml(): Document {
val body: String = receiveText()
logger.debug("Data received: $body")
val requestDocument: Document? = XMLUtil.parseStringIntoDom(body)
if (requestDocument == null || (!XMLUtil.validateFromDom(requestDocument))) {
println("Problematic document was: $requestDocument")
throw EbicsInvalidXmlError()
}
val requestedHostID = requestDocument.getElementsByTagName("HostID")
this.attributes.put(
EbicsHostIdAttribute,
requestedHostID.item(0).textContent
)
return requestDocument
}
private fun makePartnerInfo(subscriber: EbicsSubscriberEntity): EbicsTypes.PartnerInfo {
val bankAccount = getBankAccountFromSubscriber(subscriber)
return EbicsTypes.PartnerInfo().apply {
this.accountInfoList = listOf(
EbicsTypes.AccountInfo().apply {
this.id = bankAccount.label
this.accountHolder = bankAccount.name
this.accountNumberList = listOf(
EbicsTypes.GeneralAccountNumber().apply {
this.international = true
this.value = bankAccount.iban
}
)
this.currency = "EUR"
this.description = "Ordinary Bank Account"
this.bankCodeList = listOf(
EbicsTypes.GeneralBankCode().apply {
this.international = true
this.value = bankAccount.bic
}
)
}
)
this.addressInfo = EbicsTypes.AddressInfo().apply {
this.name = "Address Info Object"
}
this.bankInfo = EbicsTypes.BankInfo().apply {
this.hostID = subscriber.hostId
}
this.orderInfoList = listOf(
EbicsTypes.AuthOrderInfoType().apply {
this.description = "Transactions statement"
this.orderType = "C53"
this.transferType = "Download"
},
EbicsTypes.AuthOrderInfoType().apply {
this.description = "Transactions report"
this.orderType = "C52"
this.transferType = "Download"
},
EbicsTypes.AuthOrderInfoType().apply {
this.description = "Payment initiation (ZIPped payload)"
this.orderType = "CCC"
this.transferType = "Upload"
},
EbicsTypes.AuthOrderInfoType().apply {
this.description = "Payment initiation (plain text payload)"
this.orderType = "CCT"
this.transferType = "Upload"
},
EbicsTypes.AuthOrderInfoType().apply {
this.description = "vmk"
this.orderType = "VMK"
this.transferType = "Download"
},
EbicsTypes.AuthOrderInfoType().apply {
this.description = "sta"
this.orderType = "STA"
this.transferType = "Download"
}
)
}
}
private fun handleEbicsHtd(requestContext: RequestContext): ByteArray {
val htd = HTDResponseOrderData().apply {
this.partnerInfo = makePartnerInfo(requestContext.subscriber)
this.userInfo = EbicsTypes.UserInfo().apply {
this.name = "Some User"
this.userID = EbicsTypes.UserIDType().apply {
this.status = 5
this.value = requestContext.subscriber.userId
}
this.permissionList = listOf(
EbicsTypes.UserPermission().apply {
this.orderTypes = "C53 C52 CCC VMK STA"
}
)
}
}
val str = XMLUtil.convertJaxbToString(htd)
return str.toByteArray()
}
private fun handleEbicsHkd(requestContext: RequestContext): ByteArray {
val hkd = HKDResponseOrderData().apply {
this.partnerInfo = makePartnerInfo(requestContext.subscriber)
this.userInfoList = listOf(
EbicsTypes.UserInfo().apply {
this.name = "Some User"
this.userID = EbicsTypes.UserIDType().apply {
this.status = 1
this.value = requestContext.subscriber.userId
}
this.permissionList = listOf(
EbicsTypes.UserPermission().apply {
this.orderTypes = "C54 C53 C52 CCC"
}
)
})
}
val str = XMLUtil.convertJaxbToString(hkd)
return str.toByteArray()
}
private data class RequestContext(
val ebicsHost: EbicsHostEntity,
val subscriber: EbicsSubscriberEntity,
val clientEncPub: RSAPublicKey,
val clientAuthPub: RSAPublicKey,
val clientSigPub: RSAPublicKey,
val hostEncPriv: RSAPrivateCrtKey,
val hostAuthPriv: RSAPrivateCrtKey,
val requestObject: EbicsRequest,
val uploadTransaction: EbicsUploadTransactionEntity?,
val downloadTransaction: EbicsDownloadTransactionEntity?
)
private fun handleEbicsDownloadTransactionTransfer(requestContext: RequestContext): EbicsResponse {
val segmentNumber =
requestContext.requestObject.header.mutable.segmentNumber?.value ?: throw EbicsInvalidRequestError()
val transactionID = requestContext.requestObject.header.static.transactionID ?: throw EbicsInvalidRequestError()
val downloadTransaction = requestContext.downloadTransaction ?: throw AssertionError()
return EbicsResponse.createForDownloadTransferPhase(
transactionID,
downloadTransaction.numSegments,
downloadTransaction.segmentSize,
downloadTransaction.encodedResponse,
segmentNumber.toInt()
)
}
private fun handleEbicsDownloadTransactionInitialization(requestContext: RequestContext): EbicsResponse {
val orderType =
requestContext.requestObject.header.static.orderDetails?.orderType ?: throw EbicsInvalidRequestError()
logger.debug("handling initialization for order type $orderType")
val response = when (orderType) {
"HTD" -> handleEbicsHtd(requestContext)
"HKD" -> handleEbicsHkd(requestContext)
"C53" -> handleEbicsC53(requestContext)
"C52" -> handleEbicsC52(requestContext)
"TSD" -> handleEbicsTSD()
"PTK" -> handleEbicsPTK()
else -> throw EbicsInvalidXmlError()
}
val transactionID = EbicsOrderUtil.generateTransactionId()
val compressedResponse = DeflaterInputStream(response.inputStream()).use {
it.readAllBytes()
}
val enc = CryptoUtil.encryptEbicsE002(compressedResponse, requestContext.clientEncPub)
val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData)
val segmentSize = 4096
val totalSize = encodedResponse.length
val numSegments = ((totalSize + segmentSize - 1) / segmentSize)
EbicsDownloadTransactionEntity.new(transactionID) {
this.subscriber = requestContext.subscriber
this.host = requestContext.ebicsHost
this.orderType = orderType
this.segmentSize = segmentSize
this.transactionKeyEnc = ExposedBlob(enc.encryptedTransactionKey)
this.encodedResponse = encodedResponse
this.numSegments = numSegments
this.receiptReceived = false
}
return EbicsResponse.createForDownloadInitializationPhase(
transactionID,
numSegments,
segmentSize,
enc,
encodedResponse
)
}
private fun handleEbicsUploadTransactionInitialization(requestContext: RequestContext): EbicsResponse {
val orderType =
requestContext.requestObject.header.static.orderDetails?.orderType ?: throw EbicsInvalidRequestError()
val transactionID = EbicsOrderUtil.generateTransactionId()
val oidn = requestContext.subscriber.nextOrderID++
if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError()
val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn)
val numSegments =
requestContext.requestObject.header.static.numSegments ?: throw EbicsInvalidRequestError()
val transactionKeyEnc =
requestContext.requestObject.body.dataTransfer?.dataEncryptionInfo?.transactionKey
?: throw EbicsInvalidRequestError()
val encPubKeyDigest =
requestContext.requestObject.body.dataTransfer?.dataEncryptionInfo?.encryptionPubKeyDigest?.value
?: throw EbicsInvalidRequestError()
val encSigData = requestContext.requestObject.body.dataTransfer?.signatureData?.value
?: throw EbicsInvalidRequestError()
val decryptedSignatureData = CryptoUtil.decryptEbicsE002(
CryptoUtil.EncryptionResult(
transactionKeyEnc,
encPubKeyDigest,
encSigData
), requestContext.hostEncPriv
)
val plainSigData = InflaterInputStream(decryptedSignatureData.inputStream()).use {
it.readAllBytes()
}
logger.debug("creating upload transaction for transactionID $transactionID")
EbicsUploadTransactionEntity.new(transactionID) {
this.host = requestContext.ebicsHost
this.subscriber = requestContext.subscriber
this.lastSeenSegment = 0
this.orderType = orderType
this.orderID = orderID
this.numSegments = numSegments.toInt()
this.transactionKeyEnc = ExposedBlob(transactionKeyEnc)
}
logger.debug("after SQL flush")
val sigObj = XMLUtil.convertStringToJaxb(plainSigData.toString(Charsets.UTF_8))
logger.debug("got UserSignatureData: ${plainSigData.toString(Charsets.UTF_8)}")
for (sig in sigObj.value.orderSignatureList ?: listOf()) {
logger.debug("inserting order signature for orderID $orderID and orderType $orderType")
EbicsOrderSignatureEntity.new {
this.orderID = orderID
this.orderType = orderType
this.partnerID = sig.partnerID
this.userID = sig.userID
this.signatureAlgorithm = sig.signatureVersion
this.signatureValue = ExposedBlob(sig.signatureValue)
}
}
return EbicsResponse.createForUploadInitializationPhase(transactionID, orderID)
}
private fun handleEbicsUploadTransactionTransmission(requestContext: RequestContext): EbicsResponse {
val uploadTransaction = requestContext.uploadTransaction ?: throw EbicsInvalidRequestError()
val requestObject = requestContext.requestObject
val requestSegmentNumber =
requestContext.requestObject.header.mutable.segmentNumber?.value?.toInt() ?: throw EbicsInvalidRequestError()
val requestTransactionID = requestObject.header.static.transactionID ?: throw EbicsInvalidRequestError()
if (requestSegmentNumber == 1 && uploadTransaction.numSegments == 1) {
val encOrderData =
requestObject.body.dataTransfer?.orderData ?: throw EbicsInvalidRequestError()
val zippedData = CryptoUtil.decryptEbicsE002(
uploadTransaction.transactionKeyEnc.bytes,
Base64.getDecoder().decode(encOrderData),
requestContext.hostEncPriv
)
val unzippedData =
InflaterInputStream(zippedData.inputStream()).use { it.readAllBytes() }
logger.debug("got upload data: ${unzippedData.toString(Charsets.UTF_8)}")
val sigs = EbicsOrderSignatureEntity.find {
(EbicsOrderSignaturesTable.orderID eq uploadTransaction.orderID) and
(EbicsOrderSignaturesTable.orderType eq uploadTransaction.orderType)
}
if (sigs.count() == 0L) {
throw EbicsInvalidRequestError()
}
for (sig in sigs) {
if (sig.signatureAlgorithm == "A006") {
val signedData = CryptoUtil.digestEbicsOrderA006(unzippedData)
val res1 = CryptoUtil.verifyEbicsA006(
sig.signatureValue.bytes,
signedData,
requestContext.clientSigPub
)
if (!res1) {
throw EbicsInvalidRequestError()
}
} else {
throw NotImplementedError()
}
}
if (getOrderTypeFromTransactionId(requestTransactionID) == "CCT") {
logger.debug("Attempting a payment.")
handleCct(unzippedData.toString(Charsets.UTF_8))
}
return EbicsResponse.createForUploadTransferPhase(
requestTransactionID,
requestSegmentNumber,
true,
uploadTransaction.orderID
)
} else {
throw NotImplementedError()
}
}
// req.header.static.hostID.
private fun makeRequestContext(requestObject: EbicsRequest): RequestContext {
val staticHeader = requestObject.header.static
val requestedHostId = staticHeader.hostID
val ebicsHost =
EbicsHostEntity.find { EbicsHostsTable.hostID.upperCase() eq requestedHostId.uppercase(Locale.getDefault()) }
.firstOrNull()
val requestTransactionID = requestObject.header.static.transactionID
var downloadTransaction: EbicsDownloadTransactionEntity? = null
var uploadTransaction: EbicsUploadTransactionEntity? = null
val subscriber = if (requestTransactionID != null) {
println("finding subscriber by transactionID $requestTransactionID")
downloadTransaction = EbicsDownloadTransactionEntity.findById(requestTransactionID.uppercase(Locale.getDefault()))
if (downloadTransaction != null) {
downloadTransaction.subscriber
} else {
uploadTransaction = EbicsUploadTransactionEntity.findById(requestTransactionID)
uploadTransaction?.subscriber
}
} else {
val partnerID = staticHeader.partnerID ?: throw EbicsInvalidRequestError()
val userID = staticHeader.userID ?: throw EbicsInvalidRequestError()
findEbicsSubscriber(partnerID, userID, staticHeader.systemID)
}
if (ebicsHost == null) throw EbicsInvalidRequestError()
/**
* NOTE: production logic must check against READY state (the
* one activated after the subscriber confirms their keys via post)
*/
if (subscriber == null || subscriber.state != SubscriberState.INITIALIZED)
throw EbicsSubscriberStateError()
val hostAuthPriv = CryptoUtil.loadRsaPrivateKey(
ebicsHost.authenticationPrivateKey.bytes
)
val hostEncPriv = CryptoUtil.loadRsaPrivateKey(
ebicsHost.encryptionPrivateKey.bytes
)
val clientAuthPub =
CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.bytes)
val clientEncPub =
CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.bytes)
val clientSigPub =
CryptoUtil.loadRsaPublicKey(subscriber.signatureKey!!.rsaPublicKey.bytes)
return RequestContext(
hostAuthPriv = hostAuthPriv,
hostEncPriv = hostEncPriv,
clientAuthPub = clientAuthPub,
clientEncPub = clientEncPub,
clientSigPub = clientSigPub,
ebicsHost = ebicsHost,
requestObject = requestObject,
subscriber = subscriber,
downloadTransaction = downloadTransaction,
uploadTransaction = uploadTransaction
)
}
suspend fun ApplicationCall.ebicsweb() {
val requestDocument = receiveEbicsXml()
logger.info("Processing ${requestDocument.documentElement.localName}")
when (requestDocument.documentElement.localName) {
"ebicsUnsecuredRequest" -> {
val requestObject = requestDocument.toObject()
logger.info("Serving a ${requestObject.header.static.orderDetails.orderType} request")
val orderData = requestObject.body.dataTransfer.orderData.value
val header = requestObject.header
when (header.static.orderDetails.orderType) {
"INI" -> handleEbicsIni(header, orderData)
"HIA" -> handleEbicsHia(header, orderData)
else -> throw EbicsInvalidXmlError()
}
}
"ebicsHEVRequest" -> {
val hevResponse = HEVResponse().apply {
this.systemReturnCode = SystemReturnCodeType().apply {
this.reportText = "[EBICS_OK]"
this.returnCode = "000000"
}
this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50"))
}
val strResp = XMLUtil.convertJaxbToString(hevResponse)
logger.debug("HEV response: $strResp")
if (!XMLUtil.validateFromString(strResp)) throw SandboxError(
HttpStatusCode.InternalServerError,
"Outgoing HEV response is invalid"
)
respondText(strResp, ContentType.Application.Xml, HttpStatusCode.OK)
}
"ebicsNoPubKeyDigestsRequest" -> {
val requestObject = requestDocument.toObject()
val hostInfo = ensureEbicsHost(requestObject.header.static.hostID)
when (requestObject.header.static.orderDetails.orderType) {
"HPB" -> handleEbicsHpb(hostInfo, requestDocument, requestObject.header)
else -> throw EbicsInvalidXmlError()
}
}
"ebicsRequest" -> {
logger.debug("ebicsRequest ${XMLUtil.convertDomToString(requestDocument)}")
val requestObject = requestDocument.toObject()
val responseXmlStr = transaction {
// Step 1 of 3: Get information about the host and subscriber
val requestContext = makeRequestContext(requestObject)
// Step 2 of 3: Validate the signature
val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, requestContext.clientAuthPub)
if (!verifyResult) {
throw EbicsAccountAuthorisationFailed()
}
// Step 3 of 3: Generate response
val ebicsResponse: EbicsResponse = when (requestObject.header.mutable.transactionPhase) {
EbicsTypes.TransactionPhaseType.INITIALISATION -> {
if (requestObject.header.static.numSegments == null) {
handleEbicsDownloadTransactionInitialization(requestContext)
} else {
handleEbicsUploadTransactionInitialization(requestContext)
}
}
EbicsTypes.TransactionPhaseType.TRANSFER -> {
if (requestContext.uploadTransaction != null) {
handleEbicsUploadTransactionTransmission(requestContext)
} else if (requestContext.downloadTransaction != null) {
handleEbicsDownloadTransactionTransfer(requestContext)
} else {
throw AssertionError()
}
}
EbicsTypes.TransactionPhaseType.RECEIPT -> {
val requestTransactionID =
requestObject.header.static.transactionID ?: throw EbicsInvalidRequestError()
if (requestContext.downloadTransaction == null)
throw EbicsInvalidRequestError()
val receiptCode =
requestObject.body.transferReceipt?.receiptCode ?: throw EbicsInvalidRequestError()
EbicsResponse.createForDownloadReceiptPhase(requestTransactionID, receiptCode == 0)
}
}
signEbicsResponse(ebicsResponse, requestContext.hostAuthPriv)
}
if (!XMLUtil.validateFromString(responseXmlStr)) throw SandboxError(
HttpStatusCode.InternalServerError,
"Outgoing EBICS XML is invalid"
)
respondText(responseXmlStr, ContentType.Application.Xml, HttpStatusCode.OK)
}
else -> {
/* Log to console and return "unknown type" */
logger.info("Unknown message, just logging it!")
respond(
HttpStatusCode.NotImplemented,
SandboxError(
HttpStatusCode.NotImplemented,
"Not Implemented"
)
)
}
}
}