From 861389ea98224e86f28fdf06570a260c3ae12f90 Mon Sep 17 00:00:00 2001 From: Antoine A <> Date: Mon, 11 Mar 2024 15:54:26 +0100 Subject: Clean EBICS dialect logic --- .../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 47 ++--- .../kotlin/tech/libeufin/nexus/ebics/Ebics3.kt | 198 +++++++++------------ .../tech/libeufin/nexus/ebics/EbicsCommon.kt | 11 +- .../tech/libeufin/nexus/ebics/EbicsDialect.kt | 49 +++++ 4 files changed, 157 insertions(+), 148 deletions(-) create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt index 2b713432..c74ea2f1 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -64,39 +64,6 @@ data class FetchContext( val fileLogger: FileLogger ) -/** - * Downloads content via EBICS, according to the order params passed - * by the caller. - * - * @param T [Ebics2Request] for EBICS 2 or [Ebics3Request.OrderDetails.BTOrderParams] for EBICS 3 - * @param ctx [FetchContext] - * @param req contains the instructions for the download, namely - * which document is going to be downloaded from the bank. - * @return the [ByteArray] payload. On an empty response, the array - * length is zero. It returns null, if the bank assigned an - * error to the EBICS transaction. - */ -private suspend fun downloadHelper( - ctx: FetchContext, - lastExecutionTime: Instant? = null, - doc: SupportedDocument, - processing: (InputStream) -> Unit -) { - val initXml = Ebics3BTS( - ctx.cfg, - ctx.bankKeys, - ctx.clientKeys - ).downloadInitializationDoc(doc, lastExecutionTime) - return ebicsDownload( - ctx.httpClient, - ctx.cfg, - ctx.clientKeys, - ctx.bankKeys, - initXml, - processing - ) -} - /** * Converts the 2-digits fraction value as given by the bank * (postfinance dialect), to the Taler 8-digit value (db representation). @@ -316,9 +283,19 @@ private suspend fun fetchDocuments( } else { logger.info("Fetching '${doc.fullDescription()}' from timestamp: $lastExecutionTime") } - val doc = doc.doc() // downloading the content - downloadHelper(ctx, lastExecutionTime, doc) { stream -> + val doc = doc.doc() + val (orderType, service) = downloadDocService(doc) + ebicsDownload( + ctx.httpClient, + ctx.cfg, + ctx.clientKeys, + ctx.bankKeys, + orderType, + service, + lastExecutionTime, + null + ) { stream -> val loggedStream = ctx.fileLogger.logFetch( stream, doc == SupportedDocument.PAIN_002_LOGS diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt index 11ccf30d..5acba13b 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt @@ -36,14 +36,6 @@ import java.security.interfaces.* fun Instant.xmlDate(): String = DateTimeFormatter.ISO_DATE.withZone(ZoneId.of("UTC")).format(this) fun Instant.xmlDateTime(): String = DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(ZoneId.of("UTC")).format(this) -data class Ebics3Service( - val name: String, - val scope: String, - val messageName: String, - val messageVersion: String, - val container: String? -) - // TODO WIP fun iniRequest( cfg: EbicsSetupConfig, @@ -94,110 +86,9 @@ class Ebics3BTS( private val bankKeys: BankPublicKeysFile, private val clientKeys: ClientPrivateKeysFile ) { - - /* ----- Ergonomic entrypoints ----- */ - - fun downloadInitializationDoc(whichDoc: SupportedDocument, startDate: Instant? = null, endDate: Instant? = null): ByteArray { - val (orderType, service) = when (whichDoc) { - SupportedDocument.PAIN_002 -> Pair("BTD", Ebics3Service("PSR", "CH", "pain.002", "10", "ZIP")) - SupportedDocument.CAMT_052 -> Pair("BTD", Ebics3Service("STM", "CH", "camt.052", "08", "ZIP")) - SupportedDocument.CAMT_053 -> Pair("BTD", Ebics3Service("EOP", "CH", "camt.053", "08", "ZIP")) - SupportedDocument.CAMT_054 -> Pair("BTD", Ebics3Service("REP", "CH", "camt.054", "08", "ZIP")) - SupportedDocument.PAIN_002_LOGS -> Pair("HAC", null) - } - return downloadInitialization(orderType, service, startDate, endDate) - } - - /* ----- Upload ----- */ - - fun uploadInitialization(service: Ebics3Service, preparedUploadData: PreparedUploadData): ByteArray { - val nonce = getNonce(128) - return signedRequest { - el("header") { - attr("authenticate", "true") - el("static") { - el("HostID", cfg.ebicsHostId) - el("Nonce", nonce.encodeUpHex()) - el("Timestamp", Instant.now().xmlDateTime()) - el("PartnerID", cfg.ebicsPartnerId) - el("UserID", cfg.ebicsUserId) - // SystemID - // Product - el("OrderDetails") { - el("AdminOrderType", "BTU") - el("BTUOrderParams") { - el("Service") { - el("ServiceName", service.name) - el("Scope", service.scope) - el("MsgName") { - attr("version", service.messageVersion) - text(service.messageName) - } - } - el("SignatureFlag", "true") - } - } - bankDigest() - el("NumSegments", "1") // TODO test upload of many segment - - } - el("mutable") { - el("TransactionPhase", "Initialisation") - } - } - el("AuthSignature") - el("body") { - el("DataTransfer") { - el("DataEncryptionInfo") { - attr("authenticate", "true") - el("EncryptionPubKeyDigest") { - attr("Version", "E002") - attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256") - text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64()) - } - el("TransactionKey", preparedUploadData.transactionKey.encodeBase64()) - } - el("SignatureData") { - attr("authenticate", "true") - text(preparedUploadData.userSignatureDataEncrypted.encodeBase64()) - } - el("DataDigest") { - attr("SignatureVersion", "A006") - text(preparedUploadData.dataDigest.encodeBase64()) - } - } - } - } - } - - fun uploadTransfer( - transactionId: String, - uploadData: PreparedUploadData - ): ByteArray { - val chunkIndex = 1 // TODO test upload of many segment - return signedRequest { - el("header") { - attr("authenticate", "true") - el("static") { - el("HostID", cfg.ebicsHostId) - el("TransactionID", transactionId) - } - el("mutable") { - el("TransactionPhase", "Transfer") - el("SegmentNumber") { - attr("lastSegment", "true") - text(chunkIndex.toString()) - } - } - } - el("AuthSignature") - el("body/DataTransfer/OrderData", uploadData.encryptedPayloadChunks[chunkIndex - 1]) - } - } - /* ----- Download ----- */ - fun downloadInitialization(orderType: String, service: Ebics3Service? = null, startDate: Instant? = null, endDate: Instant? = null): ByteArray { + fun downloadInitialization(orderType: String, service: Ebics3Service?, startDate: Instant?, endDate: Instant?): ByteArray { val nonce = getNonce(128) return signedRequest { el("header") { @@ -294,6 +185,93 @@ class Ebics3BTS( } } + /* ----- Upload ----- */ + + fun uploadInitialization(service: Ebics3Service, preparedUploadData: PreparedUploadData): ByteArray { + val nonce = getNonce(128) + return signedRequest { + el("header") { + attr("authenticate", "true") + el("static") { + el("HostID", cfg.ebicsHostId) + el("Nonce", nonce.encodeUpHex()) + el("Timestamp", Instant.now().xmlDateTime()) + el("PartnerID", cfg.ebicsPartnerId) + el("UserID", cfg.ebicsUserId) + // SystemID + // Product + el("OrderDetails") { + el("AdminOrderType", "BTU") + el("BTUOrderParams") { + el("Service") { + el("ServiceName", service.name) + el("Scope", service.scope) + el("MsgName") { + attr("version", service.messageVersion) + text(service.messageName) + } + } + el("SignatureFlag", "true") + } + } + bankDigest() + el("NumSegments", "1") // TODO test upload of many segment + + } + el("mutable") { + el("TransactionPhase", "Initialisation") + } + } + el("AuthSignature") + el("body") { + el("DataTransfer") { + el("DataEncryptionInfo") { + attr("authenticate", "true") + el("EncryptionPubKeyDigest") { + attr("Version", "E002") + attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256") + text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64()) + } + el("TransactionKey", preparedUploadData.transactionKey.encodeBase64()) + } + el("SignatureData") { + attr("authenticate", "true") + text(preparedUploadData.userSignatureDataEncrypted.encodeBase64()) + } + el("DataDigest") { + attr("SignatureVersion", "A006") + text(preparedUploadData.dataDigest.encodeBase64()) + } + } + } + } + } + + fun uploadTransfer( + transactionId: String, + uploadData: PreparedUploadData + ): ByteArray { + val chunkIndex = 1 // TODO test upload of many segment + return signedRequest { + el("header") { + attr("authenticate", "true") + el("static") { + el("HostID", cfg.ebicsHostId) + el("TransactionID", transactionId) + } + el("mutable") { + el("TransactionPhase", "Transfer") + el("SegmentNumber") { + attr("lastSegment", "true") + text(chunkIndex.toString()) + } + } + } + el("AuthSignature") + el("body/DataTransfer/OrderData", uploadData.encryptedPayloadChunks[chunkIndex - 1]) + } + } + /* ----- Helpers ----- */ /** Generate a signed H005 ebicsRequest */ 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 fd969340..69e6da19 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -48,6 +48,7 @@ import java.security.interfaces.RSAPrivateCrtKey import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* +import java.time.Instant import kotlinx.coroutines.* import java.security.SecureRandom import org.w3c.dom.Document @@ -169,8 +170,11 @@ suspend fun ebicsDownload( cfg: EbicsSetupConfig, clientKeys: ClientPrivateKeysFile, bankKeys: BankPublicKeysFile, - reqXml: ByteArray, - processing: (InputStream) -> Unit + orderType: String, + service: Ebics3Service?, + startDate: Instant?, + endDate: Instant?, + processing: (InputStream) -> Unit, ) = coroutineScope { val impl = Ebics3BTS(cfg, bankKeys, clientKeys) val parentScope = this @@ -181,7 +185,8 @@ suspend fun ebicsDownload( // TODO find a way to cancel the pending transaction ? withContext(NonCancellable) { // Init phase - val initResp = postBTS(client, cfg, bankKeys, reqXml) + val initReq = impl.downloadInitialization(orderType, service, startDate, endDate) + val initResp = postBTS(client, cfg, bankKeys, initReq) if (initResp.bankCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) { logger.debug("Download content is empty") return@withContext diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt new file mode 100644 index 00000000..5d672de0 --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsDialect.kt @@ -0,0 +1,49 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.nexus.ebics + +// We will support more dialect in the future + +data class Ebics3Service( + val name: String, + val scope: String, + val messageName: String, + val messageVersion: String, + val container: String? +) + +fun downloadDocService(doc: SupportedDocument): Pair { + return when (doc) { + SupportedDocument.PAIN_002 -> Pair("BTD", Ebics3Service("PSR", "CH", "pain.002", "10", "ZIP")) + SupportedDocument.CAMT_052 -> Pair("BTD", Ebics3Service("STM", "CH", "camt.052", "08", "ZIP")) + SupportedDocument.CAMT_053 -> Pair("BTD", Ebics3Service("EOP", "CH", "camt.053", "08", "ZIP")) + SupportedDocument.CAMT_054 -> Pair("BTD", Ebics3Service("REP", "CH", "camt.054", "08", "ZIP")) + SupportedDocument.PAIN_002_LOGS -> Pair("HAC", null) + } +} + +fun uploadPaymentService(): Ebics3Service { + return Ebics3Service( + name = "MCT", + scope = "CH", + messageName = "pain.001", + messageVersion = "09", + container = null + ) +} -- cgit v1.2.3