summaryrefslogtreecommitdiff
path: root/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
diff options
context:
space:
mode:
Diffstat (limited to 'nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt623
1 files changed, 191 insertions, 432 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
index 9144997e..79fcb30b 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -32,11 +32,6 @@
package tech.libeufin.nexus.ebics
-import com.itextpdf.kernel.pdf.PdfDocument
-import com.itextpdf.kernel.pdf.PdfWriter
-import com.itextpdf.layout.Document
-import com.itextpdf.layout.element.AreaBreak
-import com.itextpdf.layout.element.Paragraph
import io.ktor.client.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
@@ -55,6 +50,8 @@ import java.time.format.DateTimeFormatter
import java.util.*
import kotlinx.coroutines.*
import java.security.SecureRandom
+import org.w3c.dom.Document
+import org.xml.sax.SAXException
/**
* Available EBICS versions.
@@ -98,6 +95,13 @@ fun decryptAndDecompressPayload(
)
}.inflate()
+sealed class EbicsError(msg: String, cause: Throwable? = null): Exception(msg, cause) {
+ /** Http and network errors */
+ class Transport(msg: String, cause: Throwable? = null): EbicsError(msg, cause)
+ /** EBICS protocol & XML format error */
+ class Protocol(msg: String, cause: Throwable? = null): EbicsError(msg, cause)
+}
+
/**
* POSTs the EBICS message to the bank.
*
@@ -105,100 +109,26 @@ fun decryptAndDecompressPayload(
* @param msg EBICS message as raw bytes.
* @return the raw bank response.
*/
-suspend fun HttpClient.postToBank(bankUrl: String, msg: ByteArray): InputStream {
- logger.debug("POSTing EBICS to '$bankUrl'")
- val res = post(urlString = bankUrl) {
- contentType(ContentType.Text.Xml)
- setBody(msg)
- }
- if (res.status != HttpStatusCode.OK) {
- println(res.bodyAsText())
- throw Exception("Invalid response status: ${res.status}")
- }
- return res.bodyAsChannel().toInputStream()
-}
-
-/**
- * Generate the PDF document with all the client public keys
- * to be sent on paper to the bank.
- */
-fun generateKeysPdf(
- clientKeys: ClientPrivateKeysFile,
- cfg: EbicsSetupConfig
-): ByteArray {
- val po = ByteArrayOutputStream()
- val pdfWriter = PdfWriter(po)
- val pdfDoc = PdfDocument(pdfWriter)
- val date = LocalDateTime.now()
- val dateStr = date.format(DateTimeFormatter.ISO_LOCAL_DATE)
-
- fun formatHex(ba: ByteArray): String {
- var out = ""
- for (i in ba.indices) {
- val b = ba[i]
- if (i > 0 && i % 16 == 0) {
- out += "\n"
- }
- out += java.lang.String.format("%02X", b)
- out += " "
+suspend fun HttpClient.postToBank(bankUrl: String, msg: ByteArray): Document {
+ val res = try {
+ post(urlString = bankUrl) {
+ contentType(ContentType.Text.Xml)
+ setBody(msg)
}
- return out
- }
-
- fun writeCommon(doc: Document) {
- doc.add(
- Paragraph(
- """
- Datum: $dateStr
- Host-ID: ${cfg.ebicsHostId}
- User-ID: ${cfg.ebicsUserId}
- Partner-ID: ${cfg.ebicsPartnerId}
- 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)}"))
+ } catch (e: Exception) {
+ throw EbicsError.Transport("failed to contact bank", e)
}
-
- fun writeSigLine(doc: Document) {
- doc.add(Paragraph("Ort / Datum: ________________"))
- doc.add(Paragraph("Firma / Name: ________________"))
- doc.add(Paragraph("Unterschrift: ________________"))
+
+ if (res.status != HttpStatusCode.OK) {
+ throw EbicsError.Transport("bank HTTP error: ${res.status}")
}
-
- 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, clientKeys.signature_private_key)
- 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, clientKeys.authentication_private_key)
- 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, clientKeys.encryption_private_key)
- it.add(Paragraph("\n"))
- writeSigLine(it)
+ try {
+ return XMLUtil.parseIntoDom(res.bodyAsChannel().toInputStream())
+ } catch (e: SAXException) {
+ throw EbicsError.Protocol("invalid XML bank reponse", e)
+ } catch (e: Exception) {
+ throw EbicsError.Transport("failed read bank response", e)
}
- pdfWriter.flush()
- return po.toByteArray()
}
/**
@@ -216,52 +146,23 @@ suspend fun postEbics(
cfg: EbicsSetupConfig,
bankKeys: BankPublicKeysFile,
xmlReq: ByteArray
-): EbicsResponseContent {
- val respXml = try {
- client.postToBank(cfg.hostBaseUrl, xmlReq)
- } catch (e: Exception) {
- throw EbicsSideException(
- "POSTing to ${cfg.hostBaseUrl} failed",
- sideEc = EbicsSideError.HTTP_POST_FAILED,
- e
- )
- }
-
- // Parses the bank response from the raw XML and verifies
- // the bank signature.
- val doc = try {
- XMLUtil.parseIntoDom(respXml)
- } catch (e: Exception) {
- throw EbicsSideException(
- "Bank response apparently invalid",
- sideEc = EbicsSideError.BANK_RESPONSE_IS_INVALID
- )
- }
+): EbicsResponse {
+ val doc = client.postToBank(cfg.hostBaseUrl, xmlReq)
if (!XMLUtil.verifyEbicsDocument(
doc,
bankKeys.bank_authentication_public_key,
true
)) {
- throw EbicsSideException(
- "Bank signature did not verify",
- sideEc = EbicsSideError.BANK_SIGNATURE_DIDNT_VERIFY
- )
+ throw EbicsError.Protocol("bank signature did not verify")
+ }
+ try {
+ return Ebics3BTS.parseResponse(doc)
+ } catch (e: Exception) {
+ throw EbicsError.Protocol("invalid ebics response", e)
}
-
- return parseEbics3Response(doc)
}
/**
- * Checks that EBICS- and bank-technical return codes are both EBICS_OK.
- *
- * @param ebicsResponseContent valid response gotten from the bank.
- * @return true only if both codes are EBICS_OK.
- */
-private fun areCodesOk(ebicsResponseContent: EbicsResponseContent) =
- ebicsResponseContent.technicalReturnCode == EbicsReturnCode.EBICS_OK &&
- ebicsResponseContent.bankReturnCode == EbicsReturnCode.EBICS_OK
-
-/**
* Perform an EBICS download transaction.
*
* It conducts init -> transfer -> processing -> receipt phases.
@@ -283,138 +184,96 @@ suspend fun ebicsDownload(
reqXml: ByteArray,
processing: (InputStream) -> Unit
) = coroutineScope {
- val impl = Ebics3Impl(
- cfg,
- bankKeys,
- clientKeys
- )
- val scope = this
+ val impl = Ebics3BTS(cfg, bankKeys, clientKeys)
+ val parentScope = this
+
// We need to run the logic in a non-cancelable context because we need to send
// a receipt for each open download transaction, otherwise we'll be stuck in an
// error loop until the pending transaction timeout.
// TODO find a way to cancel the pending transaction ?
- withContext(NonCancellable) {
- val initResp = postEbics(client, cfg, bankKeys, reqXml)
- logger.debug("Download init phase done. EBICS- and bank-technical codes are: ${initResp.technicalReturnCode}, ${initResp.bankReturnCode}")
- if (initResp.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
- throw Exception("Download init phase has EBICS-technical error: ${initResp.technicalReturnCode}")
+ withContext(NonCancellable) {
+ // Init phase
+ val initResp = postEbics(client, cfg, bankKeys, reqXml)
+ if (initResp.bankCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
+ logger.debug("Download content is empty")
+ return@withContext
}
- if (initResp.bankReturnCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
- logger.debug("Download content is empty")
- return@withContext
- } else if (initResp.bankReturnCode != EbicsReturnCode.EBICS_OK) {
- throw Exception("Download init phase has bank-technical error: ${initResp.bankReturnCode}")
+ val initContent = initResp.okOrFail("Download init phase")
+ val tId = requireNotNull(initContent.transactionID) {
+ "Download init phase: missing transaction ID"
}
- val tId = initResp.transactionID
- ?: throw EbicsSideException(
- "EBICS download init phase did not return a transaction ID, cannot do the transfer phase.",
- sideEc = EbicsSideError.EBICS_UPLOAD_TRANSACTION_ID_MISSING
- )
- logger.debug("EBICS download transaction passed the init phase, got ID: $tId")
- val howManySegments = initResp.numSegments
- if (howManySegments == null) {
- throw Exception("Init response lacks the quantity of segments, failing.")
+ val howManySegments = requireNotNull(initContent.numSegments) {
+ "Download init phase: missing num segments"
}
- val ebicsChunks = mutableListOf<String>()
- // Getting the chunk(s)
- val firstDataChunk = initResp.orderDataEncChunk
- ?: throw EbicsSideException(
- "OrderData element not found, despite non empty payload, failing.",
- sideEc = EbicsSideError.ORDER_DATA_ELEMENT_NOT_FOUND
- )
- val dataEncryptionInfo = initResp.dataEncryptionInfo ?: run {
- throw EbicsSideException(
- "EncryptionInfo element not found, despite non empty payload, failing.",
- sideEc = EbicsSideError.ENCRYPTION_INFO_ELEMENT_NOT_FOUND
- )
+ val firstDataChunk = requireNotNull(initContent.payloadChunk) {
+ "Download init phase: missing OrderData"
}
- ebicsChunks.add(firstDataChunk)
- // proceed with the transfer phase.
- for (x in 2 .. howManySegments) {
- if (!scope.isActive) break
- // request segment number x.
- val transReq = impl.downloadTransfer(x, howManySegments, tId)
-
- val transResp = postEbics(client, cfg, bankKeys, transReq)
- if (!areCodesOk(transResp)) {
- throw EbicsSideException(
- "EBICS transfer segment #$x failed.",
- sideEc = EbicsSideError.TRANSFER_SEGMENT_FAILED
- )
- }
- val chunk = transResp.orderDataEncChunk
- if (chunk == null) {
- throw Exception("EBICS transfer phase lacks chunk #$x, failing.")
- }
- ebicsChunks.add(chunk)
+ val dataEncryptionInfo = requireNotNull(initContent.dataEncryptionInfo) {
+ "Download init phase: missing EncryptionInfo"
}
- suspend fun receipt(success: Boolean) {
- val receiptXml = impl.downloadReceipt(tId, success)
- // Sending the receipt to the bank.
+ logger.debug("Download init phase for transaction '$tId'")
+
+ /** Send download receipt */
+ suspend fun receipt(success: Boolean) {
postEbics(
client,
cfg,
bankKeys,
- receiptXml
- )
+ impl.downloadReceipt(tId, success)
+ ).okOrFail("Download receipt phase")
+ }
+ /** Throw if parent scope have been canceled */
+ suspend fun checkCancellation() {
+ if (!parentScope.isActive) {
+ // First send a proper EBICS transaction failure
+ receipt(false)
+ // Send throw cancelation exception
+ throw CancellationException()
+ }
+ }
+
+ // Transfer phase
+ val ebicsChunks = mutableListOf(firstDataChunk)
+ for (x in 2 .. howManySegments) {
+ checkCancellation()
+ val transReq = impl.downloadTransfer(x, howManySegments, tId)
+ val transResp = postEbics(client, cfg, bankKeys, transReq).okOrFail("Download transfer phase")
+ val chunk = requireNotNull(transResp.payloadChunk) {
+ "Download transfer phase: missing encrypted chunk"
+ }
+ ebicsChunks.add(chunk)
}
- if (scope.isActive) {
- // all chunks gotten, shaping a meaningful response now.
- val payloadBytes = decryptAndDecompressPayload(
+
+ checkCancellation()
+
+ // Decompress encrypted chunks
+ val payloadBytes = try {
+ decryptAndDecompressPayload(
clientKeys.encryption_private_key,
dataEncryptionInfo,
ebicsChunks
)
- // Process payload
- val res = runCatching {
- processing(payloadBytes)
- }
- receipt(res.isSuccess)
+ } catch (e: Exception) {
+ throw EbicsError.Protocol("invalid chunks", e)
+ }
- res.getOrThrow()
- } else {
- receipt(false)
- throw CancellationException()
+ checkCancellation()
+
+ // Run business logic
+ val res = runCatching {
+ processing(payloadBytes)
}
+
+ // First send a proper EBICS transaction receipt
+ receipt(res.isSuccess)
+ // Then throw business logic exception if any
+ res.getOrThrow()
}
Unit
}
/**
- * These errors affect an EBICS transaction regardless
- * of the standard error codes.
- */
-enum class EbicsSideError {
- BANK_SIGNATURE_DIDNT_VERIFY,
- BANK_RESPONSE_IS_INVALID,
- ENCRYPTION_INFO_ELEMENT_NOT_FOUND,
- ORDER_DATA_ELEMENT_NOT_FOUND,
- TRANSFER_SEGMENT_FAILED,
- /**
- * This might indicate that the EBICS transaction had errors.
- */
- EBICS_UPLOAD_TRANSACTION_ID_MISSING,
- /**
- * May be caused by a connection issue OR the HTTP response
- * code was not 200 OK. Both cases should lead to retry as
- * they are fixable or transient.
- */
- HTTP_POST_FAILED
-}
-
-/**
- * Those errors happen before getting to validate the bank response
- * and successfully verify its signature. They bring therefore NO
- * business meaning and may be retried.
- */
-class EbicsSideException(
- msg: String,
- val sideEc: EbicsSideError,
- cause: Exception? = null
-) : Exception(msg, cause)
-
-/**
* Signs and the encrypts the data to send via EBICS.
*
* @param cfg configuration handle.
@@ -430,27 +289,31 @@ fun prepareUploadPayload(
bankKeys: BankPublicKeysFile,
payload: ByteArray,
): PreparedUploadData {
- val innerSignedEbicsXml = signOrderEbics3( // A006 signature.
- payload,
- clientKeys.signature_private_key,
- cfg.ebicsPartnerId,
- cfg.ebicsUserId
- )
+ val innerSignedEbicsXml = XmlBuilder.toBytes("UserSignatureData") {
+ attr("xmlns", "http://www.ebics.org/S002")
+ el("OrderSignatureData") {
+ el("SignatureVersion", "A006")
+ el("SignatureValue", CryptoUtil.signEbicsA006(
+ CryptoUtil.digestEbicsOrderA006(payload),
+ clientKeys.signature_private_key,
+ ).encodeBase64())
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ }
+ }
val encryptionResult = CryptoUtil.encryptEbicsE002(
- innerSignedEbicsXml.inputStream().deflate().readAllBytes(),
+ innerSignedEbicsXml.inputStream().deflate(),
bankKeys.bank_encryption_public_key
)
- val plainTransactionKey = encryptionResult.plainTransactionKey
- ?: throw Exception("Could not generate the transaction key, cannot encrypt the payload!")
// Then only E002 symmetric (with ephemeral key) encrypt.
- val compressedInnerPayload = payload.inputStream().deflate().readAllBytes()
+ val compressedInnerPayload = payload.inputStream().deflate()
// TODO stream
val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey(
compressedInnerPayload,
bankKeys.bank_encryption_public_key,
- plainTransactionKey
+ encryptionResult.plainTransactionKey
)
- val encodedEncryptedPayload = Base64.getEncoder().encodeToString(encryptedPayload.encryptedData)
+ val encodedEncryptedPayload = encryptedPayload.encryptedData.encodeBase64()
return PreparedUploadData(
encryptionResult.encryptedTransactionKey, // ephemeral key
@@ -461,32 +324,6 @@ fun prepareUploadPayload(
}
/**
- * Possible states of an EBICS transaction.
- */
-enum class EbicsPhase {
- initialization,
- transmission,
- receipt
-}
-
-/**
- * Witnesses a failure in an EBICS communication. That
- * implies that the bank response and its signature were
- * both valid.
- */
-class EbicsUploadException(
- msg: String,
- val phase: EbicsPhase,
- val ebicsErrorCode: EbicsReturnCode,
- /**
- * If the error was EBICS-technical, then we might not
- * even have interest on the business error code, therefore
- * the value below may be null.
- */
- val bankErrorCode: EbicsReturnCode? = null
-) : Exception(msg)
-
-/**
* Collects all the steps of an EBICS 3 upload transaction.
* NOTE: this function could conveniently be reused for an EBICS 2.x
* transaction, hence this function stays in this file.
@@ -505,60 +342,27 @@ suspend fun doEbicsUpload(
bankKeys: BankPublicKeysFile,
service: Ebics3Service,
payload: ByteArray,
-): EbicsResponseContent = withContext(NonCancellable) {
- val impl = Ebics3Impl(cfg, bankKeys, clientKeys)
+): String = withContext(NonCancellable) {
+ val impl = Ebics3BTS(cfg, bankKeys, clientKeys)
// TODO use a lambda and pass the order detail there for atomicity ?
val preparedPayload = prepareUploadPayload(cfg, clientKeys, bankKeys, payload)
+
+ // Init phase
val initXml = impl.uploadInitialization(service, preparedPayload)
- val initResp = postEbics( // may throw EbicsEarlyException
- client,
- cfg,
- bankKeys,
- initXml
- )
- if (!areCodesOk(initResp)) throw EbicsUploadException(
- "EBICS upload init failed",
- phase = EbicsPhase.initialization,
- ebicsErrorCode = initResp.technicalReturnCode,
- bankErrorCode = initResp.bankReturnCode
- )
- // Init phase OK, proceeding with the transfer phase.
- val tId = initResp.transactionID
- ?: throw EbicsSideException(
- "EBICS upload init phase did not return a transaction ID, cannot do the transfer phase.",
- sideEc = EbicsSideError.EBICS_UPLOAD_TRANSACTION_ID_MISSING
- )
+ val initResp = postEbics(client, cfg, bankKeys, initXml).okOrFail("Upload init phase")
+ val tId = requireNotNull(initResp.transactionID) {
+ "Upload init phase: missing transaction ID"
+ }
+
+ // Transfer phase
val transferXml = impl.uploadTransfer(tId, preparedPayload)
- val transferResp = postEbics(
- client,
- cfg,
- bankKeys,
- transferXml
- )
- logger.debug("Download init phase done. EBICS- and bank-technical codes are: ${transferResp.technicalReturnCode}, ${transferResp.bankReturnCode}")
- if (!areCodesOk(transferResp)) throw EbicsUploadException(
- "EBICS upload transfer failed",
- phase = EbicsPhase.transmission,
- ebicsErrorCode = initResp.technicalReturnCode,
- bankErrorCode = initResp.bankReturnCode
- )
- // EBICS- and bank-technical codes were both EBICS_OK, success!
- transferResp
+ val transferResp = postEbics(client, cfg, bankKeys, transferXml).okOrFail("Upload transfer phase")
+ val orderId = requireNotNull(transferResp.orderID) {
+ "Upload transfer phase: missing order ID"
+ }
+ orderId
}
-
-data class EbicsProtocolError(
- val httpStatusCode: HttpStatusCode,
- val reason: String,
- /**
- * This class is also used when Nexus finds itself
- * in an inconsistent state, without interacting with the
- * bank. In this case, the EBICS code below can be left
- * null.
- */
- val ebicsTechnicalCode: EbicsReturnCode? = null
-) : Exception(reason)
-
/**
* @param size in bits
*/
@@ -569,113 +373,16 @@ fun getNonce(size: Int): ByteArray {
return ret
}
-data class PreparedUploadData(
+class PreparedUploadData(
val transactionKey: ByteArray,
val userSignatureDataEncrypted: ByteArray,
val dataDigest: ByteArray,
val encryptedPayloadChunks: List<String>
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as PreparedUploadData
-
- if (!transactionKey.contentEquals(other.transactionKey)) return false
- if (!userSignatureDataEncrypted.contentEquals(other.userSignatureDataEncrypted)) return false
- if (encryptedPayloadChunks != other.encryptedPayloadChunks) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = transactionKey.contentHashCode()
- result = 31 * result + userSignatureDataEncrypted.contentHashCode()
- result = 31 * result + encryptedPayloadChunks.hashCode()
- return result
- }
-}
+)
-data class DataEncryptionInfo(
+class DataEncryptionInfo(
val transactionKey: ByteArray,
val bankPubDigest: ByteArray
-) {
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as DataEncryptionInfo
-
- if (!transactionKey.contentEquals(other.transactionKey)) return false
- if (!bankPubDigest.contentEquals(other.bankPubDigest)) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = transactionKey.contentHashCode()
- result = 31 * result + bankPubDigest.contentHashCode()
- return result
- }
-}
-
-
-// TODO import missing using a script
-@Suppress("SpellCheckingInspection")
-enum class EbicsReturnCode(val errorCode: String) {
- EBICS_OK("000000"),
- EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"),
- EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"),
- EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"),
- EBICS_AUTHENTICATION_FAILED("061001"),
- EBICS_INVALID_REQUEST("061002"),
- EBICS_INTERNAL_ERROR("061099"),
- EBICS_TX_RECOVERY_SYNC("061101"),
- EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"),
- EBICS_INVALID_ORDER_DATA_FORMAT("090004"),
- EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"),
- EBICS_INVALID_USER_OR_USER_STATE("091002"),
- EBICS_USER_UNKNOWN("091003"),
- EBICS_EBICS_INVALID_USER_STATE("091004"),
- EBICS_INVALID_ORDER_IDENTIFIER("091005"),
- EBICS_UNSUPPORTED_ORDER_TYPE("091006"),
- EBICS_INVALID_XML("091010"),
- EBICS_TX_MESSAGE_REPLAY("091103"),
- EBICS_PROCESSING_ERROR("091116"),
- EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"),
- EBICS_AMOUNT_CHECK_FAILED("091303");
-
- companion object {
- fun lookup(errorCode: String): EbicsReturnCode {
- for (x in entries) {
- if (x.errorCode == errorCode) {
- return x
- }
- }
- throw Exception(
- "Unknown EBICS status code: $errorCode"
- )
- }
- }
-}
-
-data class EbicsResponseContent(
- val transactionID: String?,
- val orderID: String?,
- val dataEncryptionInfo: DataEncryptionInfo?,
- val orderDataEncChunk: String?,
- val technicalReturnCode: EbicsReturnCode,
- val bankReturnCode: EbicsReturnCode,
- val reportText: String,
- val segmentNumber: Int?,
- // Only present in init phase
- val numSegments: Int?
-)
-
-data class EbicsKeyManagementResponseContent(
- val technicalReturnCode: EbicsReturnCode,
- val bankReturnCode: EbicsReturnCode?,
- val orderData: ByteArray?
)
/**
@@ -694,7 +401,7 @@ data class EbicsKeyManagementResponseContent(
* @param httpClient HTTP client to connect to the bank.
*/
suspend fun submitPain001(
- pain001xml: String,
+ pain001xml: ByteArray,
cfg: EbicsSetupConfig,
clientKeys: ClientPrivateKeysFile,
bankkeys: BankPublicKeysFile,
@@ -707,17 +414,69 @@ suspend fun submitPain001(
messageVersion = "09",
container = null
)
- val maybeUploaded = doEbicsUpload(
+ return doEbicsUpload(
httpClient,
cfg,
clientKeys,
bankkeys,
service,
- pain001xml.toByteArray(Charsets.UTF_8),
+ pain001xml,
)
- logger.debug("Payment submitted, report text is: ${maybeUploaded.reportText}," +
- " EBICS technical code is: ${maybeUploaded.technicalReturnCode}," +
- " bank technical return code is: ${maybeUploaded.bankReturnCode}"
- )
- return maybeUploaded.orderID!!
+}
+
+// TODO import missing using a script
+@Suppress("SpellCheckingInspection")
+enum class EbicsReturnCode(val code: String) {
+ EBICS_OK("000000"),
+ EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"),
+ EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"),
+ EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"),
+ EBICS_AUTHENTICATION_FAILED("061001"),
+ EBICS_INVALID_REQUEST("061002"),
+ EBICS_INTERNAL_ERROR("061099"),
+ EBICS_TX_RECOVERY_SYNC("061101"),
+ EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"),
+ EBICS_INVALID_ORDER_DATA_FORMAT("090004"),
+ EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"),
+ EBICS_INVALID_USER_OR_USER_STATE("091002"),
+ EBICS_USER_UNKNOWN("091003"),
+ EBICS_EBICS_INVALID_USER_STATE("091004"),
+ EBICS_INVALID_ORDER_IDENTIFIER("091005"),
+ EBICS_UNSUPPORTED_ORDER_TYPE("091006"),
+ EBICS_INVALID_XML("091010"),
+ EBICS_TX_MESSAGE_REPLAY("091103"),
+ EBICS_INVALID_REQUEST_CONTENT("091113"),
+ EBICS_PROCESSING_ERROR("091116"),
+ EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"),
+ EBICS_AMOUNT_CHECK_FAILED("091303");
+
+ enum class Kind {
+ Information,
+ Note,
+ Warning,
+ Error
+ }
+
+ fun kind(): Kind {
+ return when (val errorClass = code.substring(0..1)) {
+ "00" -> Kind.Information
+ "01" -> Kind.Note
+ "03" -> Kind.Warning
+ "06", "09" -> Kind.Error
+ else -> throw Exception("Unknown EBICS status code error class: $errorClass")
+ }
+ }
+
+ companion object {
+ fun lookup(code: String): EbicsReturnCode {
+ for (x in entries) {
+ if (x.code == code) {
+ return x
+ }
+ }
+ throw Exception(
+ "Unknown EBICS status code: $code"
+ )
+ }
+ }
} \ No newline at end of file