[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: refactor EBICS protocol into nicer int
From: |
gnunet |
Subject: |
[libeufin] branch master updated: refactor EBICS protocol into nicer interface, unzip responses, allow date ranges |
Date: |
Tue, 11 Feb 2020 11:37:13 +0100 |
This is an automated email from the git hooks/post-receive script.
dold pushed a commit to branch master
in repository libeufin.
The following commit(s) were added to refs/heads/master by this push:
new b57e69e refactor EBICS protocol into nicer interface, unzip
responses, allow date ranges
b57e69e is described below
commit b57e69e8cd4fd4f8d793e5698bcb864626382384
Author: Florian Dold <address@hidden>
AuthorDate: Tue Feb 11 11:36:52 2020 +0100
refactor EBICS protocol into nicer interface, unzip responses, allow date
ranges
---
nexus/build.gradle | 1 +
.../main/kotlin/tech/libeufin/nexus/EbicsClient.kt | 285 +++++------
.../src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 138 ------
nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 61 ++-
nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 483 +++++++++++-------
.../tech/libeufin/sandbox/EbicsProtocolBackend.kt | 9 +-
sandbox/src/main/python/libeufin-cli | 102 +++-
util/src/main/kotlin/CryptoUtil.kt | 1 -
util/src/main/kotlin/Ebics.kt | 542 +++++++++++++++++++++
util/src/main/kotlin/XMLUtil.kt | 6 +-
.../ebics_h004/EbicsKeyManagementResponse.kt | 2 +-
util/src/main/kotlin/ebics_h004/EbicsRequest.kt | 26 +-
util/src/main/kotlin/ebics_hev/EbicsMessages.kt | 3 +-
util/src/main/kotlin/logger.kt | 6 -
util/src/main/kotlin/time.kt | 19 -
util/src/test/kotlin/SignatureDataTest.kt | 5 +-
16 files changed, 1119 insertions(+), 570 deletions(-)
diff --git a/nexus/build.gradle b/nexus/build.gradle
index f33be92..849283d 100644
--- 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
index 4245ba9..a0f7dde 100644
--- 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
index ed65340..e17c550 100644
--- 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
index dd63042..dcd6fd6 100644
--- 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
index b88819d..d32f31d 100644
--- 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
index 4f20ef0..670ec69 100644
--- 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
index 9a4fe52..4f35dbf 100755
--- 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
index 752c0e9..b73dee4 100644
--- 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
new file mode 100644
index 0000000..0d0c7c9
--- /dev/null
+++ 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
index a61b345..f4de82a 100644
--- 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
index d26bc4e..6db6eab 100644
--- 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
index d6ded86..71f5f8e 100644
--- 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
index 5b9ff71..0d4302b 100644
--- 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
deleted file mode 100644
index f42b470..0000000
--- a/util/src/main/kotlin/logger.kt
+++ /dev/null
@@ -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
deleted file mode 100644
index 9db12d8..0000000
--- a/util/src/main/kotlin/time.kt
+++ /dev/null
@@ -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
index c4239cc..2c4669d 100644
--- 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 {
--
To stop receiving notification emails like this one, please contact
address@hidden.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: refactor EBICS protocol into nicer interface, unzip responses, allow date ranges,
gnunet <=