libeufin

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

commit 8e6431fdf40d8142f9767c46d76731a82a072546
parent 735135e718a8b6ebf35cd479851af71d0471ef24
Author: Florian Dold <florian.dold@gmail.com>
Date:   Thu,  6 Feb 2020 14:32:40 +0100

refactoring / cleanup

Diffstat:
Mnexus/build.gradle | 2--
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt | 82++++++++++++++++++++++++-------------------------------------------------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt | 4++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 11++---------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 5+++++
Msandbox/src/main/python/libeufin-cli | 2+-
Mutil/src/main/kotlin/XMLUtil.kt | 32+++++++++++++++++++++++++++-----
Mutil/src/main/kotlin/XmlCombinators.kt | 8+++++++-
Mutil/src/main/kotlin/ebics_h004/EbicsRequest.kt | 44--------------------------------------------
9 files changed, 71 insertions(+), 119 deletions(-)

diff --git a/nexus/build.gradle b/nexus/build.gradle @@ -35,8 +35,6 @@ dependencies { implementation "javax.activation:activation:1.1" implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1" implementation 'org.apache.santuario:xmlsec:2.1.4' - kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.8.0' - compile 'com.ryanharter.ktor:ktor-moshi:1.0.1' testImplementation group: 'junit', name: 'junit', version: '4.12' } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt @@ -1,15 +1,11 @@ package tech.libeufin.nexus -import io.ktor.application.call import io.ktor.client.HttpClient -import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode -import io.ktor.response.respondText import tech.libeufin.util.CryptoUtil import tech.libeufin.util.EbicsOrderUtil import tech.libeufin.util.ebics_h004.EbicsRequest import tech.libeufin.util.ebics_h004.EbicsResponse -import tech.libeufin.util.ebics_h004.EbicsTypes import tech.libeufin.util.getGregorianCalendarNow import java.lang.StringBuilder import java.math.BigInteger @@ -17,7 +13,6 @@ import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey import java.util.* import java.util.zip.DeflaterInputStream -import javax.xml.datatype.XMLGregorianCalendar /** * This class is a mere container that keeps data found @@ -42,48 +37,6 @@ data class EbicsSubscriberDetails( ) -fun createDownloadInitializationPhase( - subscriberData: EbicsSubscriberDetails, - orderType: String, - nonce: ByteArray, - date: XMLGregorianCalendar -): EbicsRequest { - return EbicsRequest.createForDownloadInitializationPhase( - subscriberData.userId, - subscriberData.partnerId, - subscriberData.hostId, - nonce, - date, - subscriberData.bankEncPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - subscriberData.bankAuthPub ?: throw BankKeyMissing( - HttpStatusCode.PreconditionFailed - ), - orderType - ) -} - - -fun createUploadInitializationPhase( - subscriberData: EbicsSubscriberDetails, - orderType: String, - cryptoBundle: CryptoUtil.EncryptionResult -): EbicsRequest { - return EbicsRequest.createForUploadInitializationPhase( - cryptoBundle, - subscriberData.hostId, - getNonce(128), - subscriberData.partnerId, - subscriberData.userId, - getGregorianCalendarNow(), - subscriberData.bankAuthPub!!, - subscriberData.bankEncPub!!, - BigInteger.ONE, - orderType - ) -} - /** * Wrapper around the lower decryption routine, that takes a EBICS response @@ -134,11 +87,19 @@ suspend fun doEbicsDownloadTransaction( subscriberDetails: EbicsSubscriberDetails, orderType: String ): ByteArray { - val initDownloadRequest = createDownloadInitializationPhase( - subscriberDetails, - orderType, + val initDownloadRequest = EbicsRequest.createForDownloadInitializationPhase( + subscriberDetails.userId, + subscriberDetails.partnerId, + subscriberDetails.hostId, getNonce(128), - getGregorianCalendarNow() + getGregorianCalendarNow(), + subscriberDetails.bankEncPub ?: throw BankKeyMissing( + HttpStatusCode.PreconditionFailed + ), + subscriberDetails.bankAuthPub ?: throw BankKeyMissing( + HttpStatusCode.PreconditionFailed + ), + orderType ) val payloadChunks = LinkedList<String>(); val initResponse = client.postToBankSigned<EbicsRequest, EbicsResponse>( @@ -193,7 +154,7 @@ suspend fun doEbicsUploadTransaction( if (subscriberDetails.bankEncPub == null) { throw InvalidSubscriberStateError("bank encryption key unknown, request HPB first") } - val usd_encrypted = CryptoUtil.encryptEbicsE002( + val userSignatureDateEncrypted = CryptoUtil.encryptEbicsE002( EbicsOrderUtil.encodeOrderDataXml( signOrder( payload, @@ -206,10 +167,17 @@ suspend fun doEbicsUploadTransaction( ) val response = client.postToBankSignedAndVerify<EbicsRequest, EbicsResponse>( subscriberDetails.ebicsUrl, - createUploadInitializationPhase( - subscriberDetails, - orderType, - usd_encrypted + EbicsRequest.createForUploadInitializationPhase( + userSignatureDateEncrypted, + subscriberDetails.hostId, + getNonce(128), + subscriberDetails.partnerId, + subscriberDetails.userId, + getGregorianCalendarNow(), + subscriberDetails.bankAuthPub!!, + subscriberDetails.bankEncPub!!, + BigInteger.ONE, + orderType ), subscriberDetails.bankAuthPub!!, subscriberDetails.customerAuthPriv @@ -228,7 +196,7 @@ suspend fun doEbicsUploadTransaction( val encryptedPayload = CryptoUtil.encryptEbicsE002withTransactionKey( compressedInnerPayload, subscriberDetails.bankEncPub!!, - usd_encrypted.plainTransactionKey!! + userSignatureDateEncrypted.plainTransactionKey!! ) val tmp = EbicsRequest.createForUploadTransferPhase( subscriberDetails.hostId, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/JSON.kt @@ -8,6 +8,10 @@ data class NexusError( val message: String ) +data class EbicsStandardOrderParams( + val dateRange: EbicsDateRange? +) + data class EbicsDateRange( /** * ISO 8601 calendar dates: YEAR-MONTH(01-12)-DAY(1-31) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -19,8 +19,6 @@ package tech.libeufin.nexus -import com.ryanharter.ktor.moshi.moshi -import com.squareup.moshi.JsonDataException import io.ktor.application.ApplicationCallPipeline import io.ktor.application.call import io.ktor.application.install @@ -146,8 +144,6 @@ fun main() { this.logger = tech.libeufin.nexus.logger } install(ContentNegotiation) { - moshi { - } gson { setDateFormat(DateFormat.LONG) setPrettyPrinting() @@ -159,11 +155,6 @@ fun main() { call.respondText("Internal server error.\n", ContentType.Text.Plain, HttpStatusCode.InternalServerError) } - exception<JsonDataException> { cause -> - logger.error("Exception while handling '${call.request.uri}'", cause) - call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) - } - exception<NotAnIdError> { cause -> logger.error("Exception while handling '${call.request.uri}'", cause) call.respondText("Bad request\n", ContentType.Text.Plain, HttpStatusCode.BadRequest) @@ -252,6 +243,8 @@ fun main() { post("/ebics/subscribers/{id}/sendPTK") { val id = expectId(call.parameters["id"]) + val params = call.receive<EbicsStandardOrderParams>() + println("PTK order params: $params") val subscriberData = getSubscriberDetailsFromId(id) val response = doEbicsDownloadTransaction(client, subscriberData, "PTK") call.respondText( diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -280,6 +280,10 @@ private fun handleEbicsTSD(requestContext: RequestContext): ByteArray { return "Hello World".toByteArray() } +private fun handleEbicsPTK(requestContext: RequestContext): ByteArray { + return "Hello I am a dummy PTK response.".toByteArray() +} + private fun handleEbicsC52(requestContext: RequestContext): ByteArray { val subscriber = requestContext.subscriber @@ -650,6 +654,7 @@ private fun handleEbicsDownloadTransactionInitialization(requestContext: Request "C52" -> handleEbicsC52(requestContext) "C53" -> handleEbicsC52(requestContext) "TSD" -> handleEbicsTSD(requestContext) + "PTK" -> handleEbicsPTK(requestContext) else -> throw EbicsInvalidXmlError() } diff --git a/sandbox/src/main/python/libeufin-cli b/sandbox/src/main/python/libeufin-cli @@ -405,7 +405,7 @@ def c52(obj, account_id, nexus_base_url): def ptk(obj, account_id, nexus_base_url): url = urljoin(nexus_base_url, "/ebics/subscribers/{}/sendPTK".format(account_id)) try: - resp = post(url) + resp = post(url, json=dict()) except Exception: print("Could not reach the bank") return diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt @@ -84,9 +84,11 @@ class XMLUtil private constructor() { if (myRef.uri != "#xpointer($ebicsXpathExpr)") throw Exception("invalid EBICS XML signature URI: '${myRef.uri}'") val xp: XPath = XPathFactory.newInstance().newXPath() - val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").evaluate(myRef.here - .ownerDocument, XPathConstants - .NODESET) + val nodeSet = xp.compile("//*[@authenticate='true']/descendant-or-self::node()").evaluate( + myRef.here + .ownerDocument, XPathConstants + .NODESET + ) if (nodeSet !is NodeList) throw Exception("invalid type") if (nodeSet.length <= 0) { @@ -100,6 +102,7 @@ class XMLUtil private constructor() { return NodeSetData { nodeList.iterator() } } } + /** * Validator for EBICS messages. */ @@ -108,6 +111,7 @@ class XMLUtil private constructor() { e.printStackTrace() throw e } + companion object { private var cachedEbicsValidator: Validator? = null private fun getEbicsValidator(): Validator { @@ -148,7 +152,8 @@ class XMLUtil private constructor() { } } val schemaInputs: Array<Source> = listOf("xsd/ebics_H004.xsd", "xsd/ebics_hev.xsd").map { - val stream = classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") + val stream = + classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") StreamSource(stream) }.toTypedArray() val bundle = sf.newSchema(schemaInputs) @@ -156,6 +161,7 @@ class XMLUtil private constructor() { cachedEbicsValidator = newValidator return newValidator } + /** * * @param xmlDoc the XML document to validate @@ -170,6 +176,7 @@ class XMLUtil private constructor() { } return true; } + /** * Validates the DOM against the Schema(s) of this object. * @param domDocument DOM to validate @@ -184,6 +191,7 @@ class XMLUtil private constructor() { } return true } + /** * Craft object to be passed to the XML validator. * @param xmlString XML body, as read from the POST body. @@ -194,6 +202,7 @@ class XMLUtil private constructor() { val xmlSource = StreamSource(xmlInputStream) return validate(xmlSource) } + inline fun <reified T> convertJaxbToString(obj: T): String { val sw = StringWriter() val jc = JAXBContext.newInstance(T::class.java) @@ -203,8 +212,9 @@ class XMLUtil private constructor() { m.marshal(obj, sw) return sw.toString() } + inline fun <reified T> convertJaxbToDocument(obj: T): Document { - val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() + val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() dbf.isNamespaceAware = true val doc = dbf.newDocumentBuilder().newDocument() val jc = JAXBContext.newInstance(T::class.java) @@ -214,6 +224,7 @@ class XMLUtil private constructor() { m.marshal(obj, doc) return doc } + /** * Convert a XML string to the JAXB representation. * @@ -228,6 +239,7 @@ class XMLUtil private constructor() { T::class.java ) } + /** * Extract String from DOM. * @@ -248,6 +260,7 @@ class XMLUtil private constructor() { t.transform(DOMSource(document), StreamResult(sw)) return sw.toString() } + /** * Convert a node to a string without the XML declaration or * indentation. @@ -263,6 +276,7 @@ class XMLUtil private constructor() { t.transform(DOMSource(node), StreamResult(sw)) return sw.toString() } + /** * Convert a DOM document to the JAXB representation. * @@ -276,6 +290,7 @@ class XMLUtil private constructor() { val m = jc.createUnmarshaller() return m.unmarshal(document, finalType) // document "went" into Jaxb } + /** * Parse string into XML DOM. * @param xmlString the string to parse. @@ -289,6 +304,7 @@ class XMLUtil private constructor() { val builder = factory.newDocumentBuilder() return builder.parse(InputSource(xmlInputStream)) } + fun signEbicsResponse(ebicsResponse: EbicsResponse, privateKey: RSAPrivateCrtKey): String { val doc = convertJaxbToDocument(ebicsResponse) signEbicsDocument(doc, privateKey) @@ -296,6 +312,7 @@ class XMLUtil private constructor() { println("response: $signedDoc") return signedDoc } + /** * Sign an EBICS document with the authentication and identity signature. */ @@ -308,9 +325,11 @@ class XMLUtil private constructor() { else -> throw IllegalArgumentException() } } + override fun getPrefix(p0: String?): String { throw UnsupportedOperationException() } + override fun getPrefixes(p0: String?): MutableIterator<String> { throw UnsupportedOperationException() } @@ -345,6 +364,7 @@ class XMLUtil private constructor() { } authSigNode.removeChild(innerSig) } + fun verifyEbicsDocument(doc: Document, signingPub: PublicKey): Boolean { val xpath = XPathFactory.newInstance().newXPath() xpath.namespaceContext = object : NamespaceContext { @@ -354,9 +374,11 @@ class XMLUtil private constructor() { else -> throw IllegalArgumentException() } } + override fun getPrefix(p0: String?): String { throw UnsupportedOperationException() } + override fun getPrefixes(p0: String?): MutableIterator<String> { throw UnsupportedOperationException() } diff --git a/util/src/main/kotlin/XmlCombinators.kt b/util/src/main/kotlin/XmlCombinators.kt @@ -19,13 +19,16 @@ class XmlElementBuilder(val w: XMLStreamWriter) { this.element(path, f) w.writeEndElement() } + fun element(path: String, f: XmlElementBuilder.() -> Unit = {}) { val splitPath = path.trim('/').split("/").toMutableList() this.element(splitPath, f) } + fun attribute(name: String, value: String) { w.writeAttribute(name, value) } + fun text(content: String) { w.writeCharacters(content) } @@ -42,12 +45,15 @@ class XmlDocumentBuilder { set(w: XMLStreamWriter) { maybeWriter = w } + fun namespace(prefix: String, uri: String) { writer.setPrefix(prefix, uri) } + fun defaultNamespace(uri: String) { writer.setDefaultNamespace(uri) } + fun root(name: String, f: XmlElementBuilder.() -> Unit) { val elementBuilder = XmlElementBuilder(writer) writer.writeStartElement(name) @@ -75,7 +81,7 @@ fun constructXml(indent: Boolean = false, f: XmlDocumentBuilder.() -> Unit): Str class XmlDocumentDestructor { } -fun <T>destructXml(f: XmlDocumentDestructor.() -> T): T { +fun <T> destructXml(f: XmlDocumentDestructor.() -> T): T { val d = XmlDocumentDestructor() return f(d) } diff --git a/util/src/main/kotlin/ebics_h004/EbicsRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -2,8 +2,6 @@ package tech.libeufin.util.ebics_h004 import org.apache.xml.security.binding.xmldsig.SignatureType import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.LOGGER -import tech.libeufin.util.XMLUtil import java.math.BigInteger import java.security.interfaces.RSAPublicKey import java.util.* @@ -11,7 +9,6 @@ import javax.xml.bind.annotation.* import javax.xml.bind.annotation.adapters.CollapsedStringAdapter import javax.xml.bind.annotation.adapters.HexBinaryAdapter import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import javax.xml.datatype.DatatypeConstants import javax.xml.datatype.XMLGregorianCalendar @XmlAccessorType(XmlAccessType.NONE) @@ -320,44 +317,6 @@ class EbicsRequest { } - /* Take a time range (useful for C52 and C53) */ - fun createForDownloadInitializationPhase( - userId: String, - partnerId: String, - hostId: String, - nonceArg: ByteArray, - date: XMLGregorianCalendar, - bankEncPub: RSAPublicKey, - bankAuthPub: RSAPublicKey, - aOrderType: String, - dateStart: XMLGregorianCalendar, - dateEnd: XMLGregorianCalendar - ): EbicsRequest { - - /** - * Make sure there is NO time portion. - */ - dateStart.timezone = DatatypeConstants.FIELD_UNDEFINED - dateEnd.timezone = DatatypeConstants.FIELD_UNDEFINED - - val tmp = createForDownloadInitializationPhase( - userId, - partnerId, - hostId, - nonceArg, - date, - bankEncPub, - bankAuthPub, - aOrderType - ) - - (tmp.header.static.orderDetails?.orderParams as StandardOrderParams).dateRange = DateRange().apply { - start = dateStart - end = dateEnd - } - - return tmp - } fun createForDownloadInitializationPhase( userId: String, @@ -369,7 +328,6 @@ class EbicsRequest { bankAuthPub: RSAPublicKey, aOrderType: String ): EbicsRequest { - return EbicsRequest().apply { version = "H004" revision = 1 @@ -479,8 +437,6 @@ class EbicsRequest { } } } - - } fun createForUploadTransferPhase(