commit b57e69e8cd4fd4f8d793e5698bcb864626382384 parent 83b2536fd0e3eb7b959991a2691385a797a8d487 Author: Florian Dold <florian.dold@gmail.com> Date: Tue, 11 Feb 2020 11:36:52 +0100 refactor EBICS protocol into nicer interface, unzip responses, allow date ranges Diffstat:
16 files changed, 1120 insertions(+), 574 deletions(-)
diff --git a/nexus/build.gradle b/nexus/build.gradle @@ -35,6 +35,7 @@ dependencies { implementation "javax.activation:activation:1.1" implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1" implementation 'org.apache.santuario:xmlsec:2.1.4' + implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.20' testImplementation group: 'junit', name: 'junit', version: '4.12' } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt @@ -1,82 +1,38 @@ package tech.libeufin.nexus import io.ktor.client.HttpClient +import io.ktor.client.request.post import io.ktor.http.HttpStatusCode -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.EbicsOrderUtil -import tech.libeufin.util.ebics_h004.EbicsRequest -import tech.libeufin.util.ebics_h004.EbicsResponse -import tech.libeufin.util.getGregorianCalendarNow -import java.lang.StringBuilder -import java.math.BigInteger -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey +import tech.libeufin.util.* import java.util.* -import java.util.zip.DeflaterInputStream -/** - * This class is a mere container that keeps data found - * in the database and that is further needed to sign / verify - * / make messages. And not all the values are needed all - * the time. - */ -data class EbicsSubscriberDetails( - val partnerId: String, - val userId: String, - var bankAuthPub: RSAPublicKey?, - var bankEncPub: RSAPublicKey?, - // needed to send the message - val ebicsUrl: String, - // needed to craft further messages - val hostId: String, - // needed to decrypt data coming from the bank - val customerEncPriv: RSAPrivateCrtKey, - // needed to sign documents - val customerAuthPriv: RSAPrivateCrtKey, - val customerSignPriv: RSAPrivateCrtKey -) +suspend inline fun HttpClient.postToBank(url: String, body: String): String { + logger.debug("Posting: $body") + val response = try { + this.post<String>( + urlString = url, + block = { + this.body = body + } + ) + } catch (e: Exception) { + throw UnreachableBankError(HttpStatusCode.InternalServerError) + } + return response +} +sealed class EbicsDownloadResult +class EbicsDownloadSuccessResult( + val orderData: ByteArray +) : EbicsDownloadResult() /** - * Wrapper around the lower decryption routine, that takes a EBICS response - * object containing a encrypted payload, and return the plain version of it - * (including decompression). + * Some bank-technical error occured. */ -fun decryptAndDecompressResponse(chunks: List<String>, transactionKey: ByteArray, privateKey: RSAPrivateCrtKey, pubDigest: ByteArray): ByteArray { - val buf = StringBuilder() - chunks.forEach { buf.append(it) } - val decoded = Base64.getDecoder().decode(buf.toString()) - val er = CryptoUtil.EncryptionResult( - transactionKey, - pubDigest, - decoded - ) - val dataCompr = CryptoUtil.decryptEbicsE002( - er, - privateKey - ) - return EbicsOrderUtil.decodeOrderData(dataCompr) -} - - -/** - * Get the private key that matches the given public key digest. - */ -fun getDecryptionKey(subscriberDetails: EbicsSubscriberDetails, pubDigest: ByteArray): RSAPrivateCrtKey { - val authPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerAuthPriv) - val encPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv) - val authPubDigest = CryptoUtil.getEbicsPublicKeyHash(authPub) - val encPubDigest = CryptoUtil.getEbicsPublicKeyHash(encPub) - if (pubDigest.contentEquals(authPubDigest)) { - return subscriberDetails.customerAuthPriv - } - if (pubDigest.contentEquals(encPubDigest)) { - return subscriberDetails.customerEncPriv - } - throw Exception("no matching private key to decrypt response") -} - +class EbicsDownloadBankErrorResult( + val returnCode: EbicsReturnCode +) : EbicsDownloadResult() /** * Do an EBICS download transaction. This includes the initialization phase, transaction phase @@ -84,133 +40,112 @@ fun getDecryptionKey(subscriberDetails: EbicsSubscriberDetails, pubDigest: ByteA */ suspend fun doEbicsDownloadTransaction( client: HttpClient, - subscriberDetails: EbicsSubscriberDetails, - orderType: String -): ByteArray { - val initDownloadRequest = EbicsRequest.createForDownloadInitializationPhase( - subscriberDetails.userId, - subscriberDetails.partnerId, - subscriberDetails.hostId, - getNonce(128), - getGregorianCalendarNow(), - subscriberDetails.bankEncPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberDetails.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - orderType - ) + subscriberDetails: EbicsClientSubscriberDetails, + orderType: String, + orderParams: EbicsOrderParams +): EbicsDownloadResult { + + // Initialization phase + val initDownloadRequestStr = createEbicsRequestForDownloadInitialization(subscriberDetails, orderType, orderParams) val payloadChunks = LinkedList<String>(); - val initResponse = client.postToBankSigned<EbicsRequest, EbicsResponse>( - subscriberDetails.ebicsUrl, - initDownloadRequest, - subscriberDetails.customerAuthPriv - ) - if (initResponse.value.body.returnCode.value != "000000") { - throw EbicsError(initResponse.value.body.returnCode.value) + val initResponseStr = client.postToBank(subscriberDetails.ebicsUrl, initDownloadRequestStr) + + val initResponse = parseAndValidateEbicsResponse(subscriberDetails, initResponseStr) + + when (initResponse.technicalReturnCode) { + EbicsReturnCode.EBICS_OK -> { + // Success, nothing to do! + } + else -> { + throw ProtocolViolationError("unexpected return code ${initResponse.technicalReturnCode}") + } + } + + when (initResponse.bankReturnCode) { + EbicsReturnCode.EBICS_OK -> { + // Success, nothing to do! + } + else -> { + return EbicsDownloadBankErrorResult(initResponse.bankReturnCode) + } } - val initDataTransfer = initResponse.value.body.dataTransfer + + val transactionID = + initResponse.transactionID ?: throw ProtocolViolationError("initial response must contain transaction ID") + + val encryptionInfo = initResponse.dataEncryptionInfo + ?: throw ProtocolViolationError("initial response did not contain encryption info") + + val initOrderDataEncChunk = initResponse.orderDataEncChunk ?: throw ProtocolViolationError("initial response for download transaction does not contain data transfer") - val dataEncryptionInfo = initDataTransfer.dataEncryptionInfo - ?: throw ProtocolViolationError("initial response for download transaction does not contain date encryption info") - val initOrderData = initDataTransfer.orderData.value - // FIXME: Also verify that algorithm matches! - val decryptionKey = getDecryptionKey(subscriberDetails, dataEncryptionInfo.encryptionPubKeyDigest.value) - payloadChunks.add(initOrderData) - val respPayload = decryptAndDecompressResponse( - payloadChunks, - dataEncryptionInfo.transactionKey, - decryptionKey, - dataEncryptionInfo.encryptionPubKeyDigest.value - ) - val ackRequest = EbicsRequest.createForDownloadReceiptPhase( - initResponse.value.header._static.transactionID ?: throw BankInvalidResponse( - HttpStatusCode.ExpectationFailed - ), - subscriberDetails.hostId - ) - val ackResponse = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( + + payloadChunks.add(initOrderDataEncChunk) + + val respPayload = decryptAndDecompressResponse(subscriberDetails, encryptionInfo, payloadChunks) + + // Acknowledgement phase + + val ackRequest = createEbicsRequestForDownloadReceipt(subscriberDetails, transactionID) + val ackResponseStr = client.postToBank( subscriberDetails.ebicsUrl, - ackRequest, - subscriberDetails.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberDetails.customerAuthPriv + ackRequest ) - if (ackResponse.value.body.returnCode.value != "000000") { - throw EbicsError(ackResponse.value.body.returnCode.value) + val ackResponse = parseAndValidateEbicsResponse(subscriberDetails, ackResponseStr) + when (ackResponse.technicalReturnCode) { + EbicsReturnCode.EBICS_DOWNLOAD_POSTPROCESS_DONE -> { + } + else -> { + throw ProtocolViolationError("unexpected return code") + } } - return respPayload + return EbicsDownloadSuccessResult(respPayload) } suspend fun doEbicsUploadTransaction( client: HttpClient, - subscriberDetails: EbicsSubscriberDetails, + subscriberDetails: EbicsClientSubscriberDetails, orderType: String, - payload: ByteArray + payload: ByteArray, + orderParams: EbicsOrderParams ) { if (subscriberDetails.bankEncPub == null) { throw InvalidSubscriberStateError("bank encryption key unknown, request HPB first") } - val userSignatureDateEncrypted = CryptoUtil.encryptEbicsE002( - EbicsOrderUtil.encodeOrderDataXml( - signOrder( - payload, - subscriberDetails.customerSignPriv, - subscriberDetails.partnerId, - subscriberDetails.userId - ) - ), - subscriberDetails.bankEncPub!! - ) - val response = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( - subscriberDetails.ebicsUrl, - EbicsRequest.createForUploadInitializationPhase( - userSignatureDateEncrypted, - subscriberDetails.hostId, - getNonce(128), - subscriberDetails.partnerId, - subscriberDetails.userId, - getGregorianCalendarNow(), - subscriberDetails.bankAuthPub!!, - subscriberDetails.bankEncPub!!, - BigInteger.ONE, - orderType - ), - subscriberDetails.bankAuthPub!!, - subscriberDetails.customerAuthPriv - ) - if (response.value.header.mutable.returnCode != "000000") { - throw EbicsError(response.value.header.mutable.returnCode) - } - if (response.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) + val preparedUploadData = prepareUploadPayload(subscriberDetails, payload) + val req = createEbicsRequestForUploadInitialization(subscriberDetails, orderType, orderParams, preparedUploadData) + val responseStr = client.postToBank(subscriberDetails.ebicsUrl, req) + + val initResponse = parseAndValidateEbicsResponse(subscriberDetails, responseStr) + if (initResponse.technicalReturnCode != EbicsReturnCode.EBICS_OK) { + throw ProtocolViolationError("unexpected return code") } + + val transactionID = + initResponse.transactionID ?: throw ProtocolViolationError("init response must have transaction ID") + logger.debug("INIT phase passed!") /* now send actual payload */ - val compressedInnerPayload = DeflaterInputStream( - payload.inputStream() - ).use { it.readAllBytes() } - val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( - compressedInnerPayload, - subscriberDetails.bankEncPub!!, - userSignatureDateEncrypted.plainTransactionKey!! - ) - val tmp = EbicsRequest.createForUploadTransferPhase( - subscriberDetails.hostId, - response.value.header._static.transactionID!!, - BigInteger.ONE, - encryptedPayload.encryptedData + + val tmp = createEbicsRequestForUploadTransferPhase( + subscriberDetails, + transactionID, + preparedUploadData, + 0 ) - val responseTransaction = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( + + val txRespStr = client.postToBank( subscriberDetails.ebicsUrl, - tmp, - subscriberDetails.bankAuthPub!!, - subscriberDetails.customerAuthPriv + tmp ) - if (responseTransaction.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) + + val txResp = parseAndValidateEbicsResponse(subscriberDetails, txRespStr) + + when (txResp.technicalReturnCode) { + EbicsReturnCode.EBICS_OK -> { + } + else -> { + throw ProtocolViolationError("unexpected return code") + } } -} -\ No newline at end of file +} diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -1,21 +1,6 @@ package tech.libeufin.nexus -import io.ktor.client.HttpClient -import io.ktor.client.request.post import io.ktor.http.HttpStatusCode -import tech.libeufin.util.* -import tech.libeufin.util.ebics_h004.EbicsRequest -import tech.libeufin.util.ebics_h004.EbicsResponse -import tech.libeufin.util.ebics_h004.EbicsTypes -import tech.libeufin.util.ebics_s001.UserSignatureData -import java.math.BigInteger -import java.security.PrivateKey -import java.security.SecureRandom -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.util.* -import javax.xml.bind.JAXBElement -import javax.xml.datatype.XMLGregorianCalendar /** * Inserts spaces every 2 characters, and a newline after 8 pairs. @@ -42,126 +27,3 @@ fun chunkString(input: String): String { fun expectId(param: String?): String { return param ?: throw NotAnIdError(HttpStatusCode.BadRequest) } - -fun signOrder( - orderBlob: ByteArray, - signKey: RSAPrivateCrtKey, - partnerId: String, - userId: String -): UserSignatureData { - val ES_signature = CryptoUtil.signEbicsA006( - CryptoUtil.digestEbicsOrderA006(orderBlob), - signKey - ) - val userSignatureData = UserSignatureData().apply { - orderSignatureList = listOf( - UserSignatureData.OrderSignatureData().apply { - signatureVersion = "A006" - signatureValue = ES_signature - partnerID = partnerId - userID = userId - } - ) - } - return userSignatureData -} - -/** - * @return null when the bank could not be reached, otherwise returns the - * response already converted in JAXB. - */ -suspend inline fun HttpClient.postToBank(url: String, body: String): String { - logger.debug("Posting: $body") - val response = try { - this.post<String>( - urlString = url, - block = { - this.body = body - } - ) - } catch (e: Exception) { - throw UnreachableBankError(HttpStatusCode.InternalServerError) - } - return response -} - -/** - * DO verify the bank's signature - */ -suspend inline fun <reified T, reified S> HttpClient.postToBankSignedAndVerify( - url: String, - body: T, - pub: RSAPublicKey, - priv: RSAPrivateCrtKey -): JAXBElement<S> { - val doc = XMLUtil.convertJaxbToDocument(body) - XMLUtil.signEbicsDocument(doc, priv) - val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - logger.debug("About to verify: ${response}") - val responseDocument = try { - XMLUtil.parseStringIntoDom(response) - } catch (e: Exception) { - throw UnparsableResponse( - HttpStatusCode.BadRequest, - response - ) - } - if (!XMLUtil.verifyEbicsDocument(responseDocument, pub)) { - throw BadSignature(HttpStatusCode.NotAcceptable) - } - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse( - HttpStatusCode.BadRequest, - response - ) - } -} - -suspend inline fun <reified T, reified S> HttpClient.postToBankSigned( - url: String, - body: T, - priv: PrivateKey -): JAXBElement<S> { - val doc = XMLUtil.convertJaxbToDocument(body) - XMLUtil.signEbicsDocument(doc, priv) - val response: String = this.postToBank(url, XMLUtil.convertDomToString(doc)) - println("bank response: $response") - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse( - HttpStatusCode.BadRequest, - response - ) - } -} - -/** - * do NOT verify the bank's signature - */ -suspend inline fun <reified T, reified S> HttpClient.postToBankUnsigned( - url: String, - body: T -): JAXBElement<S> { - val response: String = this.postToBank(url, XMLUtil.convertJaxbToString(body)) - try { - return XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw UnparsableResponse( - HttpStatusCode.BadRequest, - response - ) - } -} - -/** - * @param size in bits - */ -fun getNonce(size: Int): ByteArray { - val sr = SecureRandom() - val ret = ByteArray(size / 8) - sr.nextBytes(ret) - return ret -} -\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -1,23 +1,40 @@ package tech.libeufin.nexus -data class EbicsBackupRequest( +import tech.libeufin.util.EbicsDateRange +import tech.libeufin.util.EbicsOrderParams +import tech.libeufin.util.EbicsStandardOrderParams +import java.time.LocalDate + +data class EbicsBackupRequestJson( val passphrase: String ) -data class NexusError( +data class NexusErrorJson( val message: String ) -data class EbicsStandardOrderParams( - val dateRange: EbicsDateRange? -) +data class EbicsStandardOrderParamsJson( + val dateRange: EbicsDateRangeJson? +) { + fun toOrderParams(): EbicsOrderParams { + var dateRange: EbicsDateRange? = if (this.dateRange != null) { + EbicsDateRange( + LocalDate.parse(this.dateRange.start), + LocalDate.parse(this.dateRange.end) + ) + } else { + null + } + return EbicsStandardOrderParams(dateRange) + } +} -data class EbicsDateRange( +data class EbicsDateRangeJson( /** * ISO 8601 calendar dates: YEAR-MONTH(01-12)-DAY(1-31) */ - val start: String, - val end: String + val start: String?, + val end: String? ) /** @@ -25,7 +42,7 @@ data class EbicsDateRange( * and as a request to the backup restore. Note: in the second case * the client must provide the passphrase. */ -data class EbicsKeysBackup( +data class EbicsKeysBackupJson( val userID: String, val partnerID: String, val hostID: String, @@ -47,7 +64,7 @@ data class EbicsPubKeyInfo( * This object is POSTed by clients _after_ having created * a EBICS subscriber at the sandbox. */ -data class EbicsSubscriberInfoRequest( +data class EbicsSubscriberInfoRequestJson( val ebicsURL: String, val hostID: String, val partnerID: String, @@ -58,7 +75,7 @@ data class EbicsSubscriberInfoRequest( /** * Contain the ID that identifies the new user in the Nexus system. */ -data class EbicsSubscriberInfoResponse( +data class EbicsSubscriberInfoResponseJson( val accountID: String, val ebicsURL: String, val hostID: String, @@ -70,16 +87,24 @@ data class EbicsSubscriberInfoResponse( /** * Admin call that tells all the subscribers managed by Nexus. */ -data class EbicsSubscribersResponse( - val ebicsSubscribers: MutableList<EbicsSubscriberInfoResponse> = mutableListOf() +data class EbicsSubscribersResponseJson( + val ebicsSubscribers: MutableList<EbicsSubscriberInfoResponseJson> = mutableListOf() ) -data class ProtocolAndVersion( +data class ProtocolAndVersionJson( val protocol: String, - val version: String, - val host: String + val version: String +) + +data class EbicsHevResponseJson( + val versions: List<ProtocolAndVersionJson> +) + +data class EbicsErrorDetailJson( + val type: String, + val ebicsReturnCode: String ) -data class EbicsHevResponse( - val versions: List<ProtocolAndVersion> +data class EbicsErrorJson( + val error: EbicsErrorDetailJson ) \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -22,7 +22,7 @@ package tech.libeufin.nexus import io.ktor.application.ApplicationCallPipeline import io.ktor.application.call import io.ktor.application.install -import io.ktor.client.* +import io.ktor.client.HttpClient import io.ktor.features.CallLogging import io.ktor.features.ContentNegotiation import io.ktor.features.StatusPages @@ -33,9 +33,14 @@ import io.ktor.request.receive import io.ktor.request.uri import io.ktor.response.respond import io.ktor.response.respondText -import io.ktor.routing.* +import io.ktor.routing.get +import io.ktor.routing.post +import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream +import org.apache.commons.compress.archivers.zip.ZipFile +import org.apache.commons.compress.utils.SeekableInMemoryByteChannel import org.jetbrains.exposed.exceptions.ExposedSQLException import org.jetbrains.exposed.sql.StdOutSqlLogger import org.jetbrains.exposed.sql.addLogger @@ -43,21 +48,15 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level -import tech.libeufin.util.ebics_h004.* import tech.libeufin.util.* -import java.text.DateFormat -import javax.sql.rowset.serial.SerialBlob -import tech.libeufin.util.toHexString -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.EbicsOrderUtil -import tech.libeufin.util.ebics_hev.HEVRequest -import tech.libeufin.util.ebics_hev.HEVResponse -import java.math.BigInteger +import tech.libeufin.util.InvalidSubscriberStateError +import java.lang.StringBuilder import java.security.interfaces.RSAPublicKey +import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* -import java.util.zip.DeflaterInputStream import javax.crypto.EncryptedPrivateKeyInfo +import javax.sql.rowset.serial.SerialBlob fun testData() { val pairA = CryptoUtil.generateRsaKeyPair(2048) @@ -97,7 +96,7 @@ data class BankInvalidResponse(val statusCode: HttpStatusCode) : Exception("Miss val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus") -fun getSubscriberDetailsFromId(id: String): EbicsSubscriberDetails { +fun getSubscriberDetailsFromId(id: String): EbicsClientSubscriberDetails { return transaction { val subscriber = EbicsSubscriberEntity.findById( id @@ -116,7 +115,7 @@ fun getSubscriberDetailsFromId(id: String): EbicsSubscriberDetails { subscriber.bankEncryptionPublicKey?.toByteArray()!! ) } - EbicsSubscriberDetails( + EbicsClientSubscriberDetails( bankAuthPub = bankAuthPubValue, bankEncPub = bankEncPubValue, @@ -243,123 +242,288 @@ fun main() { post("/ebics/subscribers/{id}/sendPTK") { val id = expectId(call.parameters["id"]) - val params = call.receive<EbicsStandardOrderParams>() - println("PTK order params: $params") + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() + println("PTK order params: $orderParams") val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "PTK") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "PTK", orderParams) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHAC") { val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HAC") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) - return@post + val response = doEbicsDownloadTransaction(client, subscriberData, "HAC", orderParams) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } } post("/ebics/subscribers/{id}/sendC52") { val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "C52") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "C52", orderParams) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } + } + + post("/ebics/subscribers/{id}/sendC53") { + val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "C53", orderParams) + when (response) { + is EbicsDownloadSuccessResult -> { + val mem = SeekableInMemoryByteChannel(response.orderData) + val zipFile = ZipFile(mem) + + val s = StringBuilder() + + zipFile.getEntriesInPhysicalOrder().iterator().forEach { entry -> + s.append("<=== File ${entry.name} ===>\n") + s.append(zipFile.getInputStream(entry).readAllBytes().toString(Charsets.UTF_8)) + s.append("\n") + } + + call.respondText( + s.toString(), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } + } + + post("/ebics/subscribers/{id}/sendC54") { + val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() + val subscriberData = getSubscriberDetailsFromId(id) + val response = doEbicsDownloadTransaction(client, subscriberData, "C54", orderParams) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHtd") { val id = expectId(call.parameters["id"]) + val paramsJson = call.receive<EbicsStandardOrderParamsJson>() + val orderParams = paramsJson.toOrderParams() val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HTD") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "HTD", orderParams) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHAA") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HAA") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "HAA", EbicsStandardOrderParams()) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHVZ") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HVZ") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + // FIXME: order params are wrong + val response = doEbicsDownloadTransaction(client, subscriberData, "HVZ", EbicsStandardOrderParams()) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHVU") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HVU") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + // FIXME: order params are wrong + val response = doEbicsDownloadTransaction(client, subscriberData, "HVU", EbicsStandardOrderParams()) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHPD") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HPD") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "HPD", EbicsStandardOrderParams()) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendHKD") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "HKD") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "HKD", EbicsStandardOrderParams()) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } post("/ebics/subscribers/{id}/sendTSD") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val response = doEbicsDownloadTransaction(client, subscriberData, "TSD") - call.respondText( - response.toString(Charsets.UTF_8), - ContentType.Text.Plain, - HttpStatusCode.OK - ) + val response = doEbicsDownloadTransaction(client, subscriberData, "TSD", EbicsGenericOrderParams()) + when (response) { + is EbicsDownloadSuccessResult -> { + call.respondText( + response.orderData.toString(Charsets.UTF_8), + ContentType.Text.Plain, + HttpStatusCode.OK + ) + } + is EbicsDownloadBankErrorResult -> { + call.respond( + HttpStatusCode.BadGateway, + EbicsErrorJson(EbicsErrorDetailJson("bankError", response.returnCode.errorCode)) + ) + } + } return@post } @@ -494,11 +658,11 @@ fun main() { } get("/ebics/subscribers") { - val ret = EbicsSubscribersResponse() + val ret = EbicsSubscribersResponseJson() transaction { EbicsSubscriberEntity.all().forEach { ret.ebicsSubscribers.add( - EbicsSubscriberInfoResponse( + EbicsSubscriberInfoResponseJson( accountID = it.id.value, hostID = it.hostID, partnerID = it.partnerID, @@ -519,7 +683,7 @@ fun main() { val tmp = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError( HttpStatusCode.NotFound ) - EbicsSubscriberInfoResponse( + EbicsSubscriberInfoResponseJson( accountID = tmp.id.value, hostID = tmp.hostID, partnerID = tmp.partnerID, @@ -534,27 +698,24 @@ fun main() { get("/ebics/{id}/sendHev") { val id = expectId(call.parameters["id"]) - val (ebicsUrl, hostID) = transaction { - val customer = EbicsSubscriberEntity.findById(id) - ?: throw SubscriberNotFoundError(HttpStatusCode.NotFound) - Pair(customer.ebicsURL, customer.hostID) - } - val request = HEVRequest().apply { - hostId = hostID - } - val response = client.postToBankUnsigned<HEVRequest, HEVResponse>(ebicsUrl, request) - // here, response is gueranteed to be successful, no need to check the status code. + val subscriberData = getSubscriberDetailsFromId(id) + val request = makeEbicsHEVRequest(subscriberData) + val response = client.postToBank(subscriberData.ebicsUrl, request) + val versionDetails = parseEbicsHEVResponse(subscriberData, response) call.respond( HttpStatusCode.OK, - EbicsHevResponse(response.value.versionNumber!!.map { - ProtocolAndVersion(it.value, it.protocolVersion, hostID) + EbicsHevResponseJson(versionDetails.versions.map { ebicsVersionSpec -> + ProtocolAndVersionJson( + ebicsVersionSpec.protocol, + ebicsVersionSpec.version + ) }) ) return@get } post("/ebics/{id}/subscribers") { - val body = call.receive<EbicsSubscriberInfoRequest>() + val body = call.receive<EbicsSubscriberInfoRequestJson>() val pairA = CryptoUtil.generateRsaKeyPair(2048) val pairB = CryptoUtil.generateRsaKeyPair(2048) val pairC = CryptoUtil.generateRsaKeyPair(2048) @@ -573,7 +734,7 @@ fun main() { } } catch (e: Exception) { print(e) - call.respond(NexusError("Could not store the new account into database")) + call.respond(NexusErrorJson("Could not store the new account into database")) return@post } call.respondText( @@ -587,25 +748,41 @@ fun main() { post("/ebics/subscribers/{id}/sendIni") { val id = expectId(call.parameters["id"]) val subscriberData = getSubscriberDetailsFromId(id) - val iniRequest = EbicsUnsecuredRequest.createIni( - subscriberData.hostId, - subscriberData.userId, - subscriberData.partnerId, - subscriberData.customerSignPriv - ) - val responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( + val iniRequest = makeEbicsIniRequest(subscriberData) + val responseStr = client.postToBank( subscriberData.ebicsUrl, iniRequest ) - if (responseJaxb.value.body.returnCode.value != "000000") { - throw EbicsError(responseJaxb.value.body.returnCode.value) + val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberData, responseStr) + if (resp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { + throw EbicsError("Unexpected INI response code: ${resp.technicalReturnCode}") } call.respondText("Bank accepted signature key\n", ContentType.Text.Plain, HttpStatusCode.OK) return@post } + post("/ebics/subscribers/{id}/sendHia") { + val id = expectId(call.parameters["id"]) + val subscriberData = getSubscriberDetailsFromId(id) + val hiaRequest = makeEbicsHiaRequest(subscriberData) + val responseStr = client.postToBank( + subscriberData.ebicsUrl, + hiaRequest + ) + val resp = parseAndDecryptEbicsKeyManagementResponse(subscriberData, responseStr) + if (resp.technicalReturnCode != EbicsReturnCode.EBICS_OK) { + throw EbicsError("Unexpected HIA response code: ${resp.technicalReturnCode}") + } + call.respondText( + "Bank accepted authentication and encryption keys\n", + ContentType.Text.Plain, + HttpStatusCode.OK + ) + return@post + } + post("/ebics/subscribers/{id}/restoreBackup") { - val body = call.receive<EbicsKeysBackup>() + val body = call.receive<EbicsKeysBackupJson>() val id = expectId(call.parameters["id"]) val subscriber = transaction { EbicsSubscriberEntity.findById(id) @@ -613,7 +790,7 @@ fun main() { if (subscriber != null) { call.respond( HttpStatusCode.Conflict, - NexusError("ID exists, please choose a new one") + NexusErrorJson("ID exists, please choose a new one") ) return@post } @@ -649,7 +826,7 @@ fun main() { } } catch (e: Exception) { print(e) - call.respond(NexusError("Could not store the new account $id into database")) + call.respond(NexusErrorJson("Could not store the new account $id into database")) return@post } call.respondText( @@ -687,12 +864,12 @@ fun main() { /* performs a keys backup */ post("/ebics/subscribers/{id}/backup") { val id = expectId(call.parameters["id"]) - val body = call.receive<EbicsBackupRequest>() + val body = call.receive<EbicsBackupRequestJson>() val response = transaction { val subscriber = EbicsSubscriberEntity.findById(id) ?: throw SubscriberNotFoundError( HttpStatusCode.NotFound ) - EbicsKeysBackup( + EbicsKeysBackupJson( userID = subscriber.userID, hostID = subscriber.hostID, partnerID = subscriber.partnerID, @@ -729,7 +906,13 @@ fun main() { val subscriberData = getSubscriberDetailsFromId(id) val payload = "PAYLOAD" - doEbicsUploadTransaction(client, subscriberData, "TSU", payload.toByteArray(Charsets.UTF_8)) + doEbicsUploadTransaction( + client, + subscriberData, + "TSU", + payload.toByteArray(Charsets.UTF_8), + EbicsGenericOrderParams() + ) call.respondText( "TST INITIALIZATION & TRANSACTION phases succeeded\n", @@ -741,83 +924,23 @@ fun main() { post("/ebics/subscribers/{id}/sync") { val id = expectId(call.parameters["id"]) val subscriberDetails = getSubscriberDetailsFromId(id) - val response = client.postToBankSigned<EbicsNpkdRequest, EbicsKeyManagementResponse>( - subscriberDetails.ebicsUrl, - EbicsNpkdRequest.createRequest( - subscriberDetails.hostId, - subscriberDetails.partnerId, - subscriberDetails.userId, - getNonce(128), - getGregorianCalendarNow() - ), - subscriberDetails.customerAuthPriv - ) - if (response.value.body.returnCode.value != "000000") { - throw EbicsError(response.value.body.returnCode.value) - } - val encPubKeyDigestViaBank = - (response.value.body.dataTransfer!!.dataEncryptionInfo as EbicsTypes.DataEncryptionInfo) - .encryptionPubKeyDigest.value; - val customerEncPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv); - val encPubKeyDigestViaNexus = CryptoUtil.getEbicsPublicKeyHash(customerEncPub) - println("encPubKeyDigestViaBank: ${encPubKeyDigestViaBank.toHexString()}") - println("encPubKeyDigestViaNexus: ${encPubKeyDigestViaNexus.toHexString()}") - val decryptionKey = getDecryptionKey(subscriberDetails, encPubKeyDigestViaBank) - val er = CryptoUtil.EncryptionResult( - response.value.body.dataTransfer!!.dataEncryptionInfo!!.transactionKey, - encPubKeyDigestViaBank, - response.value.body.dataTransfer!!.orderData.value - ) - val dataCompr = CryptoUtil.decryptEbicsE002( - er, - decryptionKey - ) - val data = EbicsOrderUtil.decodeOrderDataXml<HPBResponseOrderData>(dataCompr) - // put bank's keys into database. - transaction { - val subscriber = EbicsSubscriberEntity.findById(id) + val hpbRequest = makeEbicsHpbRequest(subscriberDetails) + val responseStr = client.postToBank(subscriberDetails.ebicsUrl, hpbRequest) - subscriber!!.bankAuthenticationPublicKey = SerialBlob( + val response = parseAndDecryptEbicsKeyManagementResponse(subscriberDetails, responseStr) + val orderData = + response.orderData ?: throw ProtocolViolationError("expected order data in HPB response") + val hpbData = parseEbicsHpbOrder(orderData) - CryptoUtil.loadRsaPublicKeyFromComponents( - data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - data.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ).encoded - ) - subscriber.bankEncryptionPublicKey = SerialBlob( - CryptoUtil.loadRsaPublicKeyFromComponents( - data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - data.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ).encoded - ) + // put bank's keys into database. + transaction { + val subscriber = EbicsSubscriberEntity.findById(id) ?: throw InvalidSubscriberStateError() + subscriber.bankAuthenticationPublicKey = SerialBlob(hpbData.authenticationPubKey.encoded) + subscriber.bankEncryptionPublicKey = SerialBlob(hpbData.encryptionPubKey.encoded) } call.respondText("Bank keys stored in database\n", ContentType.Text.Plain, HttpStatusCode.OK) return@post } - - post("/ebics/subscribers/{id}/sendHia") { - val id = expectId(call.parameters["id"]) - val subscriberData = getSubscriberDetailsFromId(id) - val responseJaxb = client.postToBankUnsigned<EbicsUnsecuredRequest, EbicsKeyManagementResponse>( - subscriberData.ebicsUrl, - EbicsUnsecuredRequest.createHia( - subscriberData.hostId, - subscriberData.userId, - subscriberData.partnerId, - subscriberData.customerAuthPriv, - subscriberData.customerEncPriv - ) - ) - if (responseJaxb.value.body.returnCode.value != "000000") { - throw EbicsError(responseJaxb.value.body.returnCode.value) - } - call.respondText( - "Bank accepted authentication and encryption keys\n", - ContentType.Text.Plain, - HttpStatusCode.OK - ) - return@post - } } } logger.info("Up and running") diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -47,6 +47,7 @@ import java.util.* import java.util.zip.DeflaterInputStream import java.util.zip.InflaterInputStream import javax.sql.rowset.serial.SerialBlob +import javax.xml.datatype.DatatypeFactory open class EbicsRequestError(errorText: String, errorCode: String) : @@ -118,7 +119,7 @@ private suspend fun ApplicationCall.respondEbicsKeyManagement( } } this.orderData = EbicsKeyManagementResponse.OrderData().apply { - this.value = dataTransfer.encryptedData + this.value = Base64.getEncoder().encodeToString(dataTransfer.encryptedData) } } } @@ -147,13 +148,13 @@ private fun iterHistory(customerId: Int, header: EbicsRequest.Header, base: XmlE (header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange!!.start.toString() } catch (e: Exception) { LOGGER.debug("Asked to iterate over history with NO start date; default to now") - getGregorianCalendarNow().toString() + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()).toString() }, try { (header.static.orderDetails?.orderParams as EbicsRequest.StandardOrderParams).dateRange!!.end.toString() } catch (e: Exception) { LOGGER.debug("Asked to iterate over history with NO end date; default to now") - getGregorianCalendarNow().toString() + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()).toString() } ) { @@ -758,7 +759,7 @@ private fun handleEbicsUploadTransactionTransmission(requestContext: RequestCont requestObject.body.dataTransfer?.orderData ?: throw EbicsInvalidRequestError() val zippedData = CryptoUtil.decryptEbicsE002( uploadTransaction.transactionKeyEnc.toByteArray(), - encOrderData, + Base64.getDecoder().decode(encOrderData), requestContext.hostEncPriv ) val unzippedData = diff --git a/sandbox/src/main/python/libeufin-cli b/sandbox/src/main/python/libeufin-cli @@ -377,18 +377,74 @@ def hkd(obj, account_id, nexus_base_url): help="Numerical ID of the customer at the Nexus", required=True ) +@click.option( + "--date-range", + help="Date range for the query", + nargs=2, + required=False, +) @click.argument( "nexus-base-url" ) -def c52(obj, account_id, nexus_base_url): - +def c52(obj, account_id, date_range, nexus_base_url): + if date_range is not None and len(date_range) == 2: + req = dict(dateRange=dict(start=date_range[0], end=date_range[1])) + else: + req = dict() url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendC52".format(account_id)) - try: - resp = post(url, json=dict(start="2020-01-15", end="2020-02-03")) - except Exception: - print("Could not reach the bank") - return + resp = post(url, json=req) + print(resp.content.decode("utf-8")) + +@ebics.command(help="Send C53 message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.option( + "--date-range", + help="Date range for the query", + nargs=2, + required=False, +) +@click.argument( + "nexus-base-url" +) +def c53(obj, account_id, date_range, nexus_base_url): + if date_range is not None and len(date_range) == 2: + req = dict(dateRange=dict(start=date_range[0], end=date_range[1])) + else: + req = dict() + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendC53".format(account_id)) + resp = post(url, json=req) + print(resp.content.decode("utf-8")) + + +@ebics.command(help="Send C54 message") +@click.pass_obj +@click.option( + "--account-id", + help="Numerical ID of the customer at the Nexus", + required=True +) +@click.option( + "--date-range", + help="Date range for the query", + nargs=2, + required=False, +) +@click.argument( + "nexus-base-url" +) +def c54(obj, account_id, date_range, nexus_base_url): + if date_range is not None and len(date_range) == 2: + req = dict(dateRange=dict(start=date_range[0], end=date_range[1])) + else: + req = dict() + url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendC54".format(account_id)) + resp = post(url, json=req) print(resp.content.decode("utf-8")) @@ -399,13 +455,24 @@ def c52(obj, account_id, nexus_base_url): help="Numerical ID of the customer at the Nexus", required=True ) +@click.option( + "--date-range", + help="Date range for the query", + nargs=2, + required=False, +) @click.argument( "nexus-base-url" ) -def ptk(obj, account_id, nexus_base_url): +def ptk(obj, account_id, date_range, nexus_base_url): url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendPTK".format(account_id)) + if date_range is not None and len(date_range) == 2: + req = dict(dateRange=dict(start=date_range[0], end=date_range[1])) + else: + req = dict() + print("requesting PTK", repr(req)) try: - resp = post(url, json=dict()) + resp = post(url, json=req) except Exception: print("Could not reach the bank") return @@ -420,13 +487,23 @@ def ptk(obj, account_id, nexus_base_url): help="Numerical ID of the customer at the Nexus", required=True ) +@click.option( + "--date-range", + help="Date range for the query", + nargs=2, + required=False, +) @click.argument( "nexus-base-url" ) -def hac(obj, account_id, nexus_base_url): +def hac(obj, account_id, date_range, nexus_base_url): url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendHAC".format(account_id)) + if date_range is not None and len(date_range) == 2: + req = dict(dateRange=dict(start=date_range[0], end=date_range[1])) + else: + req = dict() try: - resp = post(url) + resp = post(url, json=req) except Exception: print("Could not reach the bank") return @@ -500,6 +577,7 @@ def htd(ctx, account_id, prepare, nexus_base_url): print(resp.content.decode("utf-8")) + @ebics.command(help="Send HIA message") @click.pass_obj @click.option( @@ -519,6 +597,7 @@ def hia(obj, account_id, nexus_base_url): return print(resp.content.decode("utf-8")) + @ebics.command(help="Send HPB message") @click.pass_obj @click.option( @@ -610,6 +689,7 @@ def new_subscriber(obj, account_id, user_id, partner_id, system_id, host_id, ebi print(resp.content.decode("utf-8")) + @native.command(help="Ask the list of transactions related to one account") @click.option( "--user-id", diff --git a/util/src/main/kotlin/CryptoUtil.kt b/util/src/main/kotlin/CryptoUtil.kt @@ -203,7 +203,6 @@ object CryptoUtil { ) val ivParameterSpec = IvParameterSpec(ByteArray(16)) symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - LOGGER.debug("decrypting: ${encryptedData.toHexString()}") val data = symmetricCipher.doFinal(encryptedData) return data } diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -0,0 +1,542 @@ +/* + * 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 + * <http://www.gnu.org/licenses/> + */ + +/** + * 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.util + +import tech.libeufin.util.ebics_h004.* +import tech.libeufin.util.ebics_hev.HEVRequest +import tech.libeufin.util.ebics_hev.HEVResponse +import tech.libeufin.util.ebics_s001.UserSignatureData +import java.math.BigInteger +import java.security.SecureRandom +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.time.LocalDate +import java.util.* +import java.util.zip.DeflaterInputStream +import javax.xml.datatype.DatatypeFactory + +class InvalidSubscriberStateError : Exception("Invalid EBICS subscriber state") +class InvalidXmlError : Exception("Invalid EBICS XML") +class BadSignatureError : Exception("Invalid EBICS XML Signature") +class EbicsUnknownReturnCodeError(msg: String) : Exception(msg) + +data class EbicsDateRange(val start: LocalDate, val end: LocalDate) + +sealed class EbicsOrderParams + +data class EbicsStandardOrderParams( + val dateRange: EbicsDateRange? = null +) : EbicsOrderParams() + +data class EbicsGenericOrderParams( + val params: Map<String, String> = mapOf() +) : EbicsOrderParams() + +/** + * This class is a mere container that keeps data found + * in the database and that is further needed to sign / verify + * / make messages. And not all the values are needed all + * the time. + */ +data class EbicsClientSubscriberDetails( + val partnerId: String, + val userId: String, + var bankAuthPub: RSAPublicKey?, + var bankEncPub: RSAPublicKey?, + val ebicsUrl: String, + val hostId: String, + val customerEncPriv: RSAPrivateCrtKey, + val customerAuthPriv: RSAPrivateCrtKey, + val customerSignPriv: RSAPrivateCrtKey +) + +/** + * @param size in bits + */ +private fun getNonce(size: Int): ByteArray { + val sr = SecureRandom() + val ret = ByteArray(size / 8) + sr.nextBytes(ret) + return ret +} + +private fun makeOrderParams(orderParams: EbicsOrderParams): EbicsRequest.OrderParams { + return when (orderParams) { + is EbicsStandardOrderParams -> { + EbicsRequest.StandardOrderParams().apply { + val r = orderParams.dateRange + if (r != null) { + this.dateRange = EbicsRequest.DateRange().apply { + this.start = DatatypeFactory.newInstance().newXMLGregorianCalendar(r.start.toString()) + this.end = DatatypeFactory.newInstance().newXMLGregorianCalendar(r.end.toString()) + } + } + } + } + is EbicsGenericOrderParams -> { + EbicsRequest.GenericOrderParams().apply { + this.parameterList = orderParams.params.map { entry -> + EbicsTypes.Parameter().apply { + this.name = entry.key + this.value = entry.value + this.type = "string" + } + } + } + } + else -> { + throw NotImplementedError() + } + } +} + + +private fun signOrder( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): UserSignatureData { + val ES_signature = CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ) + val userSignatureData = UserSignatureData().apply { + orderSignatureList = listOf( + UserSignatureData.OrderSignatureData().apply { + signatureVersion = "A006" + signatureValue = ES_signature + partnerID = partnerId + userID = userId + } + ) + } + return userSignatureData +} + + +fun createEbicsRequestForDownloadReceipt( + subscriberDetails: EbicsClientSubscriberDetails, + transactionID: String +): String { + val req = EbicsRequest.createForDownloadReceiptPhase(transactionID, subscriberDetails.hostId) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + return XMLUtil.convertDomToString(doc) +} + +data class PreparedUploadData( + val transactionKey: ByteArray, + val userSignatureDataEncrypted: 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 + } +} + +fun prepareUploadPayload(subscriberDetails: EbicsClientSubscriberDetails, payload: ByteArray): PreparedUploadData { + val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002( + EbicsOrderUtil.encodeOrderDataXml( + signOrder( + payload, + subscriberDetails.customerSignPriv, + subscriberDetails.partnerId, + subscriberDetails.userId + ) + ), + subscriberDetails.bankEncPub!! + ) + val compressedInnerPayload = DeflaterInputStream( + payload.inputStream() + ).use { it.readAllBytes() } + val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( + compressedInnerPayload, + subscriberDetails.bankEncPub!!, + userSignatureDataEncrypted.plainTransactionKey!! + ) + val encodedEncryptedPayload = Base64.getEncoder().encodeToString(encryptedPayload.encryptedData) + return PreparedUploadData( + userSignatureDataEncrypted.encryptedTransactionKey, + userSignatureDataEncrypted.encryptedData, + listOf(encodedEncryptedPayload) + ) +} + +/** + * Create an EBICS request for the initialization phase of an upload EBICS transaction. + * + * The payload is only passed to generate the signature. + */ +fun createEbicsRequestForUploadInitialization( + subscriberDetails: EbicsClientSubscriberDetails, + orderType: String, + orderParams: EbicsOrderParams, + preparedUploadData: PreparedUploadData +): String { + val req = EbicsRequest.createForUploadInitializationPhase( + preparedUploadData.transactionKey, + preparedUploadData.userSignatureDataEncrypted, + subscriberDetails.hostId, + getNonce(128), + subscriberDetails.partnerId, + subscriberDetails.userId, + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + subscriberDetails.bankAuthPub!!, + subscriberDetails.bankEncPub!!, + BigInteger.ONE, + orderType, + makeOrderParams(orderParams) + ) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + return XMLUtil.convertDomToString(doc) +} + + +fun createEbicsRequestForDownloadInitialization( + subscriberDetails: EbicsClientSubscriberDetails, + orderType: String, + orderParams: EbicsOrderParams +): String { + val req = EbicsRequest.createForDownloadInitializationPhase( + subscriberDetails.userId, + subscriberDetails.partnerId, + subscriberDetails.hostId, + getNonce(128), + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()), + subscriberDetails.bankEncPub ?: throw InvalidSubscriberStateError(), + subscriberDetails.bankAuthPub ?: throw InvalidSubscriberStateError(), + orderType, + makeOrderParams(orderParams) + ) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + return XMLUtil.convertDomToString(doc) +} + + +fun createEbicsRequestForUploadTransferPhase( + subscriberDetails: EbicsClientSubscriberDetails, + transactionID: String, + preparedUploadData: PreparedUploadData, + chunkIndex: Int +): String { + val req = EbicsRequest.createForUploadTransferPhase( + subscriberDetails.hostId, + transactionID, + // chunks are 1-indexed + BigInteger.valueOf(chunkIndex.toLong() + 1), + preparedUploadData.encryptedPayloadChunks[chunkIndex] + ) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + return XMLUtil.convertDomToString(doc) +} + +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 + } +} + + +@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_NO_DOWNLOAD_DATA_AVAILABLE("090005"); + + companion object { + fun lookup(errorCode: String): EbicsReturnCode { + for (x in values()) { + if (x.errorCode == errorCode) { + return x; + } + } + throw EbicsUnknownReturnCodeError("Unknown return code: $errorCode") + } + } +} + +data class EbicsResponseContent( + val transactionID: String?, + val dataEncryptionInfo: DataEncryptionInfo?, + val orderDataEncChunk: String?, + val technicalReturnCode: EbicsReturnCode, + val bankReturnCode: EbicsReturnCode +) + +data class EbicsKeyManagementResponseContent( + val technicalReturnCode: EbicsReturnCode, + val bankReturnCode: EbicsReturnCode?, + val orderData: ByteArray? +) + +fun parseAndDecryptEbicsKeyManagementResponse( + subscriberDetails: EbicsClientSubscriberDetails, + responseStr: String +): EbicsKeyManagementResponseContent { + val resp = try { + XMLUtil.convertStringToJaxb<EbicsKeyManagementResponse>(responseStr) + } catch (e: Exception) { + throw InvalidXmlError() + } + val retCode = EbicsReturnCode.lookup(resp.value.header.mutable.returnCode) + + val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo + val orderData = if (daeXml != null) { + val dae = DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) + val encOrderData = resp.value.body.dataTransfer?.orderData?.value ?: throw InvalidXmlError() + decryptAndDecompressResponse(subscriberDetails, dae, listOf(encOrderData)) + } else { + null + } + + val bankReturnCodeStr = resp.value.body.returnCode.value + val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) + + return EbicsKeyManagementResponseContent(retCode, bankReturnCode, orderData) +} + +class HpbResponseData( + val hostID: String, + val encryptionPubKey: RSAPublicKey, + val encryptionVersion: String, + val authenticationPubKey: RSAPublicKey, + val authenticationVersion: String +) + +fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData { + val resp = try { + XMLUtil.convertStringToJaxb<HPBResponseOrderData>(orderDataRaw.toString(Charsets.UTF_8)) + } catch (e: Exception) { + throw InvalidXmlError() + } + val encPubKey = CryptoUtil.loadRsaPublicKeyFromComponents( + resp.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + resp.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + val authPubKey = CryptoUtil.loadRsaPublicKeyFromComponents( + resp.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + resp.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + return HpbResponseData( + hostID = resp.value.hostID, + encryptionPubKey = encPubKey, + encryptionVersion = resp.value.encryptionPubKeyInfo.encryptionVersion, + authenticationPubKey = authPubKey, + authenticationVersion = resp.value.authenticationPubKeyInfo.authenticationVersion + ) +} + +fun parseAndValidateEbicsResponse( + subscriberDetails: EbicsClientSubscriberDetails, + responseStr: String +): EbicsResponseContent { + val responseDocument = try { + XMLUtil.parseStringIntoDom(responseStr) + } catch (e: Exception) { + throw InvalidXmlError() + } + + if (!XMLUtil.verifyEbicsDocument( + responseDocument, + subscriberDetails.bankAuthPub ?: throw InvalidSubscriberStateError() + ) + ) { + throw BadSignatureError() + } + val resp = try { + XMLUtil.convertStringToJaxb<EbicsResponse>(responseStr) + } catch (e: Exception) { + throw InvalidXmlError() + } + + val bankReturnCodeStr = resp.value.body.returnCode.value + val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) + + val techReturnCodeStr = resp.value.header.mutable.returnCode + val techReturnCode = EbicsReturnCode.lookup(techReturnCodeStr) + + val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo + val dataEncryptionInfo = if (daeXml == null) { + null + } else { + DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) + } + + return EbicsResponseContent( + transactionID = resp.value.header._static.transactionID, + bankReturnCode = bankReturnCode, + technicalReturnCode = techReturnCode, + orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, + dataEncryptionInfo = dataEncryptionInfo + ) +} + +/** + * Get the private key that matches the given public key digest. + */ +fun getDecryptionKey(subscriberDetails: EbicsClientSubscriberDetails, pubDigest: ByteArray): RSAPrivateCrtKey { + val authPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerAuthPriv) + val encPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv) + val authPubDigest = CryptoUtil.getEbicsPublicKeyHash(authPub) + val encPubDigest = CryptoUtil.getEbicsPublicKeyHash(encPub) + if (pubDigest.contentEquals(authPubDigest)) { + return subscriberDetails.customerAuthPriv + } + if (pubDigest.contentEquals(encPubDigest)) { + return subscriberDetails.customerEncPriv + } + throw Exception("no matching private key to decrypt response") +} + +/** + * Wrapper around the lower decryption routine, that takes a EBICS response + * object containing a encrypted payload, and return the plain version of it + * (including decompression). + */ +fun decryptAndDecompressResponse( + subscriberDetails: EbicsClientSubscriberDetails, + encryptionInfo: DataEncryptionInfo, + chunks: List<String> +): ByteArray { + val privateKey = getDecryptionKey(subscriberDetails, encryptionInfo.bankPubDigest) + val buf = StringBuilder() + chunks.forEach { buf.append(it) } + val decoded = Base64.getDecoder().decode(buf.toString()) + val er = CryptoUtil.EncryptionResult( + encryptionInfo.transactionKey, + encryptionInfo.bankPubDigest, + decoded + ) + val dataCompr = CryptoUtil.decryptEbicsE002( + er, + privateKey + ) + return EbicsOrderUtil.decodeOrderData(dataCompr) +} + +data class EbicsVersionSpec( + val protocol: String, + val version: String +) + +data class EbicsHevDetails( + val versions: List<EbicsVersionSpec> +) + +fun makeEbicsHEVRequest(subscriberDetails: EbicsClientSubscriberDetails): String { + val req = HEVRequest().apply { + hostId = subscriberDetails.hostId + } + val doc = XMLUtil.convertJaxbToDocument(req) + return XMLUtil.convertDomToString(doc) +} + +fun parseEbicsHEVResponse(subscriberDetails: EbicsClientSubscriberDetails, respStr: String): EbicsHevDetails { + val resp = try { + XMLUtil.convertStringToJaxb<HEVResponse>(respStr) + } catch (e: Exception) { + logger.error("Exception while parsing HEV response", e) + throw InvalidXmlError() + } + val versions = resp.value.versionNumber.map { versionNumber -> + EbicsVersionSpec(versionNumber.protocolVersion, versionNumber.value) + } + return EbicsHevDetails(versions) +} + +fun makeEbicsIniRequest(subscriberDetails: EbicsClientSubscriberDetails): String { + val iniRequest = EbicsUnsecuredRequest.createIni( + subscriberDetails.hostId, + subscriberDetails.userId, + subscriberDetails.partnerId, + subscriberDetails.customerSignPriv + ) + val doc = XMLUtil.convertJaxbToDocument(iniRequest) + return XMLUtil.convertDomToString(doc) +} + +fun makeEbicsHiaRequest(subscriberDetails: EbicsClientSubscriberDetails): String { + val hiaRequest = EbicsUnsecuredRequest.createHia( + subscriberDetails.hostId, + subscriberDetails.userId, + subscriberDetails.partnerId, + subscriberDetails.customerAuthPriv, + subscriberDetails.customerEncPriv + ) + val doc = XMLUtil.convertJaxbToDocument(hiaRequest) + return XMLUtil.convertDomToString(doc) +} + +fun makeEbicsHpbRequest(subscriberDetails: EbicsClientSubscriberDetails): String { + val hpbRequest = EbicsNpkdRequest.createRequest( + subscriberDetails.hostId, + subscriberDetails.partnerId, + subscriberDetails.userId, + getNonce(128), + DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()) + ) + val doc = XMLUtil.convertJaxbToDocument(hpbRequest) + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + return XMLUtil.convertDomToString(doc) +} +\ No newline at end of file diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt @@ -21,6 +21,8 @@ package tech.libeufin.util import com.sun.org.apache.xerces.internal.dom.DOMInputImpl import com.sun.xml.bind.marshaller.NamespacePrefixMapper +import org.slf4j.Logger +import org.slf4j.LoggerFactory import org.w3c.dom.Document import org.w3c.dom.Node import org.w3c.dom.NodeList @@ -60,6 +62,8 @@ import javax.xml.xpath.XPath import javax.xml.xpath.XPathConstants import javax.xml.xpath.XPathFactory +val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") + class DefaultNamespaces : NamespacePrefixMapper() { override fun getPreferredPrefix(namespaceUri: String?, suggestion: String?, requirePrefix: Boolean): String? { if (namespaceUri == "http://www.w3.org/2000/09/xmldsig#") return "ds" @@ -171,7 +175,7 @@ class XMLUtil private constructor() { try { getEbicsValidator().validate(xmlDoc) } catch (e: Exception) { - LOGGER.warn("Validation failed: ${e}") + logger.warn("Validation failed: ${e}") return false } return true; diff --git a/util/src/main/kotlin/ebics_h004/EbicsKeyManagementResponse.kt b/util/src/main/kotlin/ebics_h004/EbicsKeyManagementResponse.kt @@ -97,6 +97,6 @@ class EbicsKeyManagementResponse { @XmlAccessorType(XmlAccessType.NONE) class OrderData { @get:XmlValue - lateinit var value: ByteArray + lateinit var value: String } } diff --git a/util/src/main/kotlin/ebics_h004/EbicsRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -230,7 +230,7 @@ class EbicsRequest { var signatureData: SignatureData? = null @get:XmlElement(name = "OrderData") - var orderData: ByteArray? = null + var orderData: String? = null @get:XmlElement(name = "HostID") var hostId: String? = null @@ -317,7 +317,6 @@ class EbicsRequest { } - fun createForDownloadInitializationPhase( userId: String, partnerId: String, @@ -326,7 +325,8 @@ class EbicsRequest { date: XMLGregorianCalendar, bankEncPub: RSAPublicKey, bankAuthPub: RSAPublicKey, - aOrderType: String + myOrderType: String, + myOrderParams: OrderParams ): EbicsRequest { return EbicsRequest().apply { version = "H004" @@ -343,9 +343,9 @@ class EbicsRequest { timestamp = date partnerID = partnerId orderDetails = OrderDetails().apply { - orderType = aOrderType + orderType = myOrderType orderAttribute = "DZHNN" - orderParams = StandardOrderParams() + orderParams = myOrderParams } bankPubKeyDigests = BankPubKeyDigests().apply { authentication = EbicsTypes.PubKeyDigest().apply { @@ -370,7 +370,8 @@ class EbicsRequest { } fun createForUploadInitializationPhase( - cryptoBundle: CryptoUtil.EncryptionResult, + encryptedTransactionKey: ByteArray, + encryptedSignatureData: ByteArray, hostId: String, nonceArg: ByteArray, partnerId: String, @@ -379,7 +380,8 @@ class EbicsRequest { bankAuthPub: RSAPublicKey, bankEncPub: RSAPublicKey, segmentsNumber: BigInteger, - aOrderType: String + aOrderType: String, + aOrderParams: OrderParams ): EbicsRequest { return EbicsRequest().apply { @@ -396,7 +398,7 @@ class EbicsRequest { orderDetails = OrderDetails().apply { orderType = aOrderType orderAttribute = "OZHNN" - orderParams = StandardOrderParams() + orderParams = aOrderParams } bankPubKeyDigests = BankPubKeyDigests().apply { authentication = EbicsTypes.PubKeyDigest().apply { @@ -423,10 +425,10 @@ class EbicsRequest { dataTransfer = DataTransfer().apply { signatureData = SignatureData().apply { authenticate = true - value = cryptoBundle.encryptedData + value = encryptedSignatureData } dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { - transactionKey = cryptoBundle.encryptedTransactionKey + transactionKey = encryptedTransactionKey authenticate = true encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" @@ -443,10 +445,8 @@ class EbicsRequest { hostId: String, transactionId: String, segNumber: BigInteger, - encryptedData: ByteArray - + encryptedData: String ): EbicsRequest { - return EbicsRequest().apply { header = Header().apply { version = "H004" diff --git a/util/src/main/kotlin/ebics_hev/EbicsMessages.kt b/util/src/main/kotlin/ebics_hev/EbicsMessages.kt @@ -19,6 +19,7 @@ package tech.libeufin.util.ebics_hev +import java.util.* import javax.xml.bind.annotation.* import javax.xml.bind.annotation.adapters.CollapsedStringAdapter import javax.xml.bind.annotation.adapters.NormalizedStringAdapter @@ -45,7 +46,7 @@ class HEVResponse { lateinit var systemReturnCode: SystemReturnCodeType @get:XmlElement(name = "VersionNumber", namespace = "http://www.ebics.org/H000") - var versionNumber: List<VersionNumber>? = null + var versionNumber: List<VersionNumber> = LinkedList() @get:XmlAnyElement(lax = true) var any: List<Any>? = null diff --git a/util/src/main/kotlin/logger.kt b/util/src/main/kotlin/logger.kt @@ -1,6 +0,0 @@ -package tech.libeufin.util - -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -val LOGGER: Logger = LoggerFactory.getLogger("tech.libeufin.util") -\ No newline at end of file diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt @@ -1,19 +0,0 @@ -package tech.libeufin.util - -import java.util.* -import javax.xml.datatype.DatatypeFactory -import javax.xml.datatype.XMLGregorianCalendar - -/* now */ -fun getGregorianCalendarNow(): XMLGregorianCalendar { - val gregorianCalendar = GregorianCalendar() - val datatypeFactory = DatatypeFactory.newInstance() - return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) -} - -/* explicit point in time */ -fun getGregorianCalendar(year: Int, month: Int, day: Int): XMLGregorianCalendar { - val gregorianCalendar = GregorianCalendar(year, month, day) - val datatypeFactory = DatatypeFactory.newInstance() - return datatypeFactory.newXMLGregorianCalendar(gregorianCalendar) -} -\ No newline at end of file diff --git a/util/src/test/kotlin/SignatureDataTest.kt b/util/src/test/kotlin/SignatureDataTest.kt @@ -4,8 +4,9 @@ import tech.libeufin.util.CryptoUtil import tech.libeufin.util.XMLUtil import tech.libeufin.util.ebics_h004.EbicsRequest import tech.libeufin.util.ebics_h004.EbicsTypes -import tech.libeufin.util.getGregorianCalendarNow import java.math.BigInteger +import java.util.* +import javax.xml.datatype.DatatypeFactory class SignatureDataTest { @@ -22,7 +23,7 @@ class SignatureDataTest { static = EbicsRequest.StaticHeaderType().apply { hostID = "some host ID" nonce = "nonce".toByteArray() - timestamp = getGregorianCalendarNow() + timestamp = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()) partnerID = "some partner ID" userID = "some user ID" orderDetails = EbicsRequest.OrderDetails().apply {