/* * 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 * */ /** * This is the main "EBICS library interface". Functions here are stateless helpers * used to implement both an EBICS server and EBICS client. */ package tech.libeufin.ebics import io.ktor.http.* import org.w3c.dom.Document import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.common.* import java.io.InputStream import java.security.SecureRandom import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey import java.time.Instant import java.time.ZoneId import java.time.ZonedDateTime import javax.xml.datatype.DatatypeFactory import javax.xml.datatype.XMLGregorianCalendar 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 */ fun getNonce(size: Int): ByteArray { val sr = SecureRandom() val ret = ByteArray(size / 8) sr.nextBytes(ret) return ret } data class PreparedUploadData( val transactionKey: ByteArray, val userSignatureDataEncrypted: ByteArray, val dataDigest: ByteArray, val encryptedPayloadChunks: List ) { 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( 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" ) } } } fun signOrderEbics3( orderBlob: ByteArray, signKey: RSAPrivateCrtKey, partnerId: String, userId: String ): ByteArray { return XmlBuilder.toString("UserSignatureData") { attr("xmlns", "http://www.ebics.org/S002") el("OrderSignatureData") { el("SignatureVersion", "A006") el("SignatureValue", CryptoUtil.signEbicsA006( CryptoUtil.digestEbicsOrderA006(orderBlob), signKey ).encodeBase64()) el("PartnerID", partnerId) el("UserID", userId) } }.toByteArray() } 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? ) class HpbResponseData( val hostID: String, val encryptionPubKey: RSAPublicKey, val encryptionVersion: String, val authenticationPubKey: RSAPublicKey, val authenticationVersion: String ) fun ebics3toInternalRepr(response: Document): EbicsResponseContent { // TODO better ebics response type return XmlDestructor.fromDoc(response, "ebicsResponse") { var transactionID: String? = null var numSegments: Int? = null lateinit var technicalReturnCode: EbicsReturnCode lateinit var bankReturnCode: EbicsReturnCode lateinit var reportText: String var orderID: String? = null var segmentNumber: Int? = null var orderDataEncChunk: String? = 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() technicalReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text()) reportText = one("ReportText").text() } } one("body") { opt("DataTransfer") { orderDataEncChunk = one("OrderData").text() dataEncryptionInfo = opt("DataEncryptionInfo") { DataEncryptionInfo( one("TransactionKey").text().decodeBase64(), one("EncryptionPubKeyDigest").text().decodeBase64() ) } } bankReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text()) } EbicsResponseContent( transactionID = transactionID, orderID = orderID, bankReturnCode = bankReturnCode, technicalReturnCode = technicalReturnCode, reportText = reportText, orderDataEncChunk = orderDataEncChunk, dataEncryptionInfo = dataEncryptionInfo, numSegments = numSegments, segmentNumber = segmentNumber ) } } fun parseEbicsHpbOrder(orderDataRaw: InputStream): HpbResponseData { return XmlDestructor.fromStream(orderDataRaw, "HPBResponseOrderData") { val (authenticationPubKey, authenticationVersion) = one("AuthenticationPubKeyInfo") { Pair( one("PubKeyValue").one("RSAKeyValue") { CryptoUtil.loadRsaPublicKeyFromComponents( one("Modulus").text().decodeBase64(), one("Exponent").text().decodeBase64(), ) }, one("AuthenticationVersion").text() ) } val (encryptionPubKey, encryptionVersion) = one("EncryptionPubKeyInfo") { Pair( one("PubKeyValue").one("RSAKeyValue") { CryptoUtil.loadRsaPublicKeyFromComponents( one("Modulus").text().decodeBase64(), one("Exponent").text().decodeBase64(), ) }, one("EncryptionVersion").text() ) } val hostID: String = one("HostID").text() HpbResponseData( hostID = hostID, encryptionPubKey = encryptionPubKey, encryptionVersion = encryptionVersion, authenticationPubKey = authenticationPubKey, authenticationVersion = authenticationVersion ) } }