diff options
author | MS <ms@taler.net> | 2023-10-12 14:45:22 +0200 |
---|---|---|
committer | MS <ms@taler.net> | 2023-10-12 14:45:22 +0200 |
commit | 2abfd2922115bf37e36f2be159354cd195cce2fa (patch) | |
tree | 2a8d36dc837cc3a5840818aec282e982662f8879 | |
parent | 42f98b64cdee41a233dbd11c44040fbe54828834 (diff) | |
download | libeufin-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.kt | 79 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 76 | ||||
-rw-r--r-- | nexus/src/test/kotlin/Common.kt | 17 | ||||
-rw-r--r-- | nexus/src/test/kotlin/Ebics.kt | 18 |
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 |