summaryrefslogtreecommitdiff
path: root/nexus/src/main/kotlin/tech/libeufin/nexus/ebics
diff options
context:
space:
mode:
Diffstat (limited to 'nexus/src/main/kotlin/tech/libeufin/nexus/ebics')
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt346
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt25
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt49
3 files changed, 172 insertions, 248 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
index 9f2d2afd..bee41525 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt
@@ -23,8 +23,6 @@
package tech.libeufin.nexus.ebics
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
import org.w3c.dom.Document
import tech.libeufin.common.crypto.CryptoUtil
import tech.libeufin.common.*
@@ -39,237 +37,179 @@ import java.util.*
import javax.xml.datatype.DatatypeFactory
import java.security.interfaces.*
-private val logger: Logger = LoggerFactory.getLogger("libeufin-nexus-ebics2")
-
-/**
- * Parses the raw XML that came from the bank into the Nexus representation.
- *
- * @param clientEncryptionKey client private encryption key, used to decrypt
- * the transaction key.
- * @param xml the bank raw XML response
- * @return the internal representation of the XML response, or null if the parsing or the decryption failed.
- * Note: it _is_ possible to successfully return the internal repr. of this response, where
- * the payload is null. That's however still useful, because the returned type provides bank
- * and EBICS return codes.
- */
-fun parseKeysMgmtResponse(
- clientEncryptionKey: RSAPrivateCrtKey,
- xml: Document
-): EbicsKeyManagementResponseContent {
- return XmlDestructor.fromDoc(xml, "ebicsKeyManagementResponse") {
- lateinit var technicalReturnCode: EbicsReturnCode
- lateinit var bankReturnCode: EbicsReturnCode
- lateinit var reportText: String
- var payload: ByteArray? = null
- one("header") {
- one("mutable") {
- technicalReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text())
- reportText = one("ReportText").text()
+/** Ebics 3 protocol for */
+class Ebics3KeyMng(
+ private val cfg: EbicsSetupConfig,
+ private val clientKeys: ClientPrivateKeysFile
+) {
+ fun INI(): ByteArray {
+ val inner = XMLOrderData(cfg, "ns2:SignaturePubKeyOrderData", "http://www.ebics.org/S001") {
+ el("ns2:SignaturePubKeyInfo") {
+ RSAKeyXml(clientKeys.signature_private_key)
+ el("ns2:SignatureVersion", "A006")
}
}
- one("body") {
- bankReturnCode = EbicsReturnCode.lookup(one("ReturnCode").text())
- payload = opt("DataTransfer") {
- val descriptionInfo = one("DataEncryptionInfo") {
- DataEncryptionInfo(
- one("TransactionKey").text().decodeBase64(),
- one("EncryptionPubKeyDigest").text().decodeBase64()
- )
+ val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", "H004")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ el("OrderType", "INI")
+ el("OrderAttribute", "DZNNN")
+ }
+ el("SecurityMedium", "0200")
}
- decryptAndDecompressPayload(
- clientEncryptionKey,
- descriptionInfo,
- listOf(one("OrderData").text())
- ).readBytes()
+ el("mutable")
}
+ el("body/DataTransfer/OrderData", inner)
}
- EbicsKeyManagementResponseContent(technicalReturnCode, bankReturnCode, payload)
+ return XMLUtil.convertDomToBytes(doc)
}
-}
-private fun XmlBuilder.RSAKeyXml(key: RSAPrivateCrtKey) {
- el("ns2:PubKeyValue") {
- el("ds:RSAKeyValue") {
- el("ds:Modulus", key.modulus.encodeBase64())
- el("ds:Exponent", key.publicExponent.encodeBase64())
- }
- }
-}
-
-private fun XMLOrderData(cfg: EbicsSetupConfig, name: String, schema: String, build: XmlBuilder.() -> Unit): String {
- return XmlBuilder.toBytes(name) {
- attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("xmlns:ns2", schema)
- build()
- el("ns2:PartnerID", cfg.ebicsPartnerId)
- el("ns2:UserID", cfg.ebicsUserId)
- }.inputStream().deflate().encodeBase64()
-}
-
-/**
- * Generates the INI message to upload the signature key.
- *
- * @param cfg handle to the configuration.
- * @param clientKeys set of all the client keys.
- * @return the raw EBICS INI message.
- */
-fun generateIniMessage(cfg: EbicsSetupConfig, clientKeys: ClientPrivateKeysFile): ByteArray {
- val inner = XMLOrderData(cfg, "ns2:SignaturePubKeyOrderData", "http://www.ebics.org/S001") {
- el("ns2:SignaturePubKeyInfo") {
- RSAKeyXml(clientKeys.signature_private_key)
- el("ns2:SignatureVersion", "A006")
+ fun HIA(): ByteArray {
+ val inner = XMLOrderData(cfg, "ns2:HIARequestOrderData", "urn:org:ebics:H004") {
+ el("ns2:AuthenticationPubKeyInfo") {
+ RSAKeyXml(clientKeys.authentication_private_key)
+ el("ns2:AuthenticationVersion", "X002")
+ }
+ el("ns2:EncryptionPubKeyInfo") {
+ RSAKeyXml(clientKeys.encryption_private_key)
+ el("ns2:EncryptionVersion", "E002")
+ }
}
- }
- val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H004")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails") {
- el("OrderType", "INI")
- el("OrderAttribute", "DZNNN")
+ val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", "H004")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ el("OrderType", "HIA")
+ el("OrderAttribute", "DZNNN")
+ }
+ el("SecurityMedium", "0200")
}
- el("SecurityMedium", "0200")
+ el("mutable")
}
- el("mutable")
+ el("body/DataTransfer/OrderData", inner)
}
- el("body/DataTransfer/OrderData", inner)
+ return XMLUtil.convertDomToBytes(doc)
}
- return XMLUtil.convertDomToBytes(doc)
-}
-/**
- * Generates the HIA message: uploads the authentication and
- * encryption keys.
- *
- * @param cfg handle to the configuration.
- * @param clientKeys set of all the client keys.
- * @return the raw EBICS HIA message.
- */
-fun generateHiaMessage(cfg: EbicsSetupConfig, clientKeys: ClientPrivateKeysFile): ByteArray {
- val inner = XMLOrderData(cfg, "ns2:HIARequestOrderData", "urn:org:ebics:H004") {
- el("ns2:AuthenticationPubKeyInfo") {
- RSAKeyXml(clientKeys.authentication_private_key)
- el("ns2:AuthenticationVersion", "X002")
- }
- el("ns2:EncryptionPubKeyInfo") {
- RSAKeyXml(clientKeys.encryption_private_key)
- el("ns2:EncryptionVersion", "E002")
- }
- }
- val doc = XmlBuilder.toDom("ebicsUnsecuredRequest", "urn:org:ebics:H004") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H004")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails") {
- el("OrderType", "HIA")
- el("OrderAttribute", "DZNNN")
+ fun HPB(): ByteArray {
+ val nonce = getNonce(128)
+ val doc = XmlBuilder.toDom("ebicsNoPubKeyDigestsRequest", "urn:org:ebics:H004") {
+ attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
+ attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("Version", "H004")
+ attr("Revision", "1")
+ el("header") {
+ attr("authenticate", "true")
+ el("static") {
+ el("HostID", cfg.ebicsHostId)
+ el("Nonce", nonce.encodeUpHex())
+ el("Timestamp", Instant.now().xmlDateTime())
+ el("PartnerID", cfg.ebicsPartnerId)
+ el("UserID", cfg.ebicsUserId)
+ el("OrderDetails") {
+ el("OrderType", "HPB")
+ el("OrderAttribute", "DZHNN")
+ }
+ el("SecurityMedium", "0000")
}
- el("SecurityMedium", "0200")
+ el("mutable")
}
- el("mutable")
+ el("AuthSignature")
+ el("body")
}
- el("body/DataTransfer/OrderData", inner)
+ XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key)
+ return XMLUtil.convertDomToBytes(doc)
}
- return XMLUtil.convertDomToBytes(doc)
-}
-/**
- * Generates the HPB message: downloads the bank keys.
- *
- * @param cfg handle to the configuration.
- * @param clientKeys set of all the client keys.
- * @return the raw EBICS HPB message.
- */
-fun generateHpbMessage(cfg: EbicsSetupConfig, clientKeys: ClientPrivateKeysFile): ByteArray {
- val nonce = getNonce(128)
- val doc = XmlBuilder.toDom("ebicsNoPubKeyDigestsRequest", "urn:org:ebics:H004") {
- attr("http://www.w3.org/2000/xmlns/", "xmlns", "urn:org:ebics:H004")
- attr("http://www.w3.org/2000/xmlns/", "xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
- attr("Version", "H004")
- attr("Revision", "1")
- el("header") {
- attr("authenticate", "true")
- el("static") {
- el("HostID", cfg.ebicsHostId)
- el("Nonce", nonce.encodeUpHex())
- el("Timestamp", Instant.now().xmlDateTime())
- el("PartnerID", cfg.ebicsPartnerId)
- el("UserID", cfg.ebicsUserId)
- el("OrderDetails") {
- el("OrderType", "HPB")
- el("OrderAttribute", "DZHNN")
- }
- el("SecurityMedium", "0000")
+ /* ----- Helpers ----- */
+
+ private fun XmlBuilder.RSAKeyXml(key: RSAPrivateCrtKey) {
+ el("ns2:PubKeyValue") {
+ el("ds:RSAKeyValue") {
+ el("ds:Modulus", key.modulus.encodeBase64())
+ el("ds:Exponent", key.publicExponent.encodeBase64())
}
- el("mutable")
}
- el("AuthSignature")
- el("body")
}
- XMLUtil.signEbicsDocument(doc, clientKeys.authentication_private_key)
- return XMLUtil.convertDomToBytes(doc)
-}
+
+ private fun XMLOrderData(cfg: EbicsSetupConfig, name: String, schema: String, build: XmlBuilder.() -> Unit): String {
+ return XmlBuilder.toBytes(name) {
+ attr("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#")
+ attr("xmlns:ns2", schema)
+ build()
+ el("ns2:PartnerID", cfg.ebicsPartnerId)
+ el("ns2:UserID", cfg.ebicsUserId)
+ }.inputStream().deflate().encodeBase64()
+ }
-class HpbResponseData(
- val hostID: String,
- val encryptionPubKey: RSAPublicKey,
- val encryptionVersion: String,
- val authenticationPubKey: RSAPublicKey,
- val authenticationVersion: String
-)
+ companion object {
+ fun parseResponse(doc: Document, clientEncryptionKey: RSAPrivateCrtKey): EbicsResponse<ByteArray?> {
+ return XmlDestructor.fromDoc(doc, "ebicsKeyManagementResponse") {
+ lateinit var technicalCode: EbicsReturnCode
+ lateinit var bankCode: EbicsReturnCode
+ var payload: ByteArray? = null
+ one("header") {
+ one("mutable") {
+ technicalCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ }
+ }
+ one("body") {
+ bankCode = EbicsReturnCode.lookup(one("ReturnCode").text())
+ payload = opt("DataTransfer") {
+ val descriptionInfo = one("DataEncryptionInfo") {
+ DataEncryptionInfo(
+ one("TransactionKey").text().decodeBase64(),
+ one("EncryptionPubKeyDigest").text().decodeBase64()
+ )
+ }
+ decryptAndDecompressPayload(
+ clientEncryptionKey,
+ descriptionInfo,
+ listOf(one("OrderData").text())
+ ).readBytes()
+ }
+ }
+ EbicsResponse(
+ technicalCode = technicalCode,
+ bankCode,
+ content = payload
+ )
+ }
+ }
-fun parseEbicsHpbOrder(orderDataRaw: InputStream): HpbResponseData {
- return XmlDestructor.fromStream(orderDataRaw, "HPBResponseOrderData") {
- val (authenticationPubKey, authenticationVersion) = one("AuthenticationPubKeyInfo") {
- Pair(
- one("PubKeyValue").one("RSAKeyValue") {
+ fun parseHpbOrder(data: ByteArray): Pair<RSAPublicKey, RSAPublicKey> {
+ return XmlDestructor.fromStream(data.inputStream(), "HPBResponseOrderData") {
+ val authPub = one("AuthenticationPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") {
CryptoUtil.loadRsaPublicKeyFromComponents(
one("Modulus").text().decodeBase64(),
one("Exponent").text().decodeBase64(),
)
- },
- one("AuthenticationVersion").text()
- )
- }
- val (encryptionPubKey, encryptionVersion) = one("EncryptionPubKeyInfo") {
- Pair(
- one("PubKeyValue").one("RSAKeyValue") {
+ }
+ val encPub = one("EncryptionPubKeyInfo").one("PubKeyValue").one("RSAKeyValue") {
CryptoUtil.loadRsaPublicKeyFromComponents(
one("Modulus").text().decodeBase64(),
one("Exponent").text().decodeBase64(),
)
- },
- one("EncryptionVersion").text()
- )
-
+ }
+ Pair(authPub, encPub)
+ }
}
- val hostID: String = one("HostID").text()
- HpbResponseData(
- hostID = hostID,
- encryptionPubKey = encryptionPubKey,
- encryptionVersion = encryptionVersion,
- authenticationPubKey = authenticationPubKey,
- authenticationVersion = authenticationVersion
- )
}
-}
-
-data class EbicsKeyManagementResponseContent(
- val technicalReturnCode: EbicsReturnCode,
- val bankReturnCode: EbicsReturnCode?,
- val orderData: ByteArray?
-) \ No newline at end of file
+} \ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
index d930104f..e4306ada 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt
@@ -331,7 +331,7 @@ class Ebics3BTS(
}
companion object {
- fun parseResponse(doc: Document): EbicsResponse {
+ fun parseResponse(doc: Document): EbicsResponse<BTSResponse> {
return XmlDestructor.fromDoc(doc, "ebicsResponse") {
var transactionID: String? = null
var numSegments: Int? = null
@@ -367,7 +367,7 @@ class Ebics3BTS(
EbicsResponse(
bankCode = bankCode,
technicalCode = technicalCode,
- content = EbicsResponseContent(
+ content = BTSResponse(
transactionID = transactionID,
orderID = orderID,
payloadChunk = payloadChunk,
@@ -381,26 +381,7 @@ class Ebics3BTS(
}
}
-
-data class EbicsResponse(
- val technicalCode: EbicsReturnCode,
- val bankCode: EbicsReturnCode,
- private val content: EbicsResponseContent
-) {
- /** Checks that return codes are both EBICS_OK or throw an exception */
- fun okOrFail(phase: String): EbicsResponseContent {
- logger.debug("$phase return codes: $technicalCode & $bankCode")
- require(technicalCode.kind() != EbicsReturnCode.Kind.Error) {
- "$phase has technical error: $technicalCode"
- }
- require(bankCode.kind() != EbicsReturnCode.Kind.Error) {
- "$phase has bank error: $bankCode"
- }
- return content
- }
-}
-
-data class EbicsResponseContent(
+data class BTSResponse(
val transactionID: String?,
val orderID: String?,
val dataEncryptionInfo: DataEncryptionInfo?,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
index 79fcb30b..63cac5cf 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt
@@ -130,23 +130,12 @@ suspend fun HttpClient.postToBank(bankUrl: String, msg: ByteArray): Document {
throw EbicsError.Transport("failed read bank response", e)
}
}
-
-/**
- * POSTs raw EBICS XML to the bank and checks the two return codes:
- * EBICS- and bank-technical.
- *
- * @param clientKeys client keys, used to sign the request.
- * @param bankKeys bank keys, used to decrypt and validate the response.
- * @param xmlReq raw EBICS request in XML.
- * @param isEbics3 true in case the communication is EBICS 3, false
- * @return [EbicsResponseContent] or throws [EbicsSideException]
- */
-suspend fun postEbics(
+suspend fun postBTS(
client: HttpClient,
cfg: EbicsSetupConfig,
bankKeys: BankPublicKeysFile,
xmlReq: ByteArray
-): EbicsResponse {
+): EbicsResponse<BTSResponse> {
val doc = client.postToBank(cfg.hostBaseUrl, xmlReq)
if (!XMLUtil.verifyEbicsDocument(
doc,
@@ -193,7 +182,7 @@ suspend fun ebicsDownload(
// TODO find a way to cancel the pending transaction ?
withContext(NonCancellable) {
// Init phase
- val initResp = postEbics(client, cfg, bankKeys, reqXml)
+ val initResp = postBTS(client, cfg, bankKeys, reqXml)
if (initResp.bankCode == EbicsReturnCode.EBICS_NO_DOWNLOAD_DATA_AVAILABLE) {
logger.debug("Download content is empty")
return@withContext
@@ -216,12 +205,8 @@ suspend fun ebicsDownload(
/** Send download receipt */
suspend fun receipt(success: Boolean) {
- postEbics(
- client,
- cfg,
- bankKeys,
- impl.downloadReceipt(tId, success)
- ).okOrFail("Download receipt phase")
+ val xml = impl.downloadReceipt(tId, success)
+ postBTS(client, cfg, bankKeys, xml).okOrFail("Download receipt phase")
}
/** Throw if parent scope have been canceled */
suspend fun checkCancellation() {
@@ -238,7 +223,7 @@ suspend fun ebicsDownload(
for (x in 2 .. howManySegments) {
checkCancellation()
val transReq = impl.downloadTransfer(x, howManySegments, tId)
- val transResp = postEbics(client, cfg, bankKeys, transReq).okOrFail("Download transfer phase")
+ val transResp = postBTS(client, cfg, bankKeys, transReq).okOrFail("Download transfer phase")
val chunk = requireNotNull(transResp.payloadChunk) {
"Download transfer phase: missing encrypted chunk"
}
@@ -349,14 +334,14 @@ suspend fun doEbicsUpload(
// Init phase
val initXml = impl.uploadInitialization(service, preparedPayload)
- val initResp = postEbics(client, cfg, bankKeys, initXml).okOrFail("Upload init phase")
+ val initResp = postBTS(client, cfg, bankKeys, initXml).okOrFail("Upload init phase")
val tId = requireNotNull(initResp.transactionID) {
"Upload init phase: missing transaction ID"
}
// Transfer phase
val transferXml = impl.uploadTransfer(tId, preparedPayload)
- val transferResp = postEbics(client, cfg, bankKeys, transferXml).okOrFail("Upload transfer phase")
+ val transferResp = postBTS(client, cfg, bankKeys, transferXml).okOrFail("Upload transfer phase")
val orderId = requireNotNull(transferResp.orderID) {
"Upload transfer phase: missing order ID"
}
@@ -424,6 +409,24 @@ suspend fun submitPain001(
)
}
+class EbicsResponse<T>(
+ val technicalCode: EbicsReturnCode,
+ val bankCode: EbicsReturnCode,
+ private val content: T
+) {
+ /** Checks that return codes are both EBICS_OK or throw an exception */
+ fun okOrFail(phase: String): T {
+ logger.debug("$phase return codes: $technicalCode & $bankCode")
+ require(technicalCode.kind() != EbicsReturnCode.Kind.Error) {
+ "$phase has technical error: $technicalCode"
+ }
+ require(bankCode.kind() != EbicsReturnCode.Kind.Error) {
+ "$phase has bank error: $bankCode"
+ }
+ return content
+ }
+}
+
// TODO import missing using a script
@Suppress("SpellCheckingInspection")
enum class EbicsReturnCode(val code: String) {