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:
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