summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-06-29 10:40:06 +0200
committerMS <ms@taler.net>2023-06-29 10:43:57 +0200
commit06452b9adc4d149bdb1532a3ea3160909eb51c9a (patch)
tree68a63bf6cd3788409c897c2e2e57ca06c421c1cb
parente0512009c7e66e9882a1d5a741c1183b8ebc21dc (diff)
downloadlibeufin-0.9.3-dev.3.tar.gz
libeufin-0.9.3-dev.3.tar.bz2
libeufin-0.9.3-dev.3.zip
EBICS 3.v0.9.3-dev.3
Getting to both upload and download documents to/from the PostFinance test platform.
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt92
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt68
-rw-r--r--nexus/src/test/kotlin/EbicsTest.kt69
-rw-r--r--nexus/src/test/kotlin/PostFinance.kt31
-rw-r--r--util/src/main/kotlin/Ebics.kt457
-rw-r--r--util/src/main/kotlin/XMLUtil.kt22
-rw-r--r--util/src/main/kotlin/ebics_h005/Ebics3Request.kt48
-rw-r--r--util/src/main/kotlin/ebics_h005/Ebics3Response.kt351
-rw-r--r--util/src/main/kotlin/ebics_h005/Ebics3Types.kt1
-rw-r--r--util/src/main/kotlin/ebics_h005/package-info.java1
-rw-r--r--util/src/main/kotlin/ebics_s002/SignatureTypes.kt91
-rw-r--r--util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt27
-rw-r--r--util/src/main/kotlin/ebics_s002/package-info.java13
13 files changed, 1009 insertions, 262 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
index 8bd1db6c..f7944d55 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -112,16 +112,31 @@ suspend fun doEbicsDownloadTransaction(
): EbicsDownloadResult {
// Initialization phase
- val initDownloadRequestStr = createEbicsRequestForDownloadInitialization(
- subscriberDetails,
- fetchSpec.orderType,
- fetchSpec.orderParams,
- fetchSpec.ebics3Service
- )
+ val initDownloadRequestStr = if (fetchSpec.isEbics3) {
+ if (fetchSpec.ebics3Service == null)
+ throw internalServerError("Expected EBICS 3 fetch spec but null was found.")
+ createEbicsRequestForDownloadInitialization(
+ subscriberDetails,
+ fetchSpec.ebics3Service,
+ fetchSpec.orderParams
+ )
+ }
+ else {
+ if (fetchSpec.orderType == null)
+ throw internalServerError("Expected EBICS 2.5 order type but null was found.")
+ createEbicsRequestForDownloadInitialization(
+ subscriberDetails,
+ fetchSpec.orderType,
+ fetchSpec.orderParams
+ )
+ }
val payloadChunks = LinkedList<String>()
val initResponseStr = client.postToBank(subscriberDetails.ebicsUrl, initDownloadRequestStr)
- val initResponse = parseAndValidateEbicsResponse(subscriberDetails, initResponseStr)
-
+ val initResponse = parseAndValidateEbicsResponse(
+ subscriberDetails,
+ initResponseStr,
+ withEbics3 = fetchSpec.isEbics3
+ )
val transactionID: String? = initResponse.transactionID
// Checking for EBICS communication problems.
when (initResponse.technicalReturnCode) {
@@ -192,7 +207,8 @@ suspend fun doEbicsDownloadTransaction(
subscriberDetails,
transactionID,
x,
- numSegments
+ numSegments,
+ fetchSpec.isEbics3
)
logger.debug("EBICS download transfer phase of ${transactionID}: sending segment $x")
val transferResponseStr = client.postToBank(subscriberDetails.ebicsUrl, transferReqStr)
@@ -236,13 +252,18 @@ suspend fun doEbicsDownloadTransaction(
// Acknowledgement phase
val ackRequest = createEbicsRequestForDownloadReceipt(
subscriberDetails,
- transactionID
+ transactionID,
+ fetchSpec.isEbics3
)
val ackResponseStr = client.postToBank(
subscriberDetails.ebicsUrl,
ackRequest
)
- val ackResponse = parseAndValidateEbicsResponse(subscriberDetails, ackResponseStr)
+ val ackResponse = parseAndValidateEbicsResponse(
+ subscriberDetails,
+ ackResponseStr,
+ withEbics3 = fetchSpec.isEbics3
+ )
when (ackResponse.technicalReturnCode) {
EbicsReturnCode.EBICS_DOWNLOAD_POSTPROCESS_DONE -> {
}
@@ -263,28 +284,46 @@ suspend fun doEbicsDownloadTransaction(
suspend fun doEbicsUploadTransaction(
client: HttpClient,
subscriberDetails: EbicsClientSubscriberDetails,
- orderType: String? = null,
- payload: ByteArray,
- orderParams: EbicsOrderParams,
- ebics3OrderService: Ebics3Request.OrderDetails.Service? = null
+ uploadSpec: EbicsUploadSpec,
+ payload: ByteArray
) {
if (subscriberDetails.bankEncPub == null) {
throw NexusError(HttpStatusCode.BadRequest,
"bank encryption key unknown, request HPB first"
)
}
- val preparedUploadData = prepareUploadPayload(subscriberDetails, payload)
- val req = createEbicsRequestForUploadInitialization(
+ val preparedUploadData = prepareUploadPayload(
subscriberDetails,
- orderType,
- orderParams,
- preparedUploadData,
- ebics3OrderService = ebics3OrderService
+ payload,
+ isEbics3 = uploadSpec.isEbics3
)
+ val req: String = if (uploadSpec.isEbics3) {
+ if (uploadSpec.ebics3Service == null)
+ throw internalServerError("EBICS 3 service data was expected, but null was found.")
+ createEbicsRequestForUploadInitialization(
+ subscriberDetails,
+ uploadSpec.ebics3Service,
+ uploadSpec.orderParams,
+ preparedUploadData
+ )
+ } else {
+ if (uploadSpec.orderType == null)
+ throw internalServerError("EBICS 2.5 order type was expected, but null was found.")
+ createEbicsRequestForUploadInitialization(
+ subscriberDetails,
+ uploadSpec.orderType,
+ uploadSpec.orderParams ?: EbicsStandardOrderParams(),
+ preparedUploadData
+ )
+ }
logger.debug("EBICS upload message to: ${subscriberDetails.ebicsUrl}")
val responseStr = client.postToBank(subscriberDetails.ebicsUrl, req)
- val initResponse = parseAndValidateEbicsResponse(subscriberDetails, responseStr)
+ val initResponse = parseAndValidateEbicsResponse(
+ subscriberDetails,
+ responseStr,
+ withEbics3 = uploadSpec.isEbics3
+ )
// The bank indicated one error, hence Nexus sent invalid data.
if (initResponse.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
throw NexusError(
@@ -311,13 +350,18 @@ suspend fun doEbicsUploadTransaction(
subscriberDetails,
transactionID,
preparedUploadData,
- 0
+ 0,
+ withEbics3 = uploadSpec.isEbics3
)
val txRespStr = client.postToBank(
subscriberDetails.ebicsUrl,
ebicsPayload
)
- val txResp = parseAndValidateEbicsResponse(subscriberDetails, txRespStr)
+ val txResp = parseAndValidateEbicsResponse(
+ subscriberDetails,
+ txRespStr,
+ withEbics3 = uploadSpec.isEbics3
+ )
when (txResp.technicalReturnCode) {
EbicsReturnCode.EBICS_OK -> {/* do nothing */}
else -> {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
index d1c44e0f..5e1c14d1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -78,26 +78,21 @@ data class EbicsFetchSpec(
val ebics3Service: Ebics3Request.OrderDetails.Service? = null, // unused for 2.5
// Not always available, for example at raw POST /download/${ebicsMessageName} calls.
// It helps to trace back the original level.
- val originalLevel: FetchLevel? = null
+ val originalLevel: FetchLevel? = null,
+ val isEbics3: Boolean = false
)
/**
- * Maps EBICS specific history types to their camt
- * counterparts. That allows the database to store
- * camt types per-se, without any reference to the
- * EBICS message that brought them. For example, a
- * EBICS "Z52" and "C52" will both bring a camt.052.
- * Such camt.052 is associated with the more generic
- * type of FetchLevel.REPORT.
+ * Collects EBICS 2.5 and/or 3.0 parameters for a unified
+ * way of passing parameters. Individual helpers will then
+ * act according to the EBICS version.
*/
-private fun getFetchLevelFromEbicsOrder(ebicsHistoryType: String): FetchLevel {
- return when(ebicsHistoryType) {
- "C52", "Z52" -> FetchLevel.REPORT
- "C53", "Z53" -> FetchLevel.STATEMENT
- "C54", "Z54" -> FetchLevel.NOTIFICATION
- else -> throw internalServerError("EBICS history type '$ebicsHistoryType' not supported")
- }
-}
+data class EbicsUploadSpec(
+ val isEbics3: Boolean = false,
+ val ebics3Service: Ebics3Request.OrderDetails.Service? = null, // unused for 2.5
+ val orderType: String? = null,
+ val orderParams: EbicsOrderParams? = null
+)
// Validate and store the received document for later ingestion.
private fun validateAndStoreCamt(
@@ -206,7 +201,6 @@ private suspend fun fetchEbicsTransactions(
// re-throw in any other error case.
throw e
}
-
handleEbicsDownloadResult(
response,
bankConnectionId,
@@ -523,10 +517,17 @@ private fun getNotificationSpecAfterDialect(dialect: String? = null, p: EbicsOrd
orderParams = p,
ebics3Service = Ebics3Request.OrderDetails.Service().apply {
serviceName = "REP"
- messageName = "camt.054"
+ messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+ value = "camt.054"
+ version = "04"
+ }
scope = "CH"
+ container = Ebics3Request.OrderDetails.Service.Container().apply {
+ containerType = "ZIP"
+ }
},
- originalLevel = FetchLevel.NOTIFICATION
+ originalLevel = FetchLevel.NOTIFICATION,
+ isEbics3 = true
)
else -> EbicsFetchSpec(
orderType = "C54",
@@ -737,7 +738,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
subscriberDetails
)
} catch (e: Exception) {
- logger.warn("Fetching transactions (${spec.orderType}) excepted: ${e.message}.")
+ logger.warn("Fetching transactions (${spec.originalLevel}) excepted: ${e.message}.")
errors.add(e)
}
}
@@ -782,20 +783,27 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
val subscriberDetails = subscriberDetails
}
}
- // Used to validate here, then removed for performances reasons.
- doEbicsUploadTransaction(
- httpClient,
- dbData.subscriberDetails,
- getSubmissionTypeAfterDialect(dbData.subscriberDetails.dialect),
- dbData.painXml.toByteArray(Charsets.UTF_8),
- EbicsStandardOrderParams(),
- ebics3OrderService = if (dbData.subscriberDetails.dialect == "pf") {
+ val isPoFi = dbData.subscriberDetails.dialect == "pf"
+ val uploadSpec = EbicsUploadSpec(
+ isEbics3 = isPoFi,
+ orderType = if (!isPoFi) getSubmissionTypeAfterDialect(dbData.subscriberDetails.dialect) else null,
+ orderParams = EbicsStandardOrderParams(),
+ ebics3Service = if (isPoFi)
Ebics3Request.OrderDetails.Service().apply {
serviceName = "MCT"
scope = "CH"
- messageName = "pain.001"
+ messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+ value = "pain.001"
+ version = "09"
+ }
}
- } else null
+ else null
+ )
+ doEbicsUploadTransaction(
+ httpClient,
+ dbData.subscriberDetails,
+ uploadSpec,
+ dbData.painXml.toByteArray(Charsets.UTF_8)
)
transaction {
val payment = getPaymentInitiation(paymentInitiationId)
diff --git a/nexus/src/test/kotlin/EbicsTest.kt b/nexus/src/test/kotlin/EbicsTest.kt
index 7fe09f38..19646d6a 100644
--- a/nexus/src/test/kotlin/EbicsTest.kt
+++ b/nexus/src/test/kotlin/EbicsTest.kt
@@ -13,9 +13,7 @@ import tech.libeufin.nexus.*
import tech.libeufin.nexus.bankaccount.addPaymentInitiation
import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations
-import tech.libeufin.nexus.ebics.EbicsBankConnectionProtocol
-import tech.libeufin.nexus.ebics.doEbicsUploadTransaction
-import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails
+import tech.libeufin.nexus.ebics.*
import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
import tech.libeufin.nexus.iso20022.createPain001document
import tech.libeufin.nexus.server.FetchLevel
@@ -204,9 +202,12 @@ class DownloadAndSubmit {
doEbicsUploadTransaction(
client,
unallowedSubscriber,
- "CCT",
- painMessage.toByteArray(Charsets.UTF_8),
- EbicsStandardOrderParams()
+ EbicsUploadSpec(
+ orderType = "CCT",
+ isEbics3 = false,
+ orderParams = EbicsStandardOrderParams()
+ ),
+ painMessage.toByteArray(Charsets.UTF_8)
)
} catch (e: EbicsProtocolError) {
if (e.ebicsTechnicalCode ==
@@ -322,36 +323,56 @@ class DownloadAndSubmit {
class EbicsTest {
+ @Test
+ fun genEbics3Upload() {
+ withTestDatabase {
+ prepNexusDb()
+ val foo = transaction { getEbicsSubscriberDetails("foo") }
+ val uploadDoc = createEbicsRequestForUploadInitialization(
+ subscriberDetails = foo,
+ ebics3OrderService = Ebics3Request.OrderDetails.Service().apply {
+ serviceName = "OTH"
+ scope = "BIL"
+ serviceOption = "CH002LMF"
+ messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+ value = "csv"
+ }
+ },
+ null,
+ prepareUploadPayload(
+ foo,
+ "foo".toByteArray(),
+ isEbics3 = true
+ )
+ )
+ assert(XMLUtil.validateFromString(uploadDoc))
+ }
+ }
+
/**
* Tests the validity of EBICS 3.0 messages.
*/
@Test
- fun genEbics3() {
+ fun genEbics3Download() {
withTestDatabase {
prepNexusDb()
val foo = transaction { getEbicsSubscriberDetails("foo") }
val downloadDoc = createEbicsRequestForDownloadInitialization(
- foo,
- orderType = null, // triggers 3.0
- EbicsStandardOrderParams(),
- Ebics3Request.OrderDetails.Service().apply {
- messageName = "camt.054"
+ subscriberDetails = foo,
+ ebics3OrderService = Ebics3Request.OrderDetails.Service().apply {
+ messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+ value = "camt.054"
+ version = "04"
+ }
scope = "CH"
serviceName = "REP"
- }
+ container = Ebics3Request.OrderDetails.Service.Container().apply {
+ containerType = "ZIP"
+ }
+ },
+ orderParams = EbicsStandardOrderParams()
)
assert(XMLUtil.validateFromString(downloadDoc))
- val uploadDoc = createEbicsRequestForDownloadInitialization(
- foo,
- orderType = null, // triggers 3.0
- EbicsStandardOrderParams(),
- Ebics3Request.OrderDetails.Service().apply {
- messageName = "pain.001"
- scope = "EU"
- serviceName = "MCT"
- }
- )
- assert(XMLUtil.validateFromString(uploadDoc))
}
}
} \ No newline at end of file
diff --git a/nexus/src/test/kotlin/PostFinance.kt b/nexus/src/test/kotlin/PostFinance.kt
index fd798dc1..daec6c77 100644
--- a/nexus/src/test/kotlin/PostFinance.kt
+++ b/nexus/src/test/kotlin/PostFinance.kt
@@ -5,16 +5,18 @@ import org.jetbrains.exposed.sql.transactions.transaction
import tech.libeufin.nexus.bankaccount.addPaymentInitiation
import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
import tech.libeufin.nexus.bankaccount.getBankAccount
+import tech.libeufin.nexus.ebics.EbicsUploadSpec
import tech.libeufin.nexus.ebics.doEbicsUploadTransaction
import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails
import tech.libeufin.nexus.getConnectionPlugin
import tech.libeufin.nexus.getNexusUser
import tech.libeufin.nexus.server.*
import tech.libeufin.util.EbicsStandardOrderParams
+import tech.libeufin.util.ebics_h005.Ebics3Request
import java.io.BufferedReader
import java.io.File
-// Submits a Z54 to the bank, expecting a camt.054 back.
+// Asks a camt.054 to the bank.
private fun downloadPayment() {
val httpClient = HttpClient()
runBlocking {
@@ -42,15 +44,24 @@ private fun uploadQrrPayment() {
doEbicsUploadTransaction(
httpClient,
getEbicsSubscriberDetails("postfinance"),
- "XTC",
- qrr.toByteArray(Charsets.UTF_8),
- EbicsStandardOrderParams()
+ EbicsUploadSpec(
+ ebics3Service = Ebics3Request.OrderDetails.Service().apply {
+ serviceName = "OTH"
+ scope = "BIL"
+ serviceOption = "CH002LMF"
+ messageName = Ebics3Request.OrderDetails.Service.MessageName().apply {
+ value = "csv"
+ }
+ },
+ isEbics3 = true
+ ),
+ qrr.toByteArray(Charsets.UTF_8)
)
}
}
/**
- * Submits a XE2 (+ pain.001 version 2019) message to the bank.
+ * Submits a pain.001 version 2019 message to the bank.
*
* Causes one DBIT payment to show up in the camt.054. This one
* however lacks the AcctSvcrRef, so other ways to pin it are needed.
@@ -75,13 +86,12 @@ private fun uploadPain001Payment() {
}
val ebicsConn = getConnectionPlugin("ebics")
val httpClient = HttpClient()
- runBlocking {
- ebicsConn.submitPaymentInitiation(httpClient, 1L)
- }
+ runBlocking { ebicsConn.submitPaymentInitiation(httpClient, 1L) }
}
fun main() {
// Loads EBICS subscriber's keys from disk.
+ // The keys should be found under libeufin-internal.git/convenience/
val bufferedReader: BufferedReader = File("/tmp/pofi.json").bufferedReader()
val accessDataTxt = bufferedReader.use { it.readText() }
val ebicsConn = getConnectionPlugin("ebics")
@@ -102,6 +112,7 @@ fun main() {
fooBankAccount.iban = "CH9789144829733648596"
}
}
- // uploadQrrPayment()
- // downloadPayment()
+ uploadQrrPayment()
+ downloadPayment()
+ uploadPain001Payment()
} \ No newline at end of file
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
index ae5e5cda..2d90dd2c 100644
--- a/util/src/main/kotlin/Ebics.kt
+++ b/util/src/main/kotlin/Ebics.kt
@@ -29,7 +29,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.ebics_h004.*
import tech.libeufin.util.ebics_h005.Ebics3Request
-import tech.libeufin.util.ebics_h005.Ebics3Types
+import tech.libeufin.util.ebics_h005.Ebics3Response
import tech.libeufin.util.ebics_hev.HEVRequest
import tech.libeufin.util.ebics_hev.HEVResponse
import tech.libeufin.util.ebics_s001.UserSignatureData
@@ -40,6 +40,7 @@ import java.security.interfaces.RSAPublicKey
import java.time.ZonedDateTime
import java.util.*
import java.util.zip.DeflaterInputStream
+import javax.xml.bind.JAXBElement
import javax.xml.datatype.DatatypeFactory
import javax.xml.datatype.XMLGregorianCalendar
@@ -167,15 +168,48 @@ private fun signOrder(
return userSignatureData
}
+private fun signOrderEbics3(
+ orderBlob: ByteArray,
+ signKey: RSAPrivateCrtKey,
+ partnerId: String,
+ userId: String
+): tech.libeufin.util.ebics_s002.UserSignatureDataEbics3 {
+ val ES_signature = CryptoUtil.signEbicsA006(
+ CryptoUtil.digestEbicsOrderA006(orderBlob),
+ signKey
+ )
+ val userSignatureData = tech.libeufin.util.ebics_s002.UserSignatureDataEbics3().apply {
+ orderSignatureList = listOf(
+ tech.libeufin.util.ebics_s002.UserSignatureDataEbics3.OrderSignatureData().apply {
+ signatureVersion = "A006"
+ signatureValue = ES_signature
+ partnerID = partnerId
+ userID = userId
+ }
+ )
+ }
+ return userSignatureData
+}
+
fun createEbicsRequestForDownloadReceipt(
subscriberDetails: EbicsClientSubscriberDetails,
- transactionID: String?
+ transactionID: String?,
+ withEbics3: Boolean = false
): String {
- val req = EbicsRequest.createForDownloadReceiptPhase(
- transactionID,
- subscriberDetails.hostId
- )
- val doc = XMLUtil.convertJaxbToDocument(req)
+ val doc = if (withEbics3) {
+ val req = Ebics3Request.createForDownloadReceiptPhase(
+ transactionID,
+ subscriberDetails.hostId
+ )
+ XMLUtil.convertJaxbToDocument(req)
+
+ } else {
+ val req = EbicsRequest.createForDownloadReceiptPhase(
+ transactionID,
+ subscriberDetails.hostId
+ )
+ XMLUtil.convertJaxbToDocument(req)
+ }
XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv)
return XMLUtil.convertDomToString(doc)
}
@@ -183,6 +217,7 @@ fun createEbicsRequestForDownloadReceipt(
data class PreparedUploadData(
val transactionKey: ByteArray,
val userSignatureDataEncrypted: ByteArray,
+ val dataDigest: ByteArray,
val encryptedPayloadChunks: List<String>
) {
override fun equals(other: Any?): Boolean {
@@ -206,32 +241,90 @@ data class PreparedUploadData(
}
}
-fun prepareUploadPayload(subscriberDetails: EbicsClientSubscriberDetails, payload: ByteArray): PreparedUploadData {
- val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002(
- EbicsOrderUtil.encodeOrderDataXml(
- signOrder(
- payload,
- subscriberDetails.customerSignPriv,
- subscriberDetails.partnerId,
- subscriberDetails.userId
- )
- ),
- subscriberDetails.bankEncPub!!
- )
+fun prepareUploadPayload(
+ subscriberDetails: EbicsClientSubscriberDetails,
+ payload: ByteArray,
+ isEbics3: Boolean = false
+): PreparedUploadData {
+ // First A006-sign the payload, then E002-encrypt with bank's pub.
+ val encryptionResult = if (isEbics3) {
+ val innerSignedEbicsXml = signOrderEbics3( // A006 signature.
+ payload,
+ subscriberDetails.customerSignPriv,
+ subscriberDetails.partnerId,
+ subscriberDetails.userId
+ )
+ val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002(
+ EbicsOrderUtil.encodeOrderDataXml(innerSignedEbicsXml),
+ subscriberDetails.bankEncPub!!
+ )
+ userSignatureDataEncrypted
+ } else {
+ val innerSignedEbicsXml = signOrder( // A006 signature.
+ payload,
+ subscriberDetails.customerSignPriv,
+ subscriberDetails.partnerId,
+ subscriberDetails.userId
+ )
+ val userSignatureDataEncrypted = CryptoUtil.encryptEbicsE002(
+ EbicsOrderUtil.encodeOrderDataXml(innerSignedEbicsXml),
+ subscriberDetails.bankEncPub!!
+ )
+ userSignatureDataEncrypted
+ }
+ // Then only E002 symmetric (with ephemeral key) encrypt.
val compressedInnerPayload = DeflaterInputStream(
payload.inputStream()
).use { it.readAllBytes() }
val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey(
compressedInnerPayload,
subscriberDetails.bankEncPub!!,
- userSignatureDataEncrypted.plainTransactionKey!!
+ encryptionResult.plainTransactionKey!!
)
val encodedEncryptedPayload = Base64.getEncoder().encodeToString(encryptedPayload.encryptedData)
+
return PreparedUploadData(
- userSignatureDataEncrypted.encryptedTransactionKey,
- userSignatureDataEncrypted.encryptedData,
- listOf(encodedEncryptedPayload)
+ encryptionResult.encryptedTransactionKey, // ephemeral key
+ encryptionResult.encryptedData, // bank-pub-encrypted A006 signature.
+ CryptoUtil.digestEbicsOrderA006(payload), // used by EBICS 3
+ listOf(encodedEncryptedPayload) // actual payload E002 encrypted.
+ )
+}
+
+// Creates the EBICS 3 upload init request.
+fun createEbicsRequestForUploadInitialization(
+ subscriberDetails: EbicsClientSubscriberDetails,
+ ebics3OrderService: Ebics3Request.OrderDetails.Service,
+ orderParams: EbicsOrderParams? = null,
+ preparedUploadData: PreparedUploadData
+): String {
+ val nonce = getNonce(128)
+ val req = Ebics3Request.createForUploadInitializationPhase(
+ preparedUploadData.transactionKey,
+ preparedUploadData.userSignatureDataEncrypted,
+ preparedUploadData.dataDigest,
+ subscriberDetails.hostId,
+ nonce,
+ subscriberDetails.partnerId,
+ subscriberDetails.userId,
+ DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
+ subscriberDetails.bankAuthPub!!,
+ subscriberDetails.bankEncPub!!,
+ BigInteger.ONE,
+ ebics3OrderService
+ )
+ val doc = XMLUtil.convertJaxbToDocument(
+ req,
+ withSchemaLocation = "urn:org:ebics:H005 ebics_request_H005.xsd"
+ )
+ logger.debug("Created EBICS 3 document for upload initialization," +
+ " nonce: ${nonce.toHexString()}")
+ XMLUtil.signEbicsDocument(
+ doc,
+ subscriberDetails.customerAuthPriv,
+ withEbics3 = true
)
+ return XMLUtil.convertDomToString(doc)
}
/**
@@ -241,124 +334,96 @@ fun prepareUploadPayload(subscriberDetails: EbicsClientSubscriberDetails, payloa
*/
fun createEbicsRequestForUploadInitialization(
subscriberDetails: EbicsClientSubscriberDetails,
- orderType: String? = null,
+ orderType: String,
orderParams: EbicsOrderParams,
- preparedUploadData: PreparedUploadData,
- ebics3OrderService: Ebics3Request.OrderDetails.Service? = null
+ preparedUploadData: PreparedUploadData
): String {
- // Check if the call is consistent: (only) ONE instruction is expected.
- if (orderType == null && ebics3OrderService == null)
- throw internalServerError("Need exactly one upload instruction but zero was found.")
- if (orderType != null && ebics3OrderService != null)
- throw internalServerError("Need exactly one upload instruction but two were found")
val nonce = getNonce(128)
- val doc = if (orderType != null) {
- val req = EbicsRequest.createForUploadInitializationPhase(
- preparedUploadData.transactionKey,
- preparedUploadData.userSignatureDataEncrypted,
- subscriberDetails.hostId,
- nonce,
- subscriberDetails.partnerId,
- subscriberDetails.userId,
- DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
- subscriberDetails.bankAuthPub!!,
- subscriberDetails.bankEncPub!!,
- BigInteger.ONE,
- orderType,
- makeOrderParams(orderParams)
- )
- XMLUtil.convertJaxbToDocument(req)
- } else {
- val req = Ebics3Request.createForUploadInitializationPhase(
- preparedUploadData.transactionKey,
- preparedUploadData.userSignatureDataEncrypted,
- subscriberDetails.hostId,
- nonce,
- subscriberDetails.partnerId,
- subscriberDetails.userId,
- DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
- subscriberDetails.bankAuthPub!!,
- subscriberDetails.bankEncPub!!,
- BigInteger.ONE,
- ebics3OrderService!!
- )
- XMLUtil.convertJaxbToDocument(req)
- }
- /**
- * FIXME: this log should be made by the caller.
- * That way, all the EBICS transaction steps would be logged in only one function,
- * as opposed to have them spread through the helpers here. This function
- * returning a string blocks now, since the caller should parse and stringify
- * again the message, only to get its nonce.
- */
+ val req = EbicsRequest.createForUploadInitializationPhase(
+ preparedUploadData.transactionKey,
+ preparedUploadData.userSignatureDataEncrypted,
+ subscriberDetails.hostId,
+ nonce,
+ subscriberDetails.partnerId,
+ subscriberDetails.userId,
+ DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
+ subscriberDetails.bankAuthPub!!,
+ subscriberDetails.bankEncPub!!,
+ BigInteger.ONE,
+ orderType,
+ makeOrderParams(orderParams)
+ )
+ val doc = XMLUtil.convertJaxbToDocument(req)
logger.debug("Created EBICS $orderType document for upload initialization," +
" nonce: ${nonce.toHexString()}")
XMLUtil.signEbicsDocument(
doc,
+ subscriberDetails.customerAuthPriv
+ )
+ return XMLUtil.convertDomToString(doc)
+}
+
+// Generates a EBICS 2.5 signed document for the download init phase.
+fun createEbicsRequestForDownloadInitialization(
+ subscriberDetails: EbicsClientSubscriberDetails,
+ orderType: String,
+ orderParams: EbicsOrderParams
+): String {
+ val nonce = getNonce(128)
+ val req = EbicsRequest.createForDownloadInitializationPhase(
+ subscriberDetails.userId,
+ subscriberDetails.partnerId,
+ subscriberDetails.hostId,
+ nonce,
+ DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
+ subscriberDetails.bankEncPub ?: throw EbicsProtocolError(
+ HttpStatusCode.BadRequest,
+ "Invalid subscriber state 'bankEncPub' missing, please send HPB first"
+ ),
+ subscriberDetails.bankAuthPub ?: throw EbicsProtocolError(
+ HttpStatusCode.BadRequest,
+ "Invalid subscriber state 'bankAuthPub' missing, please send HPB first"
+ ),
+ orderType,
+ makeOrderParams(orderParams)
+ )
+ logger.debug("Created EBICS document for download initialization, nonce: ${nonce.toHexString()}")
+ val doc = XMLUtil.convertJaxbToDocument(req)
+ XMLUtil.signEbicsDocument(
+ doc,
subscriberDetails.customerAuthPriv,
- withEbics3 = ebics3OrderService != null
+ withEbics3 = false
)
return XMLUtil.convertDomToString(doc)
}
fun createEbicsRequestForDownloadInitialization(
subscriberDetails: EbicsClientSubscriberDetails,
- orderType: String? = null,
+ ebics3OrderService: Ebics3Request.OrderDetails.Service,
orderParams: EbicsOrderParams,
- ebics3OrderService: Ebics3Request.OrderDetails.Service? = null
): String {
- // Check if the call is consistent: (only) ONE instruction is expected.
- if (orderType == null && ebics3OrderService == null)
- throw internalServerError("Need exactly one download instruction but zero was found.")
- if (orderType != null && ebics3OrderService != null)
- throw internalServerError("Need exactly one download instruction but two were found")
val nonce = getNonce(128)
-
- val doc = if (orderType != null) {
- val req = EbicsRequest.createForDownloadInitializationPhase(
- subscriberDetails.userId,
- subscriberDetails.partnerId,
- subscriberDetails.hostId,
- nonce,
- DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
- subscriberDetails.bankEncPub ?: throw EbicsProtocolError(
- HttpStatusCode.BadRequest,
- "Invalid subscriber state 'bankEncPub' missing, please send HPB first"
- ),
- subscriberDetails.bankAuthPub ?: throw EbicsProtocolError(
- HttpStatusCode.BadRequest,
- "Invalid subscriber state 'bankAuthPub' missing, please send HPB first"
- ),
- orderType,
- makeOrderParams(orderParams)
- )
- XMLUtil.convertJaxbToDocument(req)
- } else {
- val req = Ebics3Request.createForDownloadInitializationPhase(
- subscriberDetails.userId,
- subscriberDetails.partnerId,
- subscriberDetails.hostId,
- nonce,
- DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
- subscriberDetails.bankEncPub ?: throw EbicsProtocolError(
- HttpStatusCode.BadRequest,
- "Invalid subscriber state 'bankEncPub' missing, please send HPB first"
- ),
- subscriberDetails.bankAuthPub ?: throw EbicsProtocolError(
- HttpStatusCode.BadRequest,
- "Invalid subscriber state 'bankAuthPub' missing, please send HPB first"
- ),
- ebics3OrderService!!
- )
- XMLUtil.convertJaxbToDocument(req)
- }
-
- logger.debug("Created EBICS $orderType document for download initialization," +
- " nonce: ${nonce.toHexString()}")
+ val req = Ebics3Request.createForDownloadInitializationPhase(
+ subscriberDetails.userId,
+ subscriberDetails.partnerId,
+ subscriberDetails.hostId,
+ nonce,
+ DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
+ subscriberDetails.bankEncPub ?: throw EbicsProtocolError(
+ HttpStatusCode.BadRequest,
+ "Invalid subscriber state 'bankEncPub' missing, please send HPB first"
+ ),
+ subscriberDetails.bankAuthPub ?: throw EbicsProtocolError(
+ HttpStatusCode.BadRequest,
+ "Invalid subscriber state 'bankAuthPub' missing, please send HPB first"
+ ),
+ ebics3OrderService
+ )
+ val doc = XMLUtil.convertJaxbToDocument(req)
XMLUtil.signEbicsDocument(
doc,
subscriberDetails.customerAuthPriv,
- withEbics3 = ebics3OrderService != null
+ withEbics3 = true
)
return XMLUtil.convertDomToString(doc)
}
@@ -367,16 +432,31 @@ fun createEbicsRequestForDownloadTransferPhase(
subscriberDetails: EbicsClientSubscriberDetails,
transactionID: String?,
segmentNumber: Int,
- numSegments: Int
+ numSegments: Int,
+ withEbics3: Boolean = false
): String {
- val req = EbicsRequest.createForDownloadTransferPhase(
- subscriberDetails.hostId,
- transactionID,
- segmentNumber,
- numSegments
+ val doc = if (withEbics3) {
+ val req = Ebics3Request.createForDownloadTransferPhase(
+ subscriberDetails.hostId,
+ transactionID,
+ segmentNumber,
+ numSegments
+ )
+ XMLUtil.convertJaxbToDocument(req)
+ } else {
+ val req = EbicsRequest.createForDownloadTransferPhase(
+ subscriberDetails.hostId,
+ transactionID,
+ segmentNumber,
+ numSegments
+ )
+ XMLUtil.convertJaxbToDocument(req)
+ }
+ XMLUtil.signEbicsDocument(
+ doc,
+ subscriberDetails.customerAuthPriv,
+ withEbics3 = withEbics3
)
- val doc = XMLUtil.convertJaxbToDocument(req)
- XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv)
return XMLUtil.convertDomToString(doc)
}
@@ -384,17 +464,29 @@ fun createEbicsRequestForUploadTransferPhase(
subscriberDetails: EbicsClientSubscriberDetails,
transactionID: String?,
preparedUploadData: PreparedUploadData,
- chunkIndex: Int
+ chunkIndex: Int,
+ withEbics3: Boolean = false
): 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)
+ val doc = if (withEbics3) {
+ val req = Ebics3Request.createForUploadTransferPhase(
+ subscriberDetails.hostId,
+ transactionID,
+ // chunks are 1-indexed
+ BigInteger.valueOf(chunkIndex.toLong() + 1),
+ preparedUploadData.encryptedPayloadChunks[chunkIndex]
+ )
+ XMLUtil.convertJaxbToDocument(req)
+ } else {
+ val req = EbicsRequest.createForUploadTransferPhase(
+ subscriberDetails.hostId,
+ transactionID,
+ // chunks are 1-indexed
+ BigInteger.valueOf(chunkIndex.toLong() + 1),
+ preparedUploadData.encryptedPayloadChunks[chunkIndex]
+ )
+ XMLUtil.convertJaxbToDocument(req)
+ }
+ XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv, withEbics3)
return XMLUtil.convertDomToString(doc)
}
@@ -434,6 +526,7 @@ enum class EbicsReturnCode(val errorCode: String) {
EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"),
EBICS_AMOUNT_CHECK_FAILED("091303"),
EBICS_EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"),
+ EBICS_INVALID_XML("091010"),
EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005");
companion object {
@@ -525,40 +618,51 @@ fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData {
)
}
-fun parseAndValidateEbicsResponse(
- subscriberDetails: EbicsClientSubscriberDetails,
- responseStr: String
-): EbicsResponseContent {
- val responseDocument = try {
- XMLUtil.parseStringIntoDom(responseStr)
+private fun ebics3toInternalRepr(response: String): EbicsResponseContent {
+ val resp: JAXBElement<Ebics3Response> = try {
+ XMLUtil.convertStringToJaxb(response)
} catch (e: Exception) {
throw EbicsProtocolError(
HttpStatusCode.InternalServerError,
- "Invalid XML (as EbicsResponse) received from bank"
+ "Could not transform string-response from bank into JAXB"
)
}
- if (!XMLUtil.verifyEbicsDocument(
- responseDocument,
- subscriberDetails.bankAuthPub ?: throw EbicsProtocolError(
- HttpStatusCode.InternalServerError,
- "Bank's signature verification failed"
- )
- )
- ) {
- throw EbicsProtocolError(
- HttpStatusCode.InternalServerError,
- "Bank's signature verification failed"
- )
+ 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 reportText = resp.value.header.mutable.reportText
+
+ val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo
+ val dataEncryptionInfo = if (daeXml == null) {
+ null
+ } else {
+ DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value)
}
- val resp = try {
- XMLUtil.convertStringToJaxb<EbicsResponse>(responseStr)
+
+ return EbicsResponseContent(
+ transactionID = resp.value.header._static.transactionID,
+ bankReturnCode = bankReturnCode,
+ technicalReturnCode = techReturnCode,
+ reportText = reportText,
+ orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value,
+ dataEncryptionInfo = dataEncryptionInfo,
+ numSegments = resp.value.header._static.numSegments?.toInt(),
+ segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt()
+ )
+}
+
+private fun ebics25toInternalRepr(response: String): EbicsResponseContent {
+ val resp: JAXBElement<EbicsResponse> = try {
+ XMLUtil.convertStringToJaxb(response)
} catch (e: Exception) {
throw EbicsProtocolError(
HttpStatusCode.InternalServerError,
"Could not transform string-response from bank into JAXB"
)
}
-
val bankReturnCodeStr = resp.value.body.returnCode.value
val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr)
@@ -585,6 +689,37 @@ fun parseAndValidateEbicsResponse(
segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt()
)
}
+fun parseAndValidateEbicsResponse(
+ subscriberDetails: EbicsClientSubscriberDetails,
+ responseStr: String,
+ withEbics3: Boolean = false
+): EbicsResponseContent {
+ val responseDocument = try {
+ XMLUtil.parseStringIntoDom(responseStr)
+ } catch (e: Exception) {
+ throw EbicsProtocolError(
+ HttpStatusCode.InternalServerError,
+ "Invalid XML (as EbicsResponse) received from bank"
+ )
+ }
+ if (!XMLUtil.verifyEbicsDocument(
+ responseDocument,
+ subscriberDetails.bankAuthPub ?: throw EbicsProtocolError(
+ HttpStatusCode.InternalServerError,
+ "Bank's signature verification failed"
+ ),
+ withEbics3 = withEbics3
+ )
+ ) {
+ throw EbicsProtocolError(
+ HttpStatusCode.InternalServerError,
+ "Bank's signature verification failed"
+ )
+ }
+ if (withEbics3)
+ return ebics3toInternalRepr(responseStr)
+ return ebics25toInternalRepr(responseStr)
+}
/**
* Get the private key that matches the given public key digest.
diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt
index 6d923ae6..97c56180 100644
--- a/util/src/main/kotlin/XMLUtil.kt
+++ b/util/src/main/kotlin/XMLUtil.kt
@@ -66,11 +66,11 @@ import logger
class DefaultNamespaces : NamespacePrefixMapper() {
override fun getPreferredPrefix(namespaceUri: String?, suggestion: String?, requirePrefix: Boolean): String? {
if (namespaceUri == "http://www.w3.org/2000/09/xmldsig#") return "ds"
+ if (namespaceUri == XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI) return "xsi"
return null
}
}
-
class DOMInputImpl : LSInput {
var fPublicId: String? = null
var fSystemId: String? = null
@@ -292,23 +292,35 @@ class XMLUtil private constructor() {
return validate(xmlSource)
}
- inline fun <reified T> convertJaxbToString(obj: T): String {
+ inline fun <reified T> convertJaxbToString(
+ obj: T,
+ withSchemaLocation: String? = null
+ ): String {
val sw = StringWriter()
val jc = JAXBContext.newInstance(T::class.java)
val m = jc.createMarshaller()
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true)
+ if (withSchemaLocation != null) {
+ m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation)
+ }
m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces())
m.marshal(obj, sw)
return sw.toString()
}
- inline fun <reified T> convertJaxbToDocument(obj: T): Document {
+ inline fun <reified T> convertJaxbToDocument(
+ obj: T,
+ withSchemaLocation: String? = null
+ ): Document {
val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
dbf.isNamespaceAware = true
val doc = dbf.newDocumentBuilder().newDocument()
val jc = JAXBContext.newInstance(T::class.java)
val m = jc.createMarshaller()
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true)
+ if (withSchemaLocation != null) {
+ m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation)
+ }
m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces())
m.marshal(obj, doc)
return doc
@@ -340,8 +352,6 @@ class XMLUtil private constructor() {
val tf = TransformerFactory.newInstance()
val t = tf.newTransformer()
- //t.setOutputProperty(OutputKeys.INDENT, "yes")
-
/* Make string writer. */
val sw = StringWriter()
@@ -409,7 +419,7 @@ class XMLUtil private constructor() {
doc: Document,
signingPriv: PrivateKey,
withEbics3: Boolean = false
- ): Unit {
+ ) {
val xpath = XPathFactory.newInstance().newXPath()
xpath.namespaceContext = object : NamespaceContext {
override fun getNamespaceURI(p0: String?): String {
diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt
index b9bcf2ab..2af81601 100644
--- a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt
+++ b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt
@@ -163,8 +163,7 @@ class Ebics3Request {
lateinit var adminOrderType: String
@XmlAccessorType(XmlAccessType.NONE)
- @XmlType(propOrder = ["serviceName", "scope", "messageName"])
-
+ @XmlType(propOrder = ["serviceName", "scope", "serviceOption", "container", "messageName"])
class Service {
@get:XmlElement(name = "ServiceName", required = true)
@get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
@@ -174,9 +173,31 @@ class Ebics3Request {
@get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
lateinit var scope: String
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(name = "", propOrder = ["value"])
+ class MessageName {
+ @XmlValue
+ lateinit var value: String
+
+ @XmlAttribute(name = "version")
+ var version: String? = null
+ }
+
@get:XmlElement(name = "MsgName", required = true)
+ lateinit var messageName: MessageName
+
+ @get:XmlElement(name = "ServiceOption", required = true)
@get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
- lateinit var messageName: String
+ var serviceOption: String? = null
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ class Container {
+ @XmlAttribute(name = "containerType")
+ lateinit var containerType: String
+ }
+
+ @get:XmlElement(name = "Container", required = true)
+ var container: Container? = null
}
@XmlAccessorType(XmlAccessType.NONE)
@@ -250,9 +271,8 @@ class Ebics3Request {
var value: ByteArray? = null
}
-
@XmlAccessorType(XmlAccessType.NONE)
- @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "orderData", "hostId"])
+ @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "dataDigest", "orderData", "hostId"])
class DataTransfer {
@get:XmlElement(name = "DataEncryptionInfo")
@@ -261,6 +281,18 @@ class Ebics3Request {
@get:XmlElement(name = "SignatureData")
var signatureData: SignatureData? = null
+ @XmlAccessorType(XmlAccessType.NONE)
+ class DataDigest {
+ @get:XmlAttribute(name = "SignatureVersion", required = true)
+ var signatureVersion: String = "A006"
+
+ @get:XmlValue
+ var value: ByteArray? = null
+ }
+
+ @get:XmlElement(name = "DataDigest")
+ var dataDigest: DataDigest? = null
+
@get:XmlElement(name = "OrderData")
var orderData: String? = null
@@ -411,6 +443,7 @@ class Ebics3Request {
fun createForUploadInitializationPhase(
encryptedTransactionKey: ByteArray,
encryptedSignatureData: ByteArray,
+ aDataDigest: ByteArray,
hostId: String,
nonceArg: ByteArray,
partnerId: String,
@@ -436,7 +469,7 @@ class Ebics3Request {
userID = userId
orderDetails = OrderDetails().apply {
this.adminOrderType = "BTU"
- this.btdOrderParams = OrderDetails.BTOrderParams().apply {
+ this.btuOrderParams = OrderDetails.BTOrderParams().apply {
service = aOrderService
}
}
@@ -467,6 +500,9 @@ class Ebics3Request {
authenticate = true
value = encryptedSignatureData
}
+ dataDigest = DataTransfer.DataDigest().apply {
+ value = aDataDigest
+ }
dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply {
transactionKey = encryptedTransactionKey
authenticate = true
diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Response.kt b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt
new file mode 100644
index 00000000..56de5ffb
--- /dev/null
+++ b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt
@@ -0,0 +1,351 @@
+package tech.libeufin.util.ebics_h005
+
+import org.apache.xml.security.binding.xmldsig.SignatureType
+import org.apache.xml.security.binding.xmldsig.SignedInfoType
+import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.XMLUtil
+import tech.libeufin.util.ebics_h004.EbicsTypes
+import java.math.BigInteger
+import javax.xml.bind.annotation.*
+import javax.xml.bind.annotation.adapters.CollapsedStringAdapter
+import javax.xml.bind.annotation.adapters.NormalizedStringAdapter
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter
+import kotlin.math.min
+
+@XmlAccessorType(XmlAccessType.NONE)
+@XmlType(name = "", propOrder = ["header", "authSignature", "body"])
+@XmlRootElement(name = "ebicsResponse")
+class Ebics3Response {
+ @get:XmlAttribute(name = "Version", required = true)
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ lateinit var version: String
+
+ @get:XmlAttribute(name = "Revision")
+ var revision: Int? = null
+
+ @get:XmlElement(required = true)
+ lateinit var header: Header
+
+ @get:XmlElement(name = "AuthSignature", required = true)
+ lateinit var authSignature: SignatureType
+
+ @get:XmlElement(required = true)
+ lateinit var body: Body
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(name = "", propOrder = ["_static", "mutable"])
+ class Header {
+ @get:XmlElement(name = "static", required = true)
+ lateinit var _static: StaticHeaderType
+
+ @get:XmlElement(required = true)
+ lateinit var mutable: MutableHeaderType
+
+ @get:XmlAttribute(name = "authenticate", required = true)
+ var authenticate: Boolean = false
+ }
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"])
+ class Body {
+ @get:XmlElement(name = "DataTransfer")
+ var dataTransfer: DataTransferResponseType? = null
+
+ @get:XmlElement(name = "ReturnCode", required = true)
+ lateinit var returnCode: ReturnCode
+
+ @get:XmlElement(name = "TimestampBankParameter")
+ var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null
+ }
+
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(
+ name = "",
+ propOrder = ["transactionPhase", "segmentNumber", "orderID", "returnCode", "reportText"]
+ )
+ class MutableHeaderType {
+ @get:XmlElement(name = "TransactionPhase", required = true)
+ @get:XmlSchemaType(name = "token")
+ lateinit var transactionPhase: EbicsTypes.TransactionPhaseType
+
+ @get:XmlElement(name = "SegmentNumber")
+ var segmentNumber: EbicsTypes.SegmentNumber? = null
+
+ @get:XmlElement(name = "OrderID")
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ @get:XmlSchemaType(name = "token")
+ var orderID: String? = null
+
+ @get:XmlElement(name = "ReturnCode", required = true)
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ @get:XmlSchemaType(name = "token")
+ lateinit var returnCode: String
+
+ @get:XmlElement(name = "ReportText", required = true)
+ @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class)
+ @get:XmlSchemaType(name = "normalizedString")
+ lateinit var reportText: String
+ }
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ class OrderData {
+ @get:XmlValue
+ lateinit var value: String
+ }
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ class ReturnCode {
+ @get:XmlValue
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ lateinit var value: String
+
+ @get:XmlAttribute(name = "authenticate", required = true)
+ var authenticate: Boolean = false
+ }
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"])
+ class DataTransferResponseType {
+ @get:XmlElement(name = "DataEncryptionInfo")
+ var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null
+
+ @get:XmlElement(name = "OrderData", required = true)
+ lateinit var orderData: OrderData
+ }
+
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"])
+ class StaticHeaderType {
+ @get:XmlElement(name = "TransactionID")
+ var transactionID: String? = null
+
+ @get:XmlElement(name = "NumSegments")
+ @get:XmlSchemaType(name = "positiveInteger")
+ var numSegments: BigInteger? = null
+ }
+
+ companion object {
+
+ fun createForUploadWithError(
+ errorText: String, errorCode: String, phase: EbicsTypes.TransactionPhaseType
+ ): Ebics3Response {
+ val resp = Ebics3Response().apply {
+ this.version = "H005"
+ this.revision = 1
+ this.header = Ebics3Response.Header().apply {
+ this.authenticate = true
+ this.mutable = Ebics3Response.MutableHeaderType().apply {
+ this.reportText = errorText
+ this.returnCode = errorCode
+ this.transactionPhase = phase
+ }
+ _static = Ebics3Response.StaticHeaderType()
+ }
+ this.authSignature = SignatureType()
+ this.body = Ebics3Response.Body().apply {
+ this.returnCode = Ebics3Response.ReturnCode().apply {
+ this.authenticate = true
+ this.value = errorCode
+ }
+ }
+ }
+ return resp
+ }
+
+ fun createForUploadInitializationPhase(transactionID: String, orderID: String): Ebics3Response {
+ return Ebics3Response().apply {
+ this.version = "H005"
+ this.revision = 1
+ this.header = Header().apply {
+ this.authenticate = true
+ this._static = StaticHeaderType().apply {
+ this.transactionID = transactionID
+ }
+ this.mutable = MutableHeaderType().apply {
+ this.transactionPhase =
+ EbicsTypes.TransactionPhaseType.INITIALISATION
+ this.orderID = orderID
+ this.reportText = "[EBICS_OK] OK"
+ this.returnCode = "000000"
+ }
+ }
+ this.authSignature = SignatureType()
+ this.body = Body().apply {
+ this.returnCode = ReturnCode().apply {
+ this.authenticate = true
+ this.value = "000000"
+ }
+ }
+ }
+ }
+
+ fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): Ebics3Response {
+ return Ebics3Response().apply {
+ this.version = "H005"
+ this.revision = 1
+ this.header = Header().apply {
+ this.authenticate = true
+ this._static = StaticHeaderType().apply {
+ this.transactionID = transactionID
+ }
+ this.mutable = MutableHeaderType().apply {
+ this.transactionPhase =
+ EbicsTypes.TransactionPhaseType.RECEIPT
+ if (positiveAck) {
+ this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_DONE] Received positive receipt"
+ this.returnCode = "011000"
+ } else {
+ this.reportText = "[EBICS_DOWNLOAD_POSTPROCESS_SKIPPED] Received negative receipt"
+ this.returnCode = "011001"
+ }
+ }
+ }
+ this.authSignature = SignatureType()
+ this.body = Body().apply {
+ this.returnCode = ReturnCode().apply {
+ this.authenticate = true
+ this.value = "000000"
+ }
+ }
+ }
+ }
+
+ fun createForUploadTransferPhase(
+ transactionID: String,
+ segmentNumber: Int,
+ lastSegment: Boolean,
+ orderID: String
+ ): Ebics3Response {
+ return Ebics3Response().apply {
+ this.version = "H005"
+ this.revision = 1
+ this.header = Header().apply {
+ this.authenticate = true
+ this._static = StaticHeaderType().apply {
+ this.transactionID = transactionID
+ }
+ this.mutable = MutableHeaderType().apply {
+ this.transactionPhase =
+ EbicsTypes.TransactionPhaseType.TRANSFER
+ this.segmentNumber = EbicsTypes.SegmentNumber().apply {
+ this.value = BigInteger.valueOf(segmentNumber.toLong())
+ if (lastSegment) {
+ this.lastSegment = true
+ }
+ }
+ this.orderID = orderID
+ this.reportText = "[EBICS_OK] OK"
+ this.returnCode = "000000"
+ }
+ }
+ this.authSignature = SignatureType()
+ this.body = Body().apply {
+ this.returnCode = ReturnCode().apply {
+ this.authenticate = true
+ this.value = "000000"
+ }
+ }
+ }
+ }
+
+ /**
+ * @param requestedSegment requested segment as a 1-based index
+ */
+ fun createForDownloadTransferPhase(
+ transactionID: String,
+ numSegments: Int,
+ segmentSize: Int,
+ encodedData: String,
+ requestedSegment: Int
+ ): Ebics3Response {
+ return Ebics3Response().apply {
+ this.version = "H005"
+ this.revision = 1
+ this.header = Header().apply {
+ this.authenticate = true
+ this._static = StaticHeaderType().apply {
+ this.transactionID = transactionID
+ this.numSegments = BigInteger.valueOf(numSegments.toLong())
+ }
+ this.mutable = MutableHeaderType().apply {
+ this.transactionPhase =
+ EbicsTypes.TransactionPhaseType.TRANSFER
+ this.segmentNumber = EbicsTypes.SegmentNumber().apply {
+ this.lastSegment = numSegments == requestedSegment
+ this.value = BigInteger.valueOf(requestedSegment.toLong())
+ }
+ this.reportText = "[EBICS_OK] OK"
+ this.returnCode = "000000"
+ }
+ }
+ this.authSignature = SignatureType()
+ this.body = Body().apply {
+ this.returnCode = ReturnCode().apply {
+ this.authenticate = true
+ this.value = "000000"
+ }
+ this.dataTransfer = DataTransferResponseType().apply {
+ this.orderData = OrderData().apply {
+ val start = segmentSize * (requestedSegment - 1)
+ this.value = encodedData.substring(start, min(start + segmentSize, encodedData.length))
+ }
+ }
+ }
+ }
+ }
+
+ fun createForDownloadInitializationPhase(
+ transactionID: String,
+ numSegments: Int,
+ segmentSize: Int,
+ enc: CryptoUtil.EncryptionResult,
+ encodedData: String
+ ): Ebics3Response {
+ return Ebics3Response().apply {
+ this.version = "H005"
+ this.revision = 1
+ this.header = Header().apply {
+ this.authenticate = true
+ this._static = StaticHeaderType().apply {
+ this.transactionID = transactionID
+ this.numSegments = BigInteger.valueOf(numSegments.toLong())
+ }
+ this.mutable = MutableHeaderType().apply {
+ this.transactionPhase =
+ EbicsTypes.TransactionPhaseType.INITIALISATION
+ this.segmentNumber = EbicsTypes.SegmentNumber().apply {
+ this.lastSegment = (numSegments == 1)
+ this.value = BigInteger.valueOf(1)
+ }
+ this.reportText = "[EBICS_OK] OK"
+ this.returnCode = "000000"
+ }
+ }
+ this.authSignature = SignatureType()
+ this.body = Body().apply {
+ this.returnCode = ReturnCode().apply {
+ this.authenticate = true
+ this.value = "000000"
+ }
+ this.dataTransfer = DataTransferResponseType().apply {
+ this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply {
+ this.authenticate = true
+ this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest()
+ .apply {
+ this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256"
+ this.version = "E002"
+ this.value = enc.pubKeyDigest
+ }
+ this.transactionKey = enc.encryptedTransactionKey
+ }
+ this.orderData = OrderData().apply {
+ this.value = encodedData.substring(0, min(segmentSize, encodedData.length))
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Types.kt b/util/src/main/kotlin/ebics_h005/Ebics3Types.kt
index 74c7af0f..2bb0659a 100644
--- a/util/src/main/kotlin/ebics_h005/Ebics3Types.kt
+++ b/util/src/main/kotlin/ebics_h005/Ebics3Types.kt
@@ -63,7 +63,6 @@ object Ebics3Types {
var lastSegment: Boolean? = null
}
-
@XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"])
@XmlAccessorType(XmlAccessType.NONE)
class DataEncryptionInfo {
diff --git a/util/src/main/kotlin/ebics_h005/package-info.java b/util/src/main/kotlin/ebics_h005/package-info.java
index 3300f657..6f65a4b2 100644
--- a/util/src/main/kotlin/ebics_h005/package-info.java
+++ b/util/src/main/kotlin/ebics_h005/package-info.java
@@ -8,5 +8,6 @@
elementFormDefault = XmlNsForm.QUALIFIED
)
package tech.libeufin.util.ebics_h005;
+import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file
diff --git a/util/src/main/kotlin/ebics_s002/SignatureTypes.kt b/util/src/main/kotlin/ebics_s002/SignatureTypes.kt
new file mode 100644
index 00000000..9e367fc9
--- /dev/null
+++ b/util/src/main/kotlin/ebics_s002/SignatureTypes.kt
@@ -0,0 +1,91 @@
+/*
+ * 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/>
+ */
+
+package tech.libeufin.util.ebics_s002
+
+import org.apache.xml.security.binding.xmldsig.RSAKeyValueType
+import org.apache.xml.security.binding.xmldsig.X509DataType
+import javax.xml.bind.annotation.*
+import javax.xml.bind.annotation.adapters.CollapsedStringAdapter
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter
+import javax.xml.datatype.XMLGregorianCalendar
+
+
+object SignatureTypes {
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(
+ name = "PubKeyValueType", namespace = "http://www.ebics.org/S002", propOrder = [
+ "rsaKeyValue",
+ "timeStamp"
+ ]
+ )
+ class PubKeyValueType {
+ @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true)
+ lateinit var rsaKeyValue: RSAKeyValueType
+
+ @get:XmlElement(name = "TimeStamp")
+ @get:XmlSchemaType(name = "dateTime")
+ var timeStamp: XMLGregorianCalendar? = null
+ }
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(
+ name = "",
+ propOrder = [
+ "x509Data",
+ "pubKeyValue",
+ "signatureVersion"
+ ]
+ )
+ class SignaturePubKeyInfoType {
+ @get:XmlElement(name = "X509Data")
+ var x509Data: X509DataType? = null
+
+ @get:XmlElement(name = "PubKeyValue", required = true)
+ lateinit var pubKeyValue: PubKeyValueType
+
+ @get:XmlElement(name = "SignatureVersion", required = true)
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ lateinit var signatureVersion: String
+ }
+
+ /**
+ * EBICS INI payload.
+ */
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(
+ name = "",
+ propOrder = ["signaturePubKeyInfo", "partnerID", "userID"]
+ )
+ @XmlRootElement(name = "SignaturePubKeyOrderData")
+ class SignaturePubKeyOrderData {
+ @get:XmlElement(name = "SignaturePubKeyInfo", required = true)
+ lateinit var signaturePubKeyInfo: SignaturePubKeyInfoType
+
+ @get:XmlElement(name = "PartnerID", required = true)
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ @get:XmlSchemaType(name = "token")
+ lateinit var partnerID: String
+
+ @get:XmlElement(name = "UserID", required = true)
+ @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class)
+ @get:XmlSchemaType(name = "token")
+ lateinit var userID: String
+ }
+} \ No newline at end of file
diff --git a/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt b/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt
new file mode 100644
index 00000000..6d7012a1
--- /dev/null
+++ b/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt
@@ -0,0 +1,27 @@
+package tech.libeufin.util.ebics_s002
+
+import javax.xml.bind.annotation.*
+
+@XmlAccessorType(XmlAccessType.NONE)
+@XmlRootElement(name = "UserSignatureData")
+@XmlType(name = "", propOrder = ["orderSignatureList"])
+class UserSignatureDataEbics3 {
+ @XmlElement(name = "OrderSignatureData", type = OrderSignatureData::class)
+ var orderSignatureList: List<OrderSignatureData>? = null
+
+ @XmlAccessorType(XmlAccessType.NONE)
+ @XmlType(name = "", propOrder = ["signatureVersion", "signatureValue", "partnerID", "userID"])
+ class OrderSignatureData {
+ @XmlElement(name = "SignatureVersion")
+ lateinit var signatureVersion: String
+
+ @XmlElement(name = "SignatureValue")
+ lateinit var signatureValue: ByteArray
+
+ @XmlElement(name = "PartnerID")
+ lateinit var partnerID: String
+
+ @XmlElement(name = "UserID")
+ lateinit var userID: String
+ }
+} \ No newline at end of file
diff --git a/util/src/main/kotlin/ebics_s002/package-info.java b/util/src/main/kotlin/ebics_s002/package-info.java
new file mode 100644
index 00000000..a9f7729a
--- /dev/null
+++ b/util/src/main/kotlin/ebics_s002/package-info.java
@@ -0,0 +1,13 @@
+/**
+ * This package-info.java file defines the default namespace for the JAXB bindings
+ * defined in the package.
+ */
+
+@XmlSchema(
+ namespace = "http://www.ebics.org/S002",
+ elementFormDefault = XmlNsForm.QUALIFIED
+)
+package tech.libeufin.util.ebics_s002;
+
+import javax.xml.bind.annotation.XmlNsForm;
+import javax.xml.bind.annotation.XmlSchema; \ No newline at end of file