summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-10-12 14:45:22 +0200
committerMS <ms@taler.net>2023-10-12 14:45:22 +0200
commit2abfd2922115bf37e36f2be159354cd195cce2fa (patch)
tree2a8d36dc837cc3a5840818aec282e982662f8879
parent42f98b64cdee41a233dbd11c44040fbe54828834 (diff)
downloadlibeufin-2abfd2922115bf37e36f2be159354cd195cce2fa.tar.gz
libeufin-2abfd2922115bf37e36f2be159354cd195cce2fa.tar.bz2
libeufin-2abfd2922115bf37e36f2be159354cd195cce2fa.zip
Sending INI and HIA to PostFinance.
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Ebics.kt79
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt76
-rw-r--r--nexus/src/test/kotlin/Common.kt17
-rw-r--r--nexus/src/test/kotlin/Ebics.kt18
4 files changed, 183 insertions, 7 deletions
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics.kt
index 463ebb53..22048afa 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Ebics.kt
@@ -37,10 +37,11 @@ import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
-import tech.libeufin.util.XMLUtil
+import tech.libeufin.util.*
+import tech.libeufin.util.ebics_h004.EbicsKeyManagementResponse
import tech.libeufin.util.ebics_h004.EbicsNpkdRequest
import tech.libeufin.util.ebics_h004.EbicsUnsecuredRequest
-import tech.libeufin.util.getEbicsNonce
+import java.security.interfaces.RSAPrivateCrtKey
import java.util.*
import javax.xml.datatype.DatatypeFactory
@@ -103,6 +104,80 @@ fun generateHpbMessage(cfg: EbicsSetupConfig, clientKeys: ClientPrivateKeysFile)
}
/**
+ * Decrypts and decompresses the business payload that was
+ * transported within an EBICS message from the bank
+ *
+ * @param clientEncryptionKey client private encryption key, used to decrypt
+ * the transaction key. The transaction key is the
+ * one actually used to encrypt the payload.
+ * @param encryptionInfo details related to the encrypted payload.
+ * @param chunks the several chunks that constitute the whole encrypted payload.
+ * @return the plain payload. Errors throw, so the caller must handle those.
+ *
+ */
+private fun decryptAndDecompressPayload(
+ clientEncryptionKey: RSAPrivateCrtKey,
+ encryptionInfo: DataEncryptionInfo,
+ chunks: List<String>
+): ByteArray {
+ 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,
+ clientEncryptionKey
+ )
+ return EbicsOrderUtil.decodeOrderData(dataCompr)
+}
+
+/**
+ * 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: String
+): EbicsKeyManagementResponseContent? {
+ val jaxb = try {
+ XMLUtil.convertStringToJaxb<EbicsKeyManagementResponse>(xml)
+ } catch (e: Exception) {
+ logger.error("Could not parse the raw response from bank into JAXB.")
+ return null
+ }
+ var payload: ByteArray? = null
+ jaxb.value.body.dataTransfer?.dataEncryptionInfo.apply {
+ // non-null indicates that an encrypted payload should be found.
+ if (this != null) {
+ val encOrderData = jaxb.value.body.dataTransfer?.orderData?.value
+ if (encOrderData == null) {
+ logger.error("Despite a non-null DataEncryptionInfo, OrderData could not be found, can't decrypt any payload!")
+ return null
+ }
+ payload = decryptAndDecompressPayload(
+ clientEncryptionKey,
+ DataEncryptionInfo(this.transactionKey, this.encryptionPubKeyDigest.value),
+ listOf(encOrderData)
+ )
+ }
+ }
+ val bankReturnCode = EbicsReturnCode.lookup(jaxb.value.body.returnCode.value) // business error
+ val ebicsReturnCode = EbicsReturnCode.lookup(jaxb.value.header.mutable.returnCode) // ebics error
+ return EbicsKeyManagementResponseContent(ebicsReturnCode, bankReturnCode, payload)
+}
+
+/**
* POSTs the EBICS message to the bank.
*
* @param URL where the bank serves EBICS requests.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index a8185a9c..076535e1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -33,7 +33,9 @@ import com.github.ajalt.clikt.core.subcommands
import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.versionOption
+import io.ktor.client.*
import io.ktor.util.*
+import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Contextual
import kotlinx.serialization.KSerializer
import org.slf4j.Logger
@@ -52,6 +54,7 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import net.taler.wallet.crypto.Base32Crockford
import tech.libeufin.util.CryptoUtil
+import tech.libeufin.util.EbicsReturnCode
import java.security.interfaces.RSAPrivateCrtKey
import kotlin.reflect.typeOf
@@ -136,11 +139,14 @@ class EbicsSetupConfig(config: TalerConfig) {
* that Nexus should honor in the communication with the
* bank.
*/
- val bankDialect = ebicsSetupRequireString("bank_dialect")
+ val bankDialect: String = ebicsSetupRequireString("bank_dialect").run {
+ if (this != "postfinance") throw Exception("Only 'postfinance' dialect is supported.")
+ return@run this
+ }
}
/**
- * Coverts base 32 representation of RSA private keys and vice versa.
+ * Converts base 32 representation of RSA private keys and vice versa.
*/
object RSAPrivateCrtKeySerializer : KSerializer<RSAPrivateCrtKey> {
override val descriptor: SerialDescriptor =
@@ -167,8 +173,8 @@ data class ClientPrivateKeysFile(
@Contextual val signature_private_key: RSAPrivateCrtKey,
@Contextual val encryption_private_key: RSAPrivateCrtKey,
@Contextual val authentication_private_key: RSAPrivateCrtKey,
- val submitted_ini: Boolean,
- val submitted_hia: Boolean
+ var submitted_ini: Boolean,
+ var submitted_hia: Boolean
)
/**
@@ -269,6 +275,58 @@ fun preparePrivateKeys(location: String): ClientPrivateKeysFile? {
}
return loadPrivateKeysFromDisk(location) // loads what found at location.
}
+enum class KeysOrderType {
+ INI, HIA
+}
+/**
+ * Collects all the steps from generating the INI message, to
+ * POSTing it to the bank, and finally marking the message as
+ * submitted.
+ *
+ * @param cfg handle to the configuration.
+ * @param privs bundle of all the private keys of the client.
+ * @param client the http client that requests to the bank.
+ * @param orderType INI or HIA.
+ * @return true on success, false otherwise.
+ */
+suspend fun doKeysRequestAndUpdateState(
+ cfg: EbicsSetupConfig,
+ privs: ClientPrivateKeysFile,
+ client: HttpClient,
+ orderType: KeysOrderType
+): Boolean {
+ val req = when(orderType) {
+ KeysOrderType.INI -> generateIniMessage(cfg, privs)
+ KeysOrderType.HIA -> generateHiaMessage(cfg, privs)
+ }
+ val xml = client.postToBank(cfg.hostBaseUrl, req)
+ if (xml == null) {
+ logger.error("Could not POST the ${orderType.name} message to the bank")
+ return false
+ }
+ val ebics = parseKeysMgmtResponse(privs.encryption_private_key, xml)
+ if (ebics == null) {
+ logger.error("Could not get any EBICS from the bank ${orderType.name} response ($xml).")
+ return false
+ }
+ if (ebics.technicalReturnCode != EbicsReturnCode.EBICS_OK) {
+ logger.error("EBICS ${orderType.name} failed with code: ${ebics.technicalReturnCode}")
+ return false
+ }
+ if (ebics.bankReturnCode != EbicsReturnCode.EBICS_OK) {
+ logger.error("EBICS ${orderType.name} reached the bank, but could not be fulfilled, error code: ${ebics.bankReturnCode}")
+ return false
+ }
+ when(orderType) {
+ KeysOrderType.INI -> privs.submitted_ini = true
+ KeysOrderType.HIA -> privs.submitted_hia = true
+ }
+ if (!syncJsonToDisk(privs, cfg.clientPrivateKeysFilename)) {
+ logger.error("Could not update the ${orderType.name} state on disk")
+ return false
+ }
+ return true
+}
/**
* CLI class implementing the "ebics-setup" subcommand.
@@ -332,6 +390,16 @@ class EbicsSetup: CliktCommand() {
exitProcess(1)
}
+ // Privs exist. Upload their pubs if forced, or they weren't uploaded yet.
+ // Whenever one branch fails, the process fails too (doKeysRequest logs the reason).
+ runBlocking {
+ val httpClient = HttpClient()
+ if (!(privs.submitted_ini) || forceKeysResubmission)
+ doKeysRequestAndUpdateState(cfg, privs, httpClient, KeysOrderType.INI).apply { if (!this) exitProcess(1) }
+ if (!(privs.submitted_hia) || forceKeysResubmission)
+ doKeysRequestAndUpdateState(cfg, privs, httpClient, KeysOrderType.HIA).apply { if (!this) exitProcess(1) }
+ }
+ // Upload went through, eject the PDF document with the keys.
}
}
diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt
index f980b43c..b9284822 100644
--- a/nexus/src/test/kotlin/Common.kt
+++ b/nexus/src/test/kotlin/Common.kt
@@ -32,4 +32,19 @@ fun getMockedClient(
}
}
}
-} \ No newline at end of file
+}
+
+fun getPofiConfig(userId: String, partnerId: String) = """
+ [nexus-ebics]
+ CURRENCY = KUDOS
+ HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb
+ HOST_ID = PFEBICS
+ USER_ID = $userId
+ PARTNER_ID = $partnerId
+ SYSTEM_ID = not-used
+ ACCOUNT_NUMBER = not-used-yet
+ BANK_PUBLIC_KEYS_FILE = /tmp/enc-auth-keys.json
+ CLIENT_PRIVATE_KEYS_FILE = /tmp/my-private-keys.json
+ ACCOUNT_META_DATA_FILE = /tmp/ebics-meta.json
+ BANK_DIALECT = postfinance
+""".trimIndent() \ No newline at end of file
diff --git a/nexus/src/test/kotlin/Ebics.kt b/nexus/src/test/kotlin/Ebics.kt
index 19130a5f..d9b2f12b 100644
--- a/nexus/src/test/kotlin/Ebics.kt
+++ b/nexus/src/test/kotlin/Ebics.kt
@@ -1,3 +1,4 @@
+import io.ktor.client.*
import io.ktor.client.engine.mock.*
import io.ktor.http.*
import kotlinx.coroutines.runBlocking
@@ -6,9 +7,11 @@ import org.junit.Test
import tech.libeufin.nexus.*
import tech.libeufin.util.XMLUtil
import tech.libeufin.util.ebics_h004.EbicsUnsecuredRequest
+import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
+import kotlin.test.assertTrue
class Ebics {
@@ -66,4 +69,19 @@ class Ebics {
assertNotNull(clientOk.postToBank("http://ignored.example.com/", "ignored"))
}
}
+
+ // Sends INI and HIA to the PostFinance test platform.
+ @Ignore
+ fun keysAtPofi() {
+ val handle = TalerConfig(NEXUS_CONFIG_SOURCE)
+ val ebicsUserId = File("/tmp/pofi-ebics-user-id.txt").readText()
+ val ebicsPartnerId = File("/tmp/pofi-ebics-partner-id.txt").readText()
+ handle.loadFromString(getPofiConfig(ebicsUserId, ebicsPartnerId))
+ val cfg = EbicsSetupConfig(handle)
+ // cfg loaded, send INI.
+ runBlocking {
+ assertTrue(doKeysRequestAndUpdateState(cfg, clientKeys, HttpClient(), KeysOrderType.INI))
+ assertTrue(doKeysRequestAndUpdateState(cfg, clientKeys, HttpClient(), KeysOrderType.HIA))
+ }
+ }
} \ No newline at end of file