libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit ebeb47583ae9f5c612bf3412545a4cddda574826
parent deca4cbdc5219cc209d78ac8f8210f63e2631099
Author: Florian Dold <florian.dold@gmail.com>
Date:   Fri,  8 Nov 2019 12:42:20 +0100

refactor, partially implement order upload

Diffstat:
M.idea/codeStyles/Project.xml | 1-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt | 236+++++++++++++++++++++++++++++++++++++++----------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 50++++++++++++++++++++++++++++++++++++++++++++++++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Asandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 625+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 558+------------------------------------------------------------------------------
Msandbox/src/test/kotlin/CryptoUtilTest.kt | 2+-
Asandbox/src/test/kotlin/EbicsOrderUtilTest.kt | 17+++++++++++++++++
8 files changed, 865 insertions(+), 702 deletions(-)

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml @@ -5,7 +5,6 @@ </JetCodeStyleSettings> <codeStyleSettings language="kotlin"> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> - <option name="WRAP_ON_TYPING" value="1" /> </codeStyleSettings> </code_scheme> </component> \ No newline at end of file diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CryptoUtil.kt @@ -21,10 +21,10 @@ package tech.libeufin.sandbox import org.bouncycastle.jce.provider.BouncyCastleProvider import java.io.ByteArrayOutputStream -import java.lang.Exception import java.math.BigInteger import java.security.KeyFactory import java.security.KeyPairGenerator +import java.security.MessageDigest import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey import java.security.spec.PKCS8EncodedKeySpec @@ -32,20 +32,18 @@ import java.security.spec.RSAPublicKeySpec import java.security.spec.X509EncodedKeySpec import javax.crypto.Cipher import javax.crypto.KeyGenerator -import java.security.MessageDigest import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec - -/** - * RSA key pair. - */ -data class RsaCrtKeyPair(val private: RSAPrivateCrtKey, val public: RSAPublicKey) - /** * Helpers for dealing with cryptographic operations in EBICS / LibEuFin. */ -class CryptoUtil { +object CryptoUtil { + + /** + * RSA key pair. + */ + data class RsaCrtKeyPair(val private: RSAPrivateCrtKey, val public: RSAPublicKey) class EncryptionResult( val encryptedTransactionKey: ByteArray, @@ -53,116 +51,114 @@ class CryptoUtil { val encryptedData: ByteArray ) - companion object { - private val bouncyCastleProvider = BouncyCastleProvider() - - /** - * Load an RSA private key from its binary PKCS#8 encoding. - */ - fun loadRsaPrivateKey(encodedPrivateKey: ByteArray): RSAPrivateCrtKey { - val spec = PKCS8EncodedKeySpec(encodedPrivateKey) - val priv = KeyFactory.getInstance("RSA").generatePrivate(spec) - if (priv !is RSAPrivateCrtKey) - throw Exception("wrong encoding") - return priv - } - - /** - * Load an RSA public key from its binary X509 encoding. - */ - fun loadRsaPublicKey(encodedPublicKey: ByteArray): RSAPublicKey { - val spec = X509EncodedKeySpec(encodedPublicKey) - val pub = KeyFactory.getInstance("RSA").generatePublic(spec) - if (pub !is RSAPublicKey) - throw Exception("wrong encoding") - return pub - } - - /** - * Load an RSA public key from its binary X509 encoding. - */ - fun getRsaPublicFromPrivate(rsaPrivateCrtKey: RSAPrivateCrtKey): RSAPublicKey { - val spec = RSAPublicKeySpec(rsaPrivateCrtKey.modulus, rsaPrivateCrtKey.publicExponent) - val pub = KeyFactory.getInstance("RSA").generatePublic(spec) - if (pub !is RSAPublicKey) - throw Exception("wrong encoding") - return pub - } - - /** - * Generate a fresh RSA key pair. - * - * @param nbits size of the modulus in bits - */ - fun generateRsaKeyPair(nbits: Int): RsaCrtKeyPair { - val gen = KeyPairGenerator.getInstance("RSA") - gen.initialize(nbits) - val pair = gen.genKeyPair() - val priv = pair.private - val pub = pair.public - if (priv !is RSAPrivateCrtKey) - throw Exception("key generation failed") - if (pub !is RSAPublicKey) - throw Exception("key generation failed") - return RsaCrtKeyPair(priv, pub) - } - - /** - * Load an RSA public key from its components. - * - * @param exponent - * @param modulus - * @return key - */ - fun loadRsaPublicKeyFromComponents(modulus: ByteArray, exponent: ByteArray): RSAPublicKey { - val modulusBigInt = BigInteger(1, modulus) - val exponentBigInt = BigInteger(1, exponent) - - val keyFactory = KeyFactory.getInstance("RSA") - val tmp = RSAPublicKeySpec(modulusBigInt, exponentBigInt) - return keyFactory.generatePublic(tmp) as RSAPublicKey - } - - /** - * Hash an RSA public key according to the EBICS standard (EBICS 2.5: 4.4.1.2.3). - */ - fun getEbicsPublicKeyHash(publicKey: RSAPublicKey): ByteArray { - val keyBytes = ByteArrayOutputStream() - keyBytes.writeBytes(publicKey.publicExponent.toByteArray().toHexString().toByteArray()) - keyBytes.write(' '.toInt()) - keyBytes.writeBytes(publicKey.modulus.toByteArray().toHexString().toByteArray()) - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(keyBytes.toByteArray()) - } - - /** - * Encrypt data according to the EBICS E002 encryption process. - */ - fun encryptEbicsE002(data: ByteArray, encryptionPublicKey: RSAPublicKey): EncryptionResult { - val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) - keygen.init(128) - val transactionKey = keygen.generateKey() - val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) - val ivParameterSpec = IvParameterSpec(ByteArray(16)) - symmetricCipher.init(Cipher.ENCRYPT_MODE, transactionKey, ivParameterSpec) - val encryptedData = symmetricCipher.doFinal(data) - val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) - asymmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPublicKey) - val encryptedTransactionKey = asymmetricCipher.doFinal(transactionKey.encoded) - val pubKeyDigest = getEbicsPublicKeyHash(encryptionPublicKey) - return EncryptionResult(encryptedTransactionKey, pubKeyDigest, encryptedData) - } - - fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { - val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) - asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) - val transactionKeyBytes = asymmetricCipher.doFinal(enc.encryptedTransactionKey) - val secretKeySpec = SecretKeySpec(transactionKeyBytes, "AES") - val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) - val ivParameterSpec = IvParameterSpec(ByteArray(16)) - symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) - val data = symmetricCipher.doFinal(enc.encryptedData) - return data - } + private val bouncyCastleProvider = BouncyCastleProvider() + + /** + * Load an RSA private key from its binary PKCS#8 encoding. + */ + fun loadRsaPrivateKey(encodedPrivateKey: ByteArray): RSAPrivateCrtKey { + val spec = PKCS8EncodedKeySpec(encodedPrivateKey) + val priv = KeyFactory.getInstance("RSA").generatePrivate(spec) + if (priv !is RSAPrivateCrtKey) + throw Exception("wrong encoding") + return priv + } + + /** + * Load an RSA public key from its binary X509 encoding. + */ + fun loadRsaPublicKey(encodedPublicKey: ByteArray): RSAPublicKey { + val spec = X509EncodedKeySpec(encodedPublicKey) + val pub = KeyFactory.getInstance("RSA").generatePublic(spec) + if (pub !is RSAPublicKey) + throw Exception("wrong encoding") + return pub + } + + /** + * Load an RSA public key from its binary X509 encoding. + */ + fun getRsaPublicFromPrivate(rsaPrivateCrtKey: RSAPrivateCrtKey): RSAPublicKey { + val spec = RSAPublicKeySpec(rsaPrivateCrtKey.modulus, rsaPrivateCrtKey.publicExponent) + val pub = KeyFactory.getInstance("RSA").generatePublic(spec) + if (pub !is RSAPublicKey) + throw Exception("wrong encoding") + return pub + } + + /** + * Generate a fresh RSA key pair. + * + * @param nbits size of the modulus in bits + */ + fun generateRsaKeyPair(nbits: Int): RsaCrtKeyPair { + val gen = KeyPairGenerator.getInstance("RSA") + gen.initialize(nbits) + val pair = gen.genKeyPair() + val priv = pair.private + val pub = pair.public + if (priv !is RSAPrivateCrtKey) + throw Exception("key generation failed") + if (pub !is RSAPublicKey) + throw Exception("key generation failed") + return RsaCrtKeyPair(priv, pub) + } + + /** + * Load an RSA public key from its components. + * + * @param exponent + * @param modulus + * @return key + */ + fun loadRsaPublicKeyFromComponents(modulus: ByteArray, exponent: ByteArray): RSAPublicKey { + val modulusBigInt = BigInteger(1, modulus) + val exponentBigInt = BigInteger(1, exponent) + + val keyFactory = KeyFactory.getInstance("RSA") + val tmp = RSAPublicKeySpec(modulusBigInt, exponentBigInt) + return keyFactory.generatePublic(tmp) as RSAPublicKey + } + + /** + * Hash an RSA public key according to the EBICS standard (EBICS 2.5: 4.4.1.2.3). + */ + fun getEbicsPublicKeyHash(publicKey: RSAPublicKey): ByteArray { + val keyBytes = ByteArrayOutputStream() + keyBytes.writeBytes(publicKey.publicExponent.toByteArray().toHexString().toByteArray()) + keyBytes.write(' '.toInt()) + keyBytes.writeBytes(publicKey.modulus.toByteArray().toHexString().toByteArray()) + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(keyBytes.toByteArray()) + } + + /** + * Encrypt data according to the EBICS E002 encryption process. + */ + fun encryptEbicsE002(data: ByteArray, encryptionPublicKey: RSAPublicKey): EncryptionResult { + val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) + keygen.init(128) + val transactionKey = keygen.generateKey() + val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) + val ivParameterSpec = IvParameterSpec(ByteArray(16)) + symmetricCipher.init(Cipher.ENCRYPT_MODE, transactionKey, ivParameterSpec) + val encryptedData = symmetricCipher.doFinal(data) + val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) + asymmetricCipher.init(Cipher.ENCRYPT_MODE, encryptionPublicKey) + val encryptedTransactionKey = asymmetricCipher.doFinal(transactionKey.encoded) + val pubKeyDigest = getEbicsPublicKeyHash(encryptionPublicKey) + return EncryptionResult(encryptedTransactionKey, pubKeyDigest, encryptedData) + } + + fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { + val asymmetricCipher = Cipher.getInstance("RSA/None/PKCS1Padding", bouncyCastleProvider) + asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) + val transactionKeyBytes = asymmetricCipher.doFinal(enc.encryptedTransactionKey) + val secretKeySpec = SecretKeySpec(transactionKeyBytes, "AES") + val symmetricCipher = Cipher.getInstance("AES/CBC/X9.23Padding", bouncyCastleProvider) + val ivParameterSpec = IvParameterSpec(ByteArray(16)) + symmetricCipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec) + val data = symmetricCipher.doFinal(enc.encryptedData) + return data } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -20,7 +20,9 @@ package tech.libeufin.sandbox import org.jetbrains.exposed.dao.* -import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.SchemaUtils import org.jetbrains.exposed.sql.transactions.transaction import java.sql.Blob @@ -29,6 +31,7 @@ const val EBICS_HOST_ID_MAX_LENGTH = 10 const val EBICS_USER_ID_MAX_LENGTH = 10 const val EBICS_PARTNER_ID_MAX_LENGTH = 10 const val EBICS_SYSTEM_ID_MAX_LENGTH = 10 + /** * All the states to give a subscriber. */ @@ -90,7 +93,7 @@ fun Blob.toByteArray(): ByteArray { * This table information *not* related to EBICS, for all * its customers. */ -object BankCustomersTable: IntIdTable() { +object BankCustomersTable : IntIdTable() { // Customer ID is the default 'id' field provided by the constructor. val name = varchar("name", CUSTOMER_NAME_MAX_LENGTH).primaryKey() val ebicsSubscriber = reference("ebicsSubscriber", EbicsSubscribersTable) @@ -118,6 +121,7 @@ object EbicsSubscriberPublicKeysTable : IntIdTable() { */ class EbicsSubscriberPublicKeyEntity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<EbicsSubscriberPublicKeyEntity>(EbicsSubscriberPublicKeysTable) + var rsaPublicKey by EbicsSubscriberPublicKeysTable.rsaPublicKey var state by EbicsSubscriberPublicKeysTable.state } @@ -134,6 +138,7 @@ object EbicsHostsTable : IntIdTable() { class EbicsHostEntity(id: EntityID<Int>) : IntEntity(id) { companion object : IntEntityClass<EbicsHostEntity>(EbicsHostsTable) + var hostId by EbicsHostsTable.hostID var ebicsVersion by EbicsHostsTable.ebicsVersion var signaturePrivateKey by EbicsHostsTable.signaturePrivateKey @@ -203,6 +208,47 @@ class EbicsDownloadTransactionEntity(id: EntityID<String>) : Entity<String>(id) } +object EbicsUploadTransactionsTable : IdTable<String>() { + override val id = text("transactionID").entityId() + val orderType = text("orderType") + val orderID = text("orderID") + val host = reference("host", EbicsHostsTable) + val subscriber = reference("subscriber", EbicsSubscribersTable) + val numSegments = integer("numSegments") + val lastSeenSegment = integer("lastSeenSegment") + val transactionKeyEnc = blob("transactionKeyEnc") +} + + +class EbicsUploadTransactionEntity(id: EntityID<String>) : Entity<String>(id) { + companion object : EntityClass<String, EbicsUploadTransactionEntity>(EbicsUploadTransactionsTable) + + var orderType by EbicsUploadTransactionsTable.orderType + var orderID by EbicsUploadTransactionsTable.orderID + var host by EbicsHostEntity referencedOn EbicsUploadTransactionsTable.host + var subscriber by EbicsSubscriberEntity referencedOn EbicsUploadTransactionsTable.subscriber + var numSegments by EbicsUploadTransactionsTable.numSegments + var lastSeenSegment by EbicsUploadTransactionsTable.lastSeenSegment + var transactionKeyEnc by EbicsDownloadTransactionsTable.transactionKeyEnc +} + + +object EbicsUploadTransactionChunksTable : IdTable<String>() { + override val id = + text("transactionID").entityId().references(EbicsUploadTransactionsTable.id, ReferenceOption.CASCADE) + val chunkIndex = integer("chunkIndex") + val chunkContent = blob("chunkContent") +} + + +class EbicsUploadTransactionChunkEntity(id : EntityID<String>): Entity<String>(id) { + companion object : EntityClass<String, EbicsUploadTransactionChunkEntity>(EbicsUploadTransactionChunksTable) + + var chunkIndex by EbicsUploadTransactionChunksTable.chunkIndex + var chunkContent by EbicsUploadTransactionChunksTable.chunkContent +} + + fun dbCreateTables() { Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsOrderUtil.kt @@ -19,6 +19,7 @@ package tech.libeufin.sandbox +import java.lang.IllegalArgumentException import java.security.SecureRandom import java.util.zip.DeflaterInputStream import java.util.zip.InflaterInputStream @@ -26,36 +27,63 @@ import java.util.zip.InflaterInputStream /** * Helpers for dealing with order compression, encryption, decryption, chunking and re-assembly. */ -class EbicsOrderUtil private constructor() { - companion object { - inline fun <reified T> decodeOrderDataXml(encodedOrderData: ByteArray): T { - return InflaterInputStream(encodedOrderData.inputStream()).use { - val bytes = it.readAllBytes() - XMLUtil.convertStringToJaxb<T>(bytes.toString(Charsets.UTF_8)).value - } +object EbicsOrderUtil { + inline fun <reified T> decodeOrderDataXml(encodedOrderData: ByteArray): T { + return InflaterInputStream(encodedOrderData.inputStream()).use { + val bytes = it.readAllBytes() + XMLUtil.convertStringToJaxb<T>(bytes.toString(Charsets.UTF_8)).value } + } - inline fun <reified T>encodeOrderDataXml(obj: T): ByteArray { - val bytes = XMLUtil.convertJaxbToString(obj).toByteArray() - return DeflaterInputStream(bytes.inputStream()).use { - it.readAllBytes() - } + inline fun <reified T> encodeOrderDataXml(obj: T): ByteArray { + val bytes = XMLUtil.convertJaxbToString(obj).toByteArray() + return DeflaterInputStream(bytes.inputStream()).use { + it.readAllBytes() } + } - fun generateTransactionId(): String { - val rng = SecureRandom() - val res = ByteArray(16) - rng.nextBytes(res) - return res.toHexString() - } + fun generateTransactionId(): String { + val rng = SecureRandom() + val res = ByteArray(16) + rng.nextBytes(res) + return res.toHexString() + } + + /** + * Calculate the resulting size of base64-encoding data of the given length, + * including padding. + */ + fun calculateBase64EncodedLength(dataLength: Int): Int { + val blocks = (dataLength + 3 - 1) / 3 + return blocks * 4 + } + + fun checkOrderIDOverflow(n: Int): Boolean { + if (n <= 0) + throw IllegalArgumentException() + val base = 10 + 26 + return n >= base * base + } - /** - * Calculate the resulting size of base64-encoding data of the given length, - * including padding. - */ - fun calculateBase64EncodedLength(dataLength: Int): Int { - val blocks = (dataLength + 3 - 1) / 3 - return blocks * 4 + private fun getDigitChar(x: Int): Char { + if (x < 10) { + return '0' + x } + return 'A' + (x - 10) + } + + fun computeOrderIDFromNumber(n: Int): String { + if (n <= 0) + throw IllegalArgumentException() + if (checkOrderIDOverflow(n)) + throw IllegalArgumentException() + var ni = n + val base = 10 + 26 + val x1 = ni % base + ni = ni / base + val x2 = ni % base + val c1 = getDigitChar(x1) + val c2 = getDigitChar(x2) + return String(charArrayOf('O', 'R', c2, c1)) } } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -0,0 +1,625 @@ +/* + * 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.sandbox + +import io.ktor.application.ApplicationCall +import io.ktor.http.ContentType +import io.ktor.http.HttpStatusCode +import io.ktor.request.receiveText +import io.ktor.response.respond +import io.ktor.response.respondText +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.jetbrains.exposed.sql.transactions.transaction +import org.w3c.dom.Document +import tech.libeufin.schema.ebics_h004.* +import tech.libeufin.schema.ebics_hev.HEVResponse +import tech.libeufin.schema.ebics_hev.SystemReturnCodeType +import tech.libeufin.schema.ebics_s001.SignaturePubKeyOrderData +import java.math.BigInteger +import java.util.* +import java.util.zip.DeflaterInputStream +import javax.sql.rowset.serial.SerialBlob + + +open class EbicsRequestError(val errorText: String, val errorCode: String) : + Exception("EBICS request management error: $errorText ($errorCode)") + +class EbicsInvalidRequestError : EbicsRequestError("[EBICS_INVALID_REQUEST] Invalid request", "060102") + +open class EbicsKeyManagementError(val errorText: String, val errorCode: String) : + Exception("EBICS key management error: $errorText ($errorCode)") + +private class EbicsInvalidXmlError : EbicsKeyManagementError("[EBICS_INVALID_XML]", "091010") + +private class EbicsInvalidOrderType : EbicsRequestError( + "[EBICS_UNSUPPORTED_ORDER_TYPE] Order type not supported", + "091005" +) + + +private suspend fun ApplicationCall.respondEbicsKeyManagement( + errorText: String, + errorCode: String, + bankReturnCode: String, + dataTransfer: CryptoUtil.EncryptionResult? = null, + orderId: String? = null +) { + val responseXml = EbicsKeyManagementResponse().apply { + version = "H004" + header = EbicsKeyManagementResponse.Header().apply { + authenticate = true + mutable = EbicsKeyManagementResponse.MutableHeaderType().apply { + reportText = errorText + returnCode = errorCode + if (orderId != null) { + this.orderID = orderId + } + } + _static = EbicsKeyManagementResponse.EmptyStaticHeader() + } + body = EbicsKeyManagementResponse.Body().apply { + this.returnCode = EbicsKeyManagementResponse.ReturnCode().apply { + this.authenticate = true + this.value = bankReturnCode + } + if (dataTransfer != null) { + this.dataTransfer = EbicsKeyManagementResponse.DataTransfer().apply { + this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + this.authenticate = true + this.transactionKey = dataTransfer.encryptedTransactionKey + this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { + this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + this.version = "E002" + this.value = dataTransfer.pubKeyDigest + } + } + this.orderData = EbicsKeyManagementResponse.OrderData().apply { + this.value = dataTransfer.encryptedData + } + } + } + } + } + val text = XMLUtil.convertJaxbToString(responseXml) + logger.info("responding with:\n${text}") + respondText(text, ContentType.Application.Xml, HttpStatusCode.OK) +} + + +private suspend fun ApplicationCall.handleEbicsHia(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) { + val keyObject = EbicsOrderUtil.decodeOrderDataXml<HIARequestOrderData>(orderData) + val encPubXml = keyObject.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue + val authPubXml = keyObject.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue + val encPub = CryptoUtil.loadRsaPublicKeyFromComponents(encPubXml.modulus, encPubXml.exponent) + val authPub = CryptoUtil.loadRsaPublicKeyFromComponents(authPubXml.modulus, authPubXml.exponent) + + transaction { + val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) + if (ebicsSubscriber == null) { + logger.warn("ebics subscriber not found") + throw EbicsInvalidRequestError() + } + ebicsSubscriber.authenticationKey = EbicsSubscriberPublicKeyEntity.new { + this.rsaPublicKey = SerialBlob(authPub.encoded) + state = KeyState.NEW + } + ebicsSubscriber.encryptionKey = EbicsSubscriberPublicKeyEntity.new { + this.rsaPublicKey = SerialBlob(encPub.encoded) + state = KeyState.NEW + } + ebicsSubscriber.state = when (ebicsSubscriber.state) { + SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_HIA + SubscriberState.PARTIALLY_INITIALIZED_INI -> SubscriberState.INITIALIZED + else -> ebicsSubscriber.state + } + } + respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000") +} + + +private suspend fun ApplicationCall.handleEbicsIni(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) { + val keyObject = EbicsOrderUtil.decodeOrderDataXml<SignaturePubKeyOrderData>(orderData) + val sigPubXml = keyObject.signaturePubKeyInfo.pubKeyValue.rsaKeyValue + val sigPub = CryptoUtil.loadRsaPublicKeyFromComponents(sigPubXml.modulus, sigPubXml.exponent) + + transaction { + val ebicsSubscriber = + findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) + if (ebicsSubscriber == null) { + logger.warn("ebics subscriber ('${header.static.partnerID}' / '${header.static.userID}' / '${header.static.systemID}') not found") + throw EbicsInvalidRequestError() + } + ebicsSubscriber.signatureKey = EbicsSubscriberPublicKeyEntity.new { + this.rsaPublicKey = SerialBlob(sigPub.encoded) + state = KeyState.NEW + } + ebicsSubscriber.state = when (ebicsSubscriber.state) { + SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_INI + SubscriberState.PARTIALLY_INITIALIZED_HIA -> SubscriberState.INITIALIZED + else -> ebicsSubscriber.state + } + } + logger.info("Signature key inserted in database _and_ subscriber state changed accordingly") + respondEbicsKeyManagement("[EBICS_OK]", "000000", bankReturnCode = "000000", orderId = "OR01") +} + +private suspend fun ApplicationCall.handleEbicsHpb( + ebicsHostInfo: EbicsHostPublicInfo, + requestDocument: Document, + header: EbicsNpkdRequest.Header +) { + val subscriberKeys = transaction { + val ebicsSubscriber = + findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) + if (ebicsSubscriber == null) { + throw EbicsInvalidRequestError() + } + if (ebicsSubscriber.state != SubscriberState.INITIALIZED) { + throw EbicsInvalidRequestError() + } + val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey + val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey + val sigPubBlob = ebicsSubscriber.signatureKey!!.rsaPublicKey + SubscriberKeys( + CryptoUtil.loadRsaPublicKey(authPubBlob.toByteArray()), + CryptoUtil.loadRsaPublicKey(encPubBlob.toByteArray()), + CryptoUtil.loadRsaPublicKey(sigPubBlob.toByteArray()) + ) + } + val validationResult = + XMLUtil.verifyEbicsDocument(requestDocument, subscriberKeys.authenticationPublicKey) + logger.info("validationResult: $validationResult") + if (!validationResult) { + throw EbicsKeyManagementError("invalid signature", "90000"); + } + val hpbRespondeData = HPBResponseOrderData().apply { + this.authenticationPubKeyInfo = EbicsTypes.AuthenticationPubKeyInfoType().apply { + this.authenticationVersion = "X002" + this.pubKeyValue = EbicsTypes.PubKeyValueType().apply { + this.rsaKeyValue = RSAKeyValueType().apply { + this.exponent = ebicsHostInfo.authenticationPublicKey.publicExponent.toByteArray() + this.modulus = ebicsHostInfo.authenticationPublicKey.modulus.toByteArray() + } + } + } + this.encryptionPubKeyInfo = EbicsTypes.EncryptionPubKeyInfoType().apply { + this.encryptionVersion = "E002" + this.pubKeyValue = EbicsTypes.PubKeyValueType().apply { + this.rsaKeyValue = RSAKeyValueType().apply { + this.exponent = ebicsHostInfo.encryptionPublicKey.publicExponent.toByteArray() + this.modulus = ebicsHostInfo.encryptionPublicKey.modulus.toByteArray() + } + } + } + this.hostID = ebicsHostInfo.hostID + } + + val compressedOrderData = EbicsOrderUtil.encodeOrderDataXml(hpbRespondeData) + + val encryptionResult = CryptoUtil.encryptEbicsE002(compressedOrderData, subscriberKeys.encryptionPublicKey) + + respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000", encryptionResult, "OR01") +} + +/** + * Find the ebics host corresponding to the one specified in the header. + */ +private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostPublicInfo { + return transaction { + val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestHostID }.firstOrNull() + if (ebicsHost == null) { + logger.warn("client requested unknown HostID") + throw EbicsKeyManagementError("[EBICS_INVALID_HOST_ID]", "091011") + } + val encryptionPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.encryptionPrivateKey.toByteArray()) + val authenticationPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.authenticationPrivateKey.toByteArray()) + EbicsHostPublicInfo( + requestHostID, + CryptoUtil.getRsaPublicFromPrivate(encryptionPrivateKey), + CryptoUtil.getRsaPublicFromPrivate(authenticationPrivateKey) + ) + } +} + + +private suspend fun ApplicationCall.receiveEbicsXml(): Document { + val body: String = receiveText() + logger.debug("Data received: $body") + val requestDocument: Document? = XMLUtil.parseStringIntoDom(body) + if (requestDocument == null || (!XMLUtil.validateFromDom(requestDocument))) { + throw EbicsInvalidXmlError() + } + return requestDocument +} + + +fun handleEbicsHtd(): ByteArray { + val htd = HTDResponseOrderData().apply { + this.partnerInfo = HTDResponseOrderData.PartnerInfo().apply { + this.accountInfoList = listOf( + HTDResponseOrderData.AccountInfo().apply { + this.id = "acctid1" + this.accountHolder = "Mina Musterfrau" + this.accountNumberList = listOf( + HTDResponseOrderData.GeneralAccountNumber().apply { + this.international = true + this.value = "DE21500105174751659277" + } + ) + this.currency = "EUR" + this.description = "ACCT" + this.bankCodeList = listOf( + HTDResponseOrderData.GeneralBankCode().apply { + this.international = true + this.value = "INGDDEFFXXX" + } + ) + }, + HTDResponseOrderData.AccountInfo().apply { + this.id = "glsdemo" + this.accountHolder = "Mina Musterfrau" + this.accountNumberList = listOf( + HTDResponseOrderData.GeneralAccountNumber().apply { + this.international = true + this.value = "DE91430609670123123123" + } + ) + this.currency = "EUR" + this.description = "glsdemoacct" + this.bankCodeList = listOf( + HTDResponseOrderData.GeneralBankCode().apply { + this.international = true + this.value = "GENODEM1GLS" + } + ) + } + ) + this.addressInfo = HTDResponseOrderData.AddressInfo().apply { + this.name = "Foo" + } + this.bankInfo = HTDResponseOrderData.BankInfo().apply { + this.hostID = "host01" + } + this.orderInfoList = listOf( + HTDResponseOrderData.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "C53" + this.transferType = "Download" + }, + HTDResponseOrderData.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "C52" + this.transferType = "Download" + }, + HTDResponseOrderData.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "CCC" + this.transferType = "Upload" + } + ) + } + this.userInfo = HTDResponseOrderData.UserInfo().apply { + this.name = "Some User" + this.userID = HTDResponseOrderData.UserIDType().apply { + this.status = 5 + this.value = "USER1" + } + this.permissionList = listOf( + HTDResponseOrderData.UserPermission().apply { + this.orderTypes = "C54 C53 C52 CCC" + } + ) + } + } + + val str = XMLUtil.convertJaxbToString(htd) + return str.toByteArray() +} + + +fun createEbicsResponseForDownloadInitializationPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + enc: CryptoUtil.EncryptionResult, + encodedData: String +): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + this.segmentNumber = EbicsResponse.SegmentNumber().apply { + this.lastSegment = (numSegments == 1) + this.value = BigInteger.valueOf(1) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = EbicsResponse.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 = EbicsResponse.OrderData().apply { + this.value = encodedData.substring(0, Math.min(segmentSize, encodedData.length)) + } + } + } + } +} + + +fun createEbicsResponseForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = EbicsResponse.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 = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } +} + +fun createEbicsResponseForUploadInitializationPhase(transactionID: String, orderID: String): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this._static = EbicsResponse.StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } +} + + +suspend fun ApplicationCall.ebicsweb() { + val requestDocument = receiveEbicsXml() + + logger.info("Processing ${requestDocument.documentElement.localName}") + + when (requestDocument.documentElement.localName) { + "ebicsUnsecuredRequest" -> { + val requestObject = requestDocument.toObject<EbicsUnsecuredRequest>() + logger.info("Serving a ${requestObject.header.static.orderDetails.orderType} request") + + val orderData = requestObject.body.dataTransfer.orderData.value + val header = requestObject.header + + when (header.static.orderDetails.orderType) { + "INI" -> handleEbicsIni(header, orderData) + "HIA" -> handleEbicsHia(header, orderData) + else -> throw EbicsInvalidXmlError() + } + } + "ebicsHEVRequest" -> { + val hevResponse = HEVResponse().apply { + this.systemReturnCode = SystemReturnCodeType().apply { + this.reportText = "[EBICS_OK]" + this.returnCode = "000000" + } + this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50")) + } + + val strResp = XMLUtil.convertJaxbToString(hevResponse) + respondText(strResp, ContentType.Application.Xml, HttpStatusCode.OK) + } + "ebicsNoPubKeyDigestsRequest" -> { + val requestObject = requestDocument.toObject<EbicsNpkdRequest>() + val hostInfo = ensureEbicsHost(requestObject.header.static.hostID) + when (requestObject.header.static.orderDetails.orderType) { + "HPB" -> handleEbicsHpb(hostInfo, requestDocument, requestObject.header) + else -> throw EbicsInvalidXmlError() + } + } + "ebicsRequest" -> { + println("ebicsRequest ${XMLUtil.convertDomToString(requestDocument)}") + val requestObject = requestDocument.toObject<EbicsRequest>() + val staticHeader = requestObject.header.static + val requestedHostId = staticHeader.hostID + + val responseXmlStr = transaction { + // Step 1 of 3: Get information about the host and subscriber + + val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestedHostId }.firstOrNull() + val requestTransactionID = requestObject.header.static.transactionID + var downloadTransaction: EbicsDownloadTransactionEntity? = null + var uploadTransaction: EbicsUploadTransactionEntity? = null + val subscriber = if (requestTransactionID != null) { + downloadTransaction = EbicsDownloadTransactionEntity.findById(requestTransactionID) + if (downloadTransaction != null) { + downloadTransaction.subscriber + } else { + uploadTransaction = EbicsUploadTransactionEntity.findById(requestTransactionID) + uploadTransaction?.subscriber + } + } else { + val partnerID = staticHeader.partnerID ?: throw EbicsInvalidRequestError() + val userID = staticHeader.userID ?: throw EbicsInvalidRequestError() + findEbicsSubscriber(partnerID, userID, staticHeader.systemID) + } + + if (ebicsHost == null) throw EbicsInvalidRequestError() + if (subscriber == null) throw EbicsInvalidRequestError() + + val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( + ebicsHost.authenticationPrivateKey + .toByteArray() + ) + val clientAuthPub = + CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.toByteArray()) + val clientEncPub = + CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.toByteArray()) + + // Step 2 of 3: Validate the signature + val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, clientAuthPub) + if (!verifyResult) { + throw EbicsInvalidRequestError() + } + + val ebicsResponse: EbicsResponse = when (requestObject.header.mutable.transactionPhase) { + EbicsTypes.TransactionPhaseType.INITIALISATION -> { + val transactionID = EbicsOrderUtil.generateTransactionId() + val orderType = + requestObject.header.static.orderDetails?.orderType ?: throw EbicsInvalidRequestError() + if (staticHeader.numSegments == null) { + val response = when (orderType) { + "HTD" -> handleEbicsHtd() + else -> throw EbicsInvalidXmlError() + } + + val compressedResponse = DeflaterInputStream(response.inputStream()).use { + it.readAllBytes() + } + + val enc = CryptoUtil.encryptEbicsE002(compressedResponse, clientEncPub) + val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData) + + val segmentSize = 4096 + val totalSize = encodedResponse.length + val numSegments = ((totalSize + segmentSize - 1) / segmentSize) + + EbicsDownloadTransactionEntity.new(transactionID) { + this.subscriber = subscriber + this.host = ebicsHost + this.orderType = orderType + this.segmentSize = segmentSize + this.transactionKeyEnc = SerialBlob(enc.encryptedTransactionKey) + this.encodedResponse = encodedResponse + this.numSegments = numSegments + this.receiptReceived = false + } + createEbicsResponseForDownloadInitializationPhase( + transactionID, + numSegments, + segmentSize, + enc, + encodedResponse + ) + } else { + val oidn = subscriber.nextOrderID++ + if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError() + val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn) + val signatureData = requestObject.body.dataTransfer?.signatureData + if (signatureData != null) { + println("signature data: ${signatureData.toString(Charsets.UTF_8)}") + } + val numSegments = + requestObject.header.static.numSegments ?: throw EbicsInvalidRequestError() + val transactionKeyEnc = + requestObject.body.dataTransfer?.dataEncryptionInfo?.transactionKey + ?: throw EbicsInvalidRequestError() + EbicsUploadTransactionEntity.new(transactionID) { + this.host = ebicsHost + this.subscriber = subscriber + this.lastSeenSegment = 0 + this.orderType = orderType + this.orderID = orderID + this.numSegments = numSegments.toInt() + this.transactionKeyEnc = SerialBlob(transactionKeyEnc) + } + createEbicsResponseForUploadInitializationPhase(transactionID, orderID) + } + } + EbicsTypes.TransactionPhaseType.TRANSFER -> { + throw NotImplementedError() + } + EbicsTypes.TransactionPhaseType.RECEIPT -> { + requestTransactionID ?: throw EbicsInvalidRequestError() + if (downloadTransaction == null) + throw EbicsInvalidRequestError() + val receiptCode = + requestObject.body.transferReceipt?.receiptCode ?: throw EbicsInvalidRequestError() + createEbicsResponseForDownloadReceiptPhase(requestTransactionID, receiptCode == 0) + } + } + val docText = XMLUtil.convertJaxbToString(ebicsResponse) + val doc = XMLUtil.parseStringIntoDom(docText) + XMLUtil.signEbicsDocument(doc, hostAuthPriv) + val signedDoc = XMLUtil.convertDomToString(doc) + println("response: $signedDoc") + docText + } + respondText(responseXmlStr, ContentType.Application.Xml, HttpStatusCode.OK) + } + else -> { + /* Log to console and return "unknown type" */ + logger.info("Unknown message, just logging it!") + respond( + HttpStatusCode.NotImplemented, + SandboxError("Not Implemented") + ) + } + } +} diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -19,7 +19,6 @@ package tech.libeufin.sandbox -import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCallPipeline import io.ktor.application.call import io.ktor.application.install @@ -30,7 +29,6 @@ import io.ktor.gson.gson import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.request.receive -import io.ktor.request.receiveText import io.ktor.request.uri import io.ktor.response.respond import io.ktor.response.respondText @@ -39,86 +37,18 @@ import io.ktor.routing.post import io.ktor.routing.routing import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty -import org.apache.xml.security.binding.xmldsig.RSAKeyValueType -import org.apache.xml.security.binding.xmldsig.SignatureType import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.slf4j.Logger import org.slf4j.LoggerFactory import org.w3c.dom.Document -import tech.libeufin.schema.ebics_h004.* -import tech.libeufin.schema.ebics_hev.HEVResponse -import tech.libeufin.schema.ebics_hev.SystemReturnCodeType -import tech.libeufin.schema.ebics_s001.SignaturePubKeyOrderData -import java.math.BigInteger import java.security.interfaces.RSAPublicKey import java.text.DateFormat -import java.util.* -import java.util.zip.DeflaterInputStream import javax.sql.rowset.serial.SerialBlob import javax.xml.bind.JAXBContext -val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") - -open class EbicsRequestError(val errorText: String, val errorCode: String) : - Exception("EBICS request management error: $errorText ($errorCode)") - -class EbicsInvalidRequestError : EbicsRequestError("[EBICS_INVALID_REQUEST] Invalid request", "060102") - -open class EbicsKeyManagementError(val errorText: String, val errorCode: String) : - Exception("EBICS key management error: $errorText ($errorCode)") - -class EbicsInvalidXmlError : EbicsKeyManagementError("[EBICS_INVALID_XML]", "091010") -class EbicsInvalidOrderType : EbicsRequestError("[EBICS_UNSUPPORTED_ORDER_TYPE] Order type not supported", "091005") - -private suspend fun ApplicationCall.respondEbicsKeyManagement( - errorText: String, - errorCode: String, - bankReturnCode: String, - dataTransfer: CryptoUtil.EncryptionResult? = null, - orderId: String? = null -) { - val responseXml = EbicsKeyManagementResponse().apply { - version = "H004" - header = EbicsKeyManagementResponse.Header().apply { - authenticate = true - mutable = EbicsKeyManagementResponse.MutableHeaderType().apply { - reportText = errorText - returnCode = errorCode - if (orderId != null) { - this.orderID = orderId - } - } - _static = EbicsKeyManagementResponse.EmptyStaticHeader() - } - body = EbicsKeyManagementResponse.Body().apply { - this.returnCode = EbicsKeyManagementResponse.ReturnCode().apply { - this.authenticate = true - this.value = bankReturnCode - } - if (dataTransfer != null) { - this.dataTransfer = EbicsKeyManagementResponse.DataTransfer().apply { - this.dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { - this.authenticate = true - this.transactionKey = dataTransfer.encryptedTransactionKey - this.encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { - this.algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - this.version = "E002" - this.value = dataTransfer.pubKeyDigest - } - } - this.orderData = EbicsKeyManagementResponse.OrderData().apply { - this.value = dataTransfer.encryptedData - } - } - } - } - } - val text = XMLUtil.convertJaxbToString(responseXml) - logger.info("responding with:\n${text}") - respondText(text, ContentType.Application.Xml, HttpStatusCode.OK) -} +val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): EbicsSubscriberEntity? { @@ -139,9 +69,11 @@ fun findEbicsSubscriber(partnerID: String, userID: String, systemID: String?): E data class Subscriber( val partnerID: String, val userID: String, - val systemID: String? + val systemID: String?, + val keys: SubscriberKeys ) + data class SubscriberKeys( val authenticationPublicKey: RSAPublicKey, val encryptionPublicKey: RSAPublicKey, @@ -149,160 +81,13 @@ data class SubscriberKeys( ) -data class EbicsHostInfo( +data class EbicsHostPublicInfo( val hostID: String, val encryptionPublicKey: RSAPublicKey, val authenticationPublicKey: RSAPublicKey ) -private suspend fun ApplicationCall.handleEbicsHia(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) { - val keyObject = EbicsOrderUtil.decodeOrderDataXml<HIARequestOrderData>(orderData) - val encPubXml = keyObject.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue - val authPubXml = keyObject.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue - val encPub = CryptoUtil.loadRsaPublicKeyFromComponents(encPubXml.modulus, encPubXml.exponent) - val authPub = CryptoUtil.loadRsaPublicKeyFromComponents(authPubXml.modulus, authPubXml.exponent) - - transaction { - val ebicsSubscriber = findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) - if (ebicsSubscriber == null) { - logger.warn("ebics subscriber not found") - throw EbicsInvalidRequestError() - } - ebicsSubscriber.authenticationKey = EbicsSubscriberPublicKeyEntity.new { - this.rsaPublicKey = SerialBlob(authPub.encoded) - state = KeyState.NEW - } - ebicsSubscriber.encryptionKey = EbicsSubscriberPublicKeyEntity.new { - this.rsaPublicKey = SerialBlob(encPub.encoded) - state = KeyState.NEW - } - ebicsSubscriber.state = when (ebicsSubscriber.state) { - SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_HIA - SubscriberState.PARTIALLY_INITIALIZED_INI -> SubscriberState.INITIALIZED - else -> ebicsSubscriber.state - } - } - respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000") -} - - -private suspend fun ApplicationCall.handleEbicsIni(header: EbicsUnsecuredRequest.Header, orderData: ByteArray) { - val keyObject = EbicsOrderUtil.decodeOrderDataXml<SignaturePubKeyOrderData>(orderData) - val sigPubXml = keyObject.signaturePubKeyInfo.pubKeyValue.rsaKeyValue - val sigPub = CryptoUtil.loadRsaPublicKeyFromComponents(sigPubXml.modulus, sigPubXml.exponent) - - transaction { - val ebicsSubscriber = - findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) - if (ebicsSubscriber == null) { - logger.warn("ebics subscriber ('${header.static.partnerID}' / '${header.static.userID}' / '${header.static.systemID}') not found") - throw EbicsInvalidRequestError() - } - ebicsSubscriber.signatureKey = EbicsSubscriberPublicKeyEntity.new { - this.rsaPublicKey = SerialBlob(sigPub.encoded) - state = KeyState.NEW - } - ebicsSubscriber.state = when (ebicsSubscriber.state) { - SubscriberState.NEW -> SubscriberState.PARTIALLY_INITIALIZED_INI - SubscriberState.PARTIALLY_INITIALIZED_HIA -> SubscriberState.INITIALIZED - else -> ebicsSubscriber.state - } - } - logger.info("Signature key inserted in database _and_ subscriber state changed accordingly") - respondEbicsKeyManagement("[EBICS_OK]", "000000", bankReturnCode = "000000", orderId = "OR01") -} - -private suspend fun ApplicationCall.handleEbicsHpb( - ebicsHostInfo: EbicsHostInfo, - requestDocument: Document, - header: EbicsNpkdRequest.Header -) { - val subscriberKeys = transaction { - val ebicsSubscriber = - findEbicsSubscriber(header.static.partnerID, header.static.userID, header.static.systemID) - if (ebicsSubscriber == null) { - throw EbicsInvalidRequestError() - } - if (ebicsSubscriber.state != SubscriberState.INITIALIZED) { - throw EbicsInvalidRequestError() - } - val authPubBlob = ebicsSubscriber.authenticationKey!!.rsaPublicKey - val encPubBlob = ebicsSubscriber.encryptionKey!!.rsaPublicKey - val sigPubBlob = ebicsSubscriber.signatureKey!!.rsaPublicKey - SubscriberKeys( - CryptoUtil.loadRsaPublicKey(authPubBlob.toByteArray()), - CryptoUtil.loadRsaPublicKey(encPubBlob.toByteArray()), - CryptoUtil.loadRsaPublicKey(sigPubBlob.toByteArray()) - ) - } - val validationResult = - XMLUtil.verifyEbicsDocument(requestDocument, subscriberKeys.authenticationPublicKey) - logger.info("validationResult: $validationResult") - if (!validationResult) { - throw EbicsKeyManagementError("invalid signature", "90000"); - } - val hpbRespondeData = HPBResponseOrderData().apply { - this.authenticationPubKeyInfo = EbicsTypes.AuthenticationPubKeyInfoType().apply { - this.authenticationVersion = "X002" - this.pubKeyValue = EbicsTypes.PubKeyValueType().apply { - this.rsaKeyValue = RSAKeyValueType().apply { - this.exponent = ebicsHostInfo.authenticationPublicKey.publicExponent.toByteArray() - this.modulus = ebicsHostInfo.authenticationPublicKey.modulus.toByteArray() - } - } - } - this.encryptionPubKeyInfo = EbicsTypes.EncryptionPubKeyInfoType().apply { - this.encryptionVersion = "E002" - this.pubKeyValue = EbicsTypes.PubKeyValueType().apply { - this.rsaKeyValue = RSAKeyValueType().apply { - this.exponent = ebicsHostInfo.encryptionPublicKey.publicExponent.toByteArray() - this.modulus = ebicsHostInfo.encryptionPublicKey.modulus.toByteArray() - } - } - } - this.hostID = ebicsHostInfo.hostID - } - - val compressedOrderData = EbicsOrderUtil.encodeOrderDataXml(hpbRespondeData) - - val encryptionResult = CryptoUtil.encryptEbicsE002(compressedOrderData, subscriberKeys.encryptionPublicKey) - - respondEbicsKeyManagement("[EBICS_OK]", "000000", "000000", encryptionResult, "OR01") -} - -/** - * Find the ebics host corresponding to the one specified in the header. - */ -private fun ApplicationCall.ensureEbicsHost(requestHostID: String): EbicsHostInfo { - return transaction { - val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestHostID }.firstOrNull() - if (ebicsHost == null) { - logger.warn("client requested unknown HostID") - throw EbicsKeyManagementError("[EBICS_INVALID_HOST_ID]", "091011") - } - val encryptionPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.encryptionPrivateKey.toByteArray()) - val authenticationPrivateKey = CryptoUtil.loadRsaPrivateKey(ebicsHost.authenticationPrivateKey.toByteArray()) - EbicsHostInfo( - requestHostID, - CryptoUtil.getRsaPublicFromPrivate(encryptionPrivateKey), - CryptoUtil.getRsaPublicFromPrivate(authenticationPrivateKey) - ) - } -} - - -private suspend fun ApplicationCall.receiveEbicsXml(): Document { - val body: String = receiveText() - logger.debug("Data received: $body") - val requestDocument: Document? = XMLUtil.parseStringIntoDom(body) - if (requestDocument == null || (!XMLUtil.validateFromDom(requestDocument))) { - throw EbicsInvalidXmlError() - } - return requestDocument -} - - inline fun <reified T> Document.toObject(): T { val jc = JAXBContext.newInstance(T::class.java) val m = jc.createUnmarshaller() @@ -310,339 +95,6 @@ inline fun <reified T> Document.toObject(): T { } -fun handleEbicsHtd(): ByteArray { - val htd = HTDResponseOrderData().apply { - this.partnerInfo = HTDResponseOrderData.PartnerInfo().apply { - this.accountInfoList = listOf( - HTDResponseOrderData.AccountInfo().apply { - this.id = "acctid1" - this.accountHolder = "Mina Musterfrau" - this.accountNumberList = listOf( - HTDResponseOrderData.GeneralAccountNumber().apply { - this.international = true - this.value = "DE21500105174751659277" - } - ) - this.currency = "EUR" - this.description = "ACCT" - this.bankCodeList = listOf( - HTDResponseOrderData.GeneralBankCode().apply { - this.international = true - this.value = "INGDDEFFXXX" - } - ) - } - ) - this.addressInfo = HTDResponseOrderData.AddressInfo().apply { - this.name = "Foo" - } - this.bankInfo = HTDResponseOrderData.BankInfo().apply { - this.hostID = "host01" - } - this.orderInfoList = listOf( - HTDResponseOrderData.AuthOrderInfoType().apply { - this.description = "foo" - this.orderType = "C53" - this.transferType = "Download" - }, - HTDResponseOrderData.AuthOrderInfoType().apply { - this.description = "foo" - this.orderType = "C52" - this.transferType = "Download" - }, - HTDResponseOrderData.AuthOrderInfoType().apply { - this.description = "foo" - this.orderType = "CCC" - this.transferType = "Upload" - } - ) - } - this.userInfo = HTDResponseOrderData.UserInfo().apply { - this.name = "Some User" - this.userID = HTDResponseOrderData.UserIDType().apply { - this.status = 5 - this.value = "USER1" - } - this.permissionList = listOf( - HTDResponseOrderData.UserPermission().apply { - this.orderTypes = "C54 C53 C52 CCC" - } - ) - } - } - - val str = XMLUtil.convertJaxbToString(htd) - return str.toByteArray() -} - - -fun createEbicsResponseForDownloadInitializationPhase( - transactionID: String, - numSegments: Int, - segmentSize: Int, - enc: CryptoUtil.EncryptionResult, - encodedData: String -): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = EbicsResponse.Header().apply { - this.authenticate = true - this._static = EbicsResponse.StaticHeaderType().apply { - this.transactionID = transactionID - this.numSegments = BigInteger.valueOf(numSegments.toLong()) - } - this.mutable = EbicsResponse.MutableHeaderType().apply { - this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - this.segmentNumber = EbicsResponse.SegmentNumber().apply { - this.lastSegment = (numSegments == 1) - this.value = BigInteger.valueOf(1) - } - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = EbicsResponse.Body().apply { - this.returnCode = EbicsResponse.ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - this.dataTransfer = EbicsResponse.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 = EbicsResponse.OrderData().apply { - this.value = encodedData.substring(0, Math.min(segmentSize, encodedData.length)) - } - } - } - } -} - - -fun createEbicsResponseForDownloadTransferPhase() { - -} - - -fun createEbicsResponseForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = EbicsResponse.Header().apply { - this.authenticate = true - this._static = EbicsResponse.StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = EbicsResponse.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_DONE] Received negative receipt" - this.returnCode = "011001" - } - } - } - this.authSignature = SignatureType() - this.body = EbicsResponse.Body().apply { - this.returnCode = EbicsResponse.ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } -} - - -private suspend fun ApplicationCall.handleEbicsDownloadInitialization() { - -} - -private suspend fun ApplicationCall.handleEbicsDownloadTransfer() { - -} - -private suspend fun ApplicationCall.handleEbicsDownloadReceipt() { - -} - -private suspend fun ApplicationCall.handleEbicsUploadInitialization() { - -} - - -private suspend fun ApplicationCall.ebicsweb() { - val requestDocument = receiveEbicsXml() - - logger.info("Processing ${requestDocument.documentElement.localName}") - - when (requestDocument.documentElement.localName) { - "ebicsUnsecuredRequest" -> { - val requestObject = requestDocument.toObject<EbicsUnsecuredRequest>() - logger.info("Serving a ${requestObject.header.static.orderDetails.orderType} request") - - val orderData = requestObject.body.dataTransfer.orderData.value - val header = requestObject.header - - when (header.static.orderDetails.orderType) { - "INI" -> handleEbicsIni(header, orderData) - "HIA" -> handleEbicsHia(header, orderData) - else -> throw EbicsInvalidXmlError() - } - } - "ebicsHEVRequest" -> { - val hevResponse = HEVResponse().apply { - this.systemReturnCode = SystemReturnCodeType().apply { - this.reportText = "[EBICS_OK]" - this.returnCode = "000000" - } - this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50")) - } - - val strResp = XMLUtil.convertJaxbToString(hevResponse) - respondText(strResp, ContentType.Application.Xml, HttpStatusCode.OK) - } - "ebicsNoPubKeyDigestsRequest" -> { - val requestObject = requestDocument.toObject<EbicsNpkdRequest>() - val hostInfo = ensureEbicsHost(requestObject.header.static.hostID) - when (requestObject.header.static.orderDetails.orderType) { - "HPB" -> handleEbicsHpb(hostInfo, requestDocument, requestObject.header) - else -> throw EbicsInvalidXmlError() - } - } - "ebicsRequest" -> { - println("ebicsRequest ${XMLUtil.convertDomToString(requestDocument)}") - val requestObject = requestDocument.toObject<EbicsRequest>() - val staticHeader = requestObject.header.static - - when (requestObject.header.mutable.transactionPhase) { - EbicsTypes.TransactionPhaseType.INITIALISATION -> { - val partnerID = staticHeader.partnerID ?: throw EbicsInvalidXmlError() - val userID = staticHeader.userID ?: throw EbicsInvalidXmlError() - val respText = transaction { - val subscriber = - findEbicsSubscriber(partnerID, userID, staticHeader.systemID) - ?: throw EbicsInvalidXmlError() - val requestedHostId = requestObject.header.static.hostID - val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestedHostId }.firstOrNull() - if (ebicsHost == null) - throw EbicsInvalidRequestError() - val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( - ebicsHost.authenticationPrivateKey - .toByteArray() - ) - val clientAuthPub = - CryptoUtil.loadRsaPublicKey(subscriber.authenticationKey!!.rsaPublicKey.toByteArray()) - val clientEncPub = - CryptoUtil.loadRsaPublicKey(subscriber.encryptionKey!!.rsaPublicKey.toByteArray()) - val verifyResult = XMLUtil.verifyEbicsDocument(requestDocument, clientAuthPub) - println("ebicsRequest verification result: $verifyResult") - val transactionID = EbicsOrderUtil.generateTransactionId() - val orderType = requestObject.header.static.orderDetails?.orderType - - val response = when (orderType) { - "HTD" -> handleEbicsHtd() - else -> throw EbicsInvalidXmlError() - } - - val compressedResponse = DeflaterInputStream(response.inputStream()).use { - it.readAllBytes() - } - - val enc = CryptoUtil.encryptEbicsE002(compressedResponse, clientEncPub) - val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData) - - val segmentSize = 4096 - val totalSize = encodedResponse.length - val numSegments = ((totalSize + segmentSize - 1) / segmentSize) - - println("inner response: " + response.toString(Charsets.UTF_8)) - - println("total size: $totalSize") - println("num segments: $numSegments") - - EbicsDownloadTransactionEntity.new(transactionID) { - this.subscriber = subscriber - this.host = ebicsHost - this.orderType = orderType - this.segmentSize = segmentSize - this.transactionKeyEnc = SerialBlob(enc.encryptedTransactionKey) - this.encodedResponse = encodedResponse - this.numSegments = numSegments - this.receiptReceived = false - } - - val ebicsResponse = createEbicsResponseForDownloadInitializationPhase( - transactionID, - numSegments, segmentSize, enc, encodedResponse - ) - val docText = XMLUtil.convertJaxbToString(ebicsResponse) - val doc = XMLUtil.parseStringIntoDom(docText) - XMLUtil.signEbicsDocument(doc, hostAuthPriv) - val signedDoc = XMLUtil.convertDomToString(doc) - println("response: $signedDoc") - docText - } - respondText(respText, ContentType.Application.Xml, HttpStatusCode.OK) - return - } - EbicsTypes.TransactionPhaseType.TRANSFER -> { - - } - EbicsTypes.TransactionPhaseType.RECEIPT -> { - val respText = transaction { - val requestedHostId = requestObject.header.static.hostID - val ebicsHost = EbicsHostEntity.find { EbicsHostsTable.hostID eq requestedHostId }.firstOrNull() - if (ebicsHost == null) - throw EbicsInvalidRequestError() - val hostAuthPriv = CryptoUtil.loadRsaPrivateKey( - ebicsHost.authenticationPrivateKey - .toByteArray() - ) - val transactionID = requestObject.header.static.transactionID - if (transactionID == null) - throw EbicsInvalidRequestError() - val downloadTransaction = EbicsDownloadTransactionEntity.findById(transactionID) - if (downloadTransaction == null) - throw EbicsInvalidRequestError() - println("sending receipt for transaction ID $transactionID") - val receiptCode = requestObject.body.transferReceipt?.receiptCode - if (receiptCode == null) - throw EbicsInvalidRequestError() - val ebicsResponse = createEbicsResponseForDownloadReceiptPhase(transactionID, receiptCode == 0) - val docText = XMLUtil.convertJaxbToString(ebicsResponse) - val doc = XMLUtil.parseStringIntoDom(docText) - XMLUtil.signEbicsDocument(doc, hostAuthPriv) - val signedDoc = XMLUtil.convertDomToString(doc) - println("response: $signedDoc") - docText - } - respondText(respText, ContentType.Application.Xml, HttpStatusCode.OK) - return - - } - } - } - else -> { - /* Log to console and return "unknown type" */ - logger.info("Unknown message, just logging it!") - respond( - HttpStatusCode.NotImplemented, - SandboxError("Not Implemented") - ) - } - } -} - fun main() { dbCreateTables() diff --git a/sandbox/src/test/kotlin/CryptoUtilTest.kt b/sandbox/src/test/kotlin/CryptoUtilTest.kt @@ -53,7 +53,7 @@ class CryptoUtilTest { val encodedPriv = keyPair.private.encoded val encodedPub = keyPair.public.encoded val otherKeyPair = - RsaCrtKeyPair(CryptoUtil.loadRsaPrivateKey(encodedPriv), CryptoUtil.loadRsaPublicKey(encodedPub)) + CryptoUtil.RsaCrtKeyPair(CryptoUtil.loadRsaPrivateKey(encodedPriv), CryptoUtil.loadRsaPublicKey(encodedPub)) assertEquals(keyPair.private, otherKeyPair.private) assertEquals(keyPair.public, otherKeyPair.public) } diff --git a/sandbox/src/test/kotlin/EbicsOrderUtilTest.kt b/sandbox/src/test/kotlin/EbicsOrderUtilTest.kt @@ -0,0 +1,16 @@ +package tech.libeufin.sandbox + +import org.junit.Test +import kotlin.test.assertEquals + + +class EbicsOrderUtilTest { + + @Test + fun testComputeOrderIDFromNumber() { + assertEquals("OR01", EbicsOrderUtil.computeOrderIDFromNumber(1)) + assertEquals("OR0A", EbicsOrderUtil.computeOrderIDFromNumber(10)) + assertEquals("OR10", EbicsOrderUtil.computeOrderIDFromNumber(36)) + assertEquals("OR11", EbicsOrderUtil.computeOrderIDFromNumber(37)) + } +} +\ No newline at end of file