/* * 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 import io.ktor.client.* import tech.libeufin.nexus.* import tech.libeufin.common.* import tech.libeufin.common.crypto.* import java.math.BigInteger import java.time.* import java.time.format.* import java.util.* import java.io.File import org.w3c.dom.* import javax.xml.datatype.XMLGregorianCalendar import javax.xml.datatype.DatatypeFactory 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, clientKeys: ClientPrivateKeysFile ): ByteArray { val temp = XmlBuilder.toBytes("ns2:SignaturePubKeyOrderData") { attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#") attr("xmlns:ns2", "http://www.ebics.org/S001") el("ns2:SignaturePubKeyInfo") { el("ns2:PubKeyValue") { el("ds:RSAKeyValue") { el("ds:Modulus", clientKeys.signature_private_key.modulus.encodeBase64()) el("ds:Exponent", clientKeys.signature_private_key.publicExponent.encodeBase64()) } } el("ns2:SignatureVersion", "A006") } el("ns2:PartnerID", cfg.ebicsPartnerId) el("ns2:UserID", cfg.ebicsUserId) } // TODO in ebics:H005 we MUST use x509 certificates ... println(temp) val inner = temp.inputStream().deflate().encodeBase64() val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H005") { attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H005") attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#") attr("Version", "H005") attr("Revision", "1") el("header") { attr("authenticate", "true") el("static") { el("HostID", cfg.ebicsHostId) el("PartnerID", cfg.ebicsPartnerId) el("UserID", cfg.ebicsUserId) el("OrderDetails/AdminOrderType", "INI") el("SecurityMedium", "0200") } el("mutable") } el("body/DataTransfer/OrderData", inner) } return XMLUtil.convertDomToBytes(doc) } /** EBICS 3 protocol for business transactions */ class Ebics3BTS( private val cfg: EbicsSetupConfig, 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 { val nonce = getNonce(128) return signedRequest { el("header") { attr("authenticate", "true") el("static") { el("HostID", cfg.ebicsHostId) el("Nonce", nonce.encodeHex()) el("Timestamp", Instant.now().xmlDateTime()) el("PartnerID", cfg.ebicsPartnerId) el("UserID", cfg.ebicsUserId) // SystemID // Product el("OrderDetails") { el("AdminOrderType", orderType) if (orderType == "BTD") { el("BTDOrderParams") { if (service != null) { el("Service") { el("ServiceName", service.name) el("Scope", service.scope) if (service.container != null) { el("Container") { attr("containerType", service.container) } } el("MsgName") { attr("version", service.messageVersion) text(service.messageName) } } } if (startDate != null) { el("DateRange") { el("Start", startDate.xmlDate()) el("End", (endDate ?: Instant.now()).xmlDate()) } } } } } bankDigest() } el("mutable/TransactionPhase", "Initialisation") } el("AuthSignature") el("body") } } fun downloadTransfer( howManySegments: Int, segmentNumber: Int, transactionId: String ): ByteArray { 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", if (howManySegments == segmentNumber) "true" else "false") } } } el("AuthSignature") el("body") } } fun downloadReceipt( transactionId: String, success: Boolean ): ByteArray { return signedRequest { el("header") { attr("authenticate", "true") el("static") { el("HostID", cfg.ebicsHostId) el("TransactionID", transactionId) } el("mutable") { el("TransactionPhase", "Receipt") } } el("AuthSignature") el("body/TransferReceipt") { attr("authenticate", "true") el("ReceiptCode", if (success) "0" else "1") } } } /* ----- Helpers ----- */ /** Generate a signed H005 ebicsRequest */ private fun signedRequest(lambda: XmlBuilder.() -> Unit): ByteArray { val doc = XmlBuilder.toDom("ebicsRequest", "urn:org:ebics:H005") { attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H005") attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#") attr("Version", "H005") attr("Revision", "1") lambda() } XMLUtil.signEbicsDocument( doc, clientKeys.authentication_private_key, withEbics3 = true ) return XMLUtil.convertDomToBytes(doc) } private fun XmlBuilder.bankDigest() { el("BankPubKeyDigests") { el("Authentication") { attr("Version", "X002") attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256") text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_authentication_public_key).encodeBase64()) } el("Encryption") { attr("Version", "E002") attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256") text(CryptoUtil.getEbicsPublicKeyHash(bankKeys.bank_encryption_public_key).encodeBase64()) } // Signature } el("SecurityMedium", "0000") } companion object { fun parseResponse(doc: Document): EbicsResponse { return XmlDestructor.fromDoc(doc, "ebicsResponse") { var transactionID: String? = null var numSegments: Int? = null lateinit var technicalCode: EbicsReturnCode lateinit var bankCode: EbicsReturnCode var orderID: String? = null var segmentNumber: Int? = null var payloadChunk: ByteArray? = null var dataEncryptionInfo: DataEncryptionInfo? = null one("header") { one("static") { transactionID = opt("TransactionID")?.text() numSegments = opt("NumSegments")?.text()?.toInt() } one("mutable") { segmentNumber = opt("SegmentNumber")?.text()?.toInt() orderID = opt("OrderID")?.text() technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text()) } } one("body") { opt("DataTransfer") { payloadChunk = one("OrderData").text().decodeBase64() dataEncryptionInfo = opt("DataEncryptionInfo") { DataEncryptionInfo( one("TransactionKey").text().decodeBase64(), one("EncryptionPubKeyDigest").text().decodeBase64() ) } } bankCode = EbicsReturnCode.lookup(one("ReturnCode").text()) } EbicsResponse( bankCode = bankCode, technicalCode = technicalCode, content = BTSResponse( transactionID = transactionID, orderID = orderID, payloadChunk = payloadChunk, dataEncryptionInfo = dataEncryptionInfo, numSegments = numSegments, segmentNumber = segmentNumber ) ) } } } } data class BTSResponse( val transactionID: String?, val orderID: String?, val dataEncryptionInfo: DataEncryptionInfo?, val payloadChunk: ByteArray?, val segmentNumber: Int?, val numSegments: Int? )