commit bb7e455b0f71ba1870f4233f58bcb4bd4fbf05ed parent 8aeffb3f9d4fa5323d896a46902ed2384a953cbd Author: Antoine A <> Date: Tue, 23 Jan 2024 18:06:55 +0100 Split utils into common and ebics and ename integration to testbench Diffstat:
251 files changed, 14239 insertions(+), 14184 deletions(-)
diff --git a/.gitignore b/.gitignore @@ -1,13 +1,8 @@ .idea/* .vscode -/nexus/bin/ -/sandbox/bin/ -/util/bin/ -nexus/libeufin-nexus-dev nexus/test -integration/test -integration/config.json -sandbox/libeufin-sandbox-dev +testbench/test +testbench/config.json configure build/ .gradle @@ -26,7 +21,7 @@ __pycache__ *.log .DS_Store *.mk -util/src/main/resources/version.txt +common/src/main/resources/version.txt debian/usr/share/libeufin/demobank-ui/index.js debian/usr/share/libeufin/demobank-ui/*.html debian/usr/share/libeufin/demobank-ui/*.css diff --git a/Makefile b/Makefile @@ -110,13 +110,13 @@ bank-test: install-nobuild-bank-files nexus-test: install-nobuild-nexus-files ./gradlew :nexus:test --tests $(test) -i -.PHONY: integration-test -integration-test: install-nobuild-bank-files install-nobuild-nexus-files - ./gradlew :integration:test --tests $(test) -i +.PHONY: testbench-test +testbench-test: install-nobuild-bank-files install-nobuild-nexus-files + ./gradlew :testbench:test --tests $(test) -i -.PHONY: integration -integration: install-nobuild-bank-files install-nobuild-nexus-files - ./gradlew :integration:run --console=plain --args="$(test)" +.PHONY: testbench +testbench: install-nobuild-bank-files install-nobuild-nexus-files + ./gradlew :testbench:run --console=plain --args="$(platform)" .PHONY: doc doc: diff --git a/bank/build.gradle b/bank/build.gradle @@ -21,8 +21,7 @@ dependencies { // Core language libraries implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") - // LibEuFin util library - implementation(project(":util")) + implementation(project(":common")) implementation("org.postgresql:postgresql:$postgres_version") implementation("com.github.ajalt.clikt:clikt:$clikt_version") @@ -41,7 +40,7 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") testImplementation("io.ktor:ktor-server-test-host:$ktor_version") - testImplementation(project(":util")) + testImplementation(project(":common")) } application { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt @@ -26,7 +26,7 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.http.* -import net.taler.common.errorcodes.TalerErrorCode +import tech.libeufin.common.TalerErrorCode import tech.libeufin.bank.db.* import tech.libeufin.bank.db.WithdrawalDAO.* import java.lang.AssertionError diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -18,13 +18,10 @@ */ package tech.libeufin.bank -import tech.libeufin.util.* -import ConfigSource -import TalerConfig -import TalerConfigError +import tech.libeufin.common.* import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json -import tech.libeufin.util.DatabaseConfig +import tech.libeufin.common.DatabaseConfig /** * Application the parsed configuration. diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -18,7 +18,7 @@ */ package tech.libeufin.bank -import ConfigSource +import tech.libeufin.common.* import java.time.Duration // Config diff --git a/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt @@ -24,11 +24,10 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import java.util.* -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.auth.* import tech.libeufin.bank.db.ConversionDAO.* import tech.libeufin.bank.db.* -import net.taler.common.errorcodes.TalerErrorCode fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allowConversion) { get("/conversion-info/config") { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -32,8 +32,6 @@ import kotlinx.serialization.json.Json import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.future.await import kotlinx.coroutines.withContext -import net.taler.common.errorcodes.TalerErrorCode -import net.taler.wallet.crypto.Base32Crockford import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.bank.* @@ -45,7 +43,7 @@ import tech.libeufin.bank.db.CashoutDAO.* import tech.libeufin.bank.db.ExchangeDAO.* import tech.libeufin.bank.db.TransactionDAO.* import tech.libeufin.bank.db.WithdrawalDAO.* -import tech.libeufin.util.* +import tech.libeufin.common.* private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-api") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -23,8 +23,7 @@ import io.ktor.server.response.* import io.ktor.server.application.ApplicationCall import io.ktor.util.AttributeKey import kotlinx.serialization.Serializable -import net.taler.common.errorcodes.TalerErrorCode -import tech.libeufin.util.* +import tech.libeufin.common.* /** * Convenience type to throw errors along the bank activity * and that is meant to be caught by Ktor and responded to the diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -48,14 +48,13 @@ import java.io.File import kotlinx.coroutines.* import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level import org.postgresql.util.PSQLState import tech.libeufin.bank.db.AccountDAO.* import tech.libeufin.bank.db.* -import tech.libeufin.util.* +import tech.libeufin.common.* private val logger: Logger = LoggerFactory.getLogger("libeufin-bank") // Dirty local variable to stop the server in test TODO remove this ugly hack diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Params.kt b/bank/src/main/kotlin/tech/libeufin/bank/Params.kt @@ -27,8 +27,7 @@ import io.ktor.server.util.* import java.time.* import java.time.temporal.* import java.util.* -import net.taler.common.errorcodes.TalerErrorCode -import tech.libeufin.util.* +import tech.libeufin.common.* fun Parameters.expect(name: String): String = get(name) ?: throw badRequest("Missing '$name' parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt @@ -24,7 +24,7 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* import java.util.* -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.auth.* import tech.libeufin.bank.db.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt @@ -19,7 +19,7 @@ package tech.libeufin.bank -import tech.libeufin.util.* +import tech.libeufin.common.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import io.ktor.server.application.* @@ -33,9 +33,6 @@ import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode -import net.taler.wallet.crypto.Base32Crockford -import net.taler.wallet.crypto.EncodingException /** 32-byte Crockford's Base32 encoded data */ @Serializable(with = Base32Crockford32B.Serializer::class) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -19,12 +19,10 @@ package tech.libeufin.bank -import tech.libeufin.util.* +import tech.libeufin.common.* import io.ktor.http.* import io.ktor.server.application.* import kotlinx.serialization.* -import net.taler.wallet.crypto.Base32Crockford -import net.taler.wallet.crypto.EncodingException import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt @@ -28,7 +28,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.util.pipeline.PipelineContext import java.time.Instant -import net.taler.common.errorcodes.TalerErrorCode +import tech.libeufin.common.* import tech.libeufin.bank.db.* import tech.libeufin.bank.db.ExchangeDAO.* import tech.libeufin.bank.auth.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt @@ -26,12 +26,10 @@ import io.ktor.server.response.header import io.ktor.util.AttributeKey import io.ktor.util.pipeline.PipelineContext import java.time.Instant -import net.taler.common.errorcodes.TalerErrorCode -import net.taler.wallet.crypto.Base32Crockford import tech.libeufin.bank.db.AccountDAO.* import tech.libeufin.bank.db.* import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* /** Used to store if the currenly authenticated user is admin */ private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin"); diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -19,7 +19,7 @@ package tech.libeufin.bank.db -import tech.libeufin.util.* +import tech.libeufin.common.* import java.time.* import java.sql.Types import tech.libeufin.bank.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -22,7 +22,7 @@ package tech.libeufin.bank.db import java.time.Duration import java.time.Instant import java.util.concurrent.TimeUnit -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.* /** Data access logic for cashout operations */ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt @@ -19,7 +19,7 @@ package tech.libeufin.bank.db -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.* import tech.libeufin.bank.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -32,9 +32,8 @@ import java.util.concurrent.TimeUnit import kotlin.math.abs import kotlinx.coroutines.flow.* import kotlinx.coroutines.* -import tech.libeufin.util.* +import tech.libeufin.common.* import io.ktor.http.HttpStatusCode -import net.taler.common.errorcodes.TalerErrorCode import tech.libeufin.bank.* private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-db") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt @@ -23,7 +23,7 @@ import java.util.UUID import java.time.Instant import java.time.Duration import java.util.concurrent.TimeUnit -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.* /** Data access logic for exchange specific logic */ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.* import org.postgresql.ds.PGSimpleDataSource import org.slf4j.Logger import org.slf4j.LoggerFactory -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.* private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-db-watcher") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt @@ -19,7 +19,7 @@ package tech.libeufin.bank.db -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.* import tech.libeufin.bank.db.* import java.util.concurrent.TimeUnit diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt @@ -19,7 +19,7 @@ package tech.libeufin.bank.db -import tech.libeufin.util.* +import tech.libeufin.common.* import java.time.Instant import tech.libeufin.bank.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt @@ -21,7 +21,7 @@ package tech.libeufin.bank.db import org.slf4j.Logger import org.slf4j.LoggerFactory -import tech.libeufin.util.* +import tech.libeufin.common.* import java.time.* import java.sql.Types import tech.libeufin.bank.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -23,7 +23,7 @@ import java.util.UUID import java.time.Instant import java.time.Duration import java.util.concurrent.TimeUnit -import tech.libeufin.util.* +import tech.libeufin.common.* import kotlinx.coroutines.flow.* import kotlinx.coroutines.* import tech.libeufin.bank.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -36,8 +36,7 @@ import java.net.* import java.time.* import java.time.temporal.* import java.util.* -import net.taler.common.errorcodes.TalerErrorCode -import tech.libeufin.util.* +import tech.libeufin.common.* import tech.libeufin.bank.db.* import tech.libeufin.bank.db.AccountDAO.* import tech.libeufin.bank.auth.* diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -26,8 +26,7 @@ import tech.libeufin.bank.* import tech.libeufin.bank.db.* import tech.libeufin.bank.db.TransactionDAO.* import tech.libeufin.bank.db.WithdrawalDAO.* -import tech.libeufin.util.* -import net.taler.common.errorcodes.TalerErrorCode +import tech.libeufin.common.* class AmountTest { // Test amount computation in database diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt @@ -26,11 +26,10 @@ import java.util.* import kotlin.test.* import kotlinx.coroutines.* import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode import org.junit.Test import tech.libeufin.bank.* import tech.libeufin.bank.db.* -import tech.libeufin.util.* +import tech.libeufin.common.* class BankIntegrationApiTest { // GET /taler-integration/config diff --git a/bank/src/test/kotlin/ConversionApiTest.kt b/bank/src/test/kotlin/ConversionApiTest.kt @@ -26,10 +26,9 @@ import java.util.* import kotlin.test.* import kotlinx.coroutines.* import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode import org.junit.Test import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* class ConversionApiTest { // GET /conversion-info/config diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -30,12 +30,10 @@ import java.util.* import kotlin.test.* import kotlinx.coroutines.* import kotlinx.serialization.json.JsonElement -import net.taler.common.errorcodes.TalerErrorCode -import net.taler.wallet.crypto.Base32Crockford import org.junit.Test import tech.libeufin.bank.* import tech.libeufin.bank.db.* -import tech.libeufin.util.* +import tech.libeufin.common.* class CoreBankConfigTest { // GET /config diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.* import org.junit.Test import tech.libeufin.bank.* import tech.libeufin.bank.db.AccountDAO.* -import tech.libeufin.util.* +import tech.libeufin.common.* class DatabaseTest { diff --git a/bank/src/test/kotlin/JsonTest.kt b/bank/src/test/kotlin/JsonTest.kt @@ -25,7 +25,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.junit.Test import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* @Serializable data class MyJsonType( diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt @@ -27,7 +27,7 @@ import kotlin.test.* import kotlinx.coroutines.* import org.junit.Test import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* class SecurityTest { @Test diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt @@ -28,7 +28,7 @@ import java.util.* import kotlin.test.* import org.junit.Test import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* class StatsTest { @Test diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -25,10 +25,9 @@ import io.ktor.server.testing.* import java.util.* import kotlinx.coroutines.* import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode import org.junit.Test import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* class WireGatewayApiTest { // Testing the POST /transfer call from the TWG API. diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -29,12 +29,10 @@ import kotlin.test.* import kotlin.random.Random import kotlinx.coroutines.* import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode -import net.taler.wallet.crypto.Base32Crockford import tech.libeufin.bank.* import tech.libeufin.bank.db.* import tech.libeufin.bank.db.AccountDAO.* -import tech.libeufin.util.* +import tech.libeufin.common.* /* ----- Setup ----- */ diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt @@ -18,7 +18,7 @@ */ import tech.libeufin.bank.* -import tech.libeufin.util.* +import tech.libeufin.common.* import io.ktor.client.statement.HttpResponse import io.ktor.server.testing.ApplicationTestBuilder import io.ktor.client.request.* @@ -26,7 +26,6 @@ import io.ktor.http.* import kotlin.test.* import kotlinx.coroutines.* import kotlinx.serialization.json.* -import net.taler.common.errorcodes.TalerErrorCode // Test endpoint is correctly authenticated suspend fun ApplicationTestBuilder.authRoutine( diff --git a/build.gradle b/build.gradle @@ -52,7 +52,7 @@ task versionFile() { } def gitHash = stdout.toString().trim() def version = getRootProject().version - new File("${projectDir}/util/src/main/resources", "version.txt").text = "v$version-git-$gitHash" + new File("${projectDir}/common/src/main/resources", "version.txt").text = "v$version-git-$gitHash" } // See: https://stackoverflow.com/questions/24936781/gradle-plugin-project-version-number diff --git a/common/build.gradle b/common/build.gradle @@ -0,0 +1,30 @@ +plugins { + id("kotlin") + id("org.jetbrains.kotlin.plugin.serialization") version "$kotlin_version" +} + +version = rootProject.version + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +compileKotlin.kotlinOptions.jvmTarget = "17" +compileTestKotlin.kotlinOptions.jvmTarget = "17" + +sourceSets.main.java.srcDirs = ["src/main/kotlin"] + +dependencies { + implementation("ch.qos.logback:logback-classic:1.4.5") + // Crypto + implementation("org.bouncycastle:bcprov-jdk15on:1.69") + // Database helper + implementation("org.postgresql:postgresql:$postgres_version") + implementation("com.zaxxer:HikariCP:5.0.1") + + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("io.ktor:ktor-server-test-host:$ktor_version") + implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") + implementation("com.github.ajalt.clikt:clikt:$clikt_version") +} +\ No newline at end of file diff --git a/common/import.py b/common/import.py @@ -0,0 +1,66 @@ +# Update EBICS constants file using latest external code sets files + +import requests +from zipfile import ZipFile +from io import BytesIO +import polars as pl + +# Get XLSX zip file from server +r = requests.get( + "https://www.iso20022.org/sites/default/files/media/file/ExternalCodeSets_XLSX.zip" +) +assert r.status_code == 200 + +# Unzip the XLSX file +zip = ZipFile(BytesIO(r.content)) +files = zip.namelist() +assert len(files) == 1 +file = zip.open(files[0]) + +# Parse excel +df = pl.read_excel(file, sheet_name="AllCodeSets") + +def extractCodeSet(setName: str, className: str) -> str: + out = f"enum class {className}(val isoCode: String, val description: String) {{" + + for row in df.filter(pl.col("Code Set") == setName).sort("Code Value").rows(named=True): + (value, isoCode, description) = ( + row["Code Value"], + row["Code Name"], + row["Code Definition"].split("\n", 1)[0].strip(), + ) + out += f'\n\t{value}("{isoCode}", "{description}"),' + + out += "\n}" + return out + +# Write kotlin file +kt = f"""/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +// THIS FILE IS GENERATED, DO NOT EDIT + +package tech.libeufin.common + +{extractCodeSet("ExternalStatusReason1Code", "ExternalStatusReasonCode")} + +{extractCodeSet("ExternalPaymentGroupStatus1Code", "ExternalPaymentGroupStatusCode")} +""" +with open("src/main/kotlin/EbicsCodeSets.kt", "w") as file1: + file1.write(kt) diff --git a/common/src/main/kotlin/Backoff.kt b/common/src/main/kotlin/Backoff.kt @@ -0,0 +1,41 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + + * 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.common + +import kotlin.random.Random + +/** Infinite exponential backoff with decorrelated jitter */ +class ExpoBackoffDecorr( + private val base: Long = 100, // 0.1 second + private val max: Long = 5000, // 5 second + private val factor: Double = 2.0, +) { + private var sleep: Long = base + + public fun next() : Long { + sleep = Random.nextDouble(base.toDouble(), sleep.toDouble() * factor) + .toLong().coerceAtMost(max) + return sleep + } + + public fun reset() { + sleep = base + } +} +\ No newline at end of file diff --git a/common/src/main/kotlin/Cli.kt b/common/src/main/kotlin/Cli.kt @@ -0,0 +1,131 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + + * 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.common + +import com.github.ajalt.clikt.core.* +import com.github.ajalt.clikt.parameters.types.* +import com.github.ajalt.clikt.parameters.arguments.* +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.groups.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.event.Level + +private val logger: Logger = LoggerFactory.getLogger("libeufin-config") + +fun cliCmd(logger: Logger, level: Level, lambda: () -> Unit) { + // Set root log level + val root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger + root.setLevel(ch.qos.logback.classic.Level.convertAnSLF4JLevel(level)); + // Run cli command catching all errors + try { + lambda() + } catch (e: Throwable) { + var msg = StringBuilder(e.message) + var cause = e.cause; + while (cause != null) { + msg.append(": ") + msg.append(cause.message) + cause = cause.cause + } + logger.error(msg.toString()) + logger.debug("$e", e) + throw ProgramResult(1) + } +} + +private fun talerConfig(configSource: ConfigSource, configPath: String?): TalerConfig { + val config = TalerConfig(configSource) + config.load(configPath) + return config +} + +class CommonOption: OptionGroup() { + val config by option( + "--config", "-c", + help = "Specifies the configuration file" + ).path( + mustExist = true, + canBeDir = false, + mustBeReadable = true, + ).convert { it.toString() } // TODO take path to load config + val log by option( + "--log", "-L", + help = "Configure logging to use LOGLEVEL" + ).enum<Level>().default(Level.INFO) +} + +class CliConfigCmd(configSource: ConfigSource) : CliktCommand("Inspect or change the configuration", name = "config") { + init { + subcommands(CliConfigDump(configSource), CliConfigPathsub(configSource), CliConfigGet(configSource)) + } + + override fun run() = Unit +} + +private class CliConfigGet(private val configSource: ConfigSource) : CliktCommand("Lookup config value", name = "get") { + private val common by CommonOption() + private val isPath by option( + "--filename", "-f", + help = "Interpret value as path with dollar-expansion" + ).flag() + private val sectionName by argument() + private val optionName by argument() + + + override fun run() = cliCmd(logger, common.log) { + val config = talerConfig(configSource, common.config) + if (isPath) { + val res = config.lookupPath(sectionName, optionName) + if (res == null) { + throw Exception("value not found in config") + } + println(res) + } else { + val res = config.lookupString(sectionName, optionName) + if (res == null) { + throw Exception("value not found in config") + } + println(res) + } + } +} + + + +private class CliConfigPathsub(private val configSource: ConfigSource) : CliktCommand("Substitute variables in a path", name = "pathsub") { + private val common by CommonOption() + private val pathExpr by argument() + + override fun run() = cliCmd(logger, common.log) { + val config = talerConfig(configSource, common.config) + println(config.pathsub(pathExpr)) + } +} + +private class CliConfigDump(private val configSource: ConfigSource) : CliktCommand("Dump the configuration", name = "dump") { + private val common by CommonOption() + + override fun run() = cliCmd(logger, common.log) { + val config = talerConfig(configSource, common.config) + println("# install path: ${config.getInstallPath()}") + println(config.stringify()) + } +} diff --git a/common/src/main/kotlin/Client.kt b/common/src/main/kotlin/Client.kt @@ -0,0 +1,80 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + + * 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.common + +import io.ktor.http.* +import kotlinx.serialization.json.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import java.io.ByteArrayOutputStream +import java.util.zip.DeflaterOutputStream +import kotlin.test.assertEquals + +/* ----- Json DSL ----- */ + +inline fun obj(from: JsonObject = JsonObject(emptyMap()), builderAction: JsonBuilder.() -> Unit): JsonObject { + val builder = JsonBuilder(from) + builder.apply(builderAction) + return JsonObject(builder.content) +} + +class JsonBuilder(from: JsonObject) { + val content: MutableMap<String, JsonElement> = from.toMutableMap() + + infix inline fun <reified T> String.to(v: T) { + val json = Json.encodeToJsonElement(kotlinx.serialization.serializer<T>(), v); + content.put(this, json) + } +} + +/* ----- Json body helper ----- */ + +inline fun <reified B> HttpRequestBuilder.json(b: B, deflate: Boolean = false) { + val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b); + contentType(ContentType.Application.Json) + if (deflate) { + headers.set("Content-Encoding", "deflate") + val bos = ByteArrayOutputStream() + val ios = DeflaterOutputStream(bos) + ios.write(json.toByteArray()) + ios.finish() + setBody(bos.toByteArray()) + } else { + setBody(json) + } +} + +inline fun HttpRequestBuilder.json( + from: JsonObject = JsonObject(emptyMap()), + deflate: Boolean = false, + builderAction: JsonBuilder.() -> Unit +) { + json(obj(from, builderAction), deflate) +} + +inline suspend fun <reified B> HttpResponse.json(): B = + Json.decodeFromString(kotlinx.serialization.serializer<B>(), bodyAsText()) + +inline suspend fun <reified B> HttpResponse.assertOkJson(lambda: (B) -> Unit = {}): B { + assertEquals(HttpStatusCode.OK, status) + val body = json<B>() + lambda(body) + return body +} +\ No newline at end of file diff --git a/common/src/main/kotlin/Config.kt b/common/src/main/kotlin/Config.kt @@ -0,0 +1,36 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import ch.qos.logback.core.util.Loader + +/** + * Putting those values into the 'attributes' container because they + * are needed by the util routines that do NOT have Sandbox and Nexus + * as dependencies, and therefore cannot access their global variables. + * + * Note: putting Sandbox and Nexus as Utils dependencies would result + * into circular dependency. + */ +fun getVersion(): String { + return Loader.getResource( + "version.txt", ClassLoader.getSystemClassLoader() + ).readText() +} +\ No newline at end of file diff --git a/common/src/main/kotlin/Constants.kt b/common/src/main/kotlin/Constants.kt @@ -0,0 +1,23 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +// DB +const val MIN_VERSION: Int = 14 +const val SERIALIZATION_RETRY: Int = 10; +\ No newline at end of file diff --git a/common/src/main/kotlin/CryptoUtil.kt b/common/src/main/kotlin/CryptoUtil.kt @@ -0,0 +1,329 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.ByteArrayOutputStream +import java.math.BigInteger +import java.security.* +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.* +import javax.crypto.* +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.PBEParameterSpec +import javax.crypto.spec.SecretKeySpec + +/** + * Helpers for dealing with cryptographic operations in EBICS / LibEuFin. + */ +object CryptoUtil { + + /** + * RSA key pair. + */ + data class RsaCrtKeyPair(val private: RSAPrivateCrtKey, val public: RSAPublicKey) + + // FIXME(dold): This abstraction needs to be improved. + class EncryptionResult( + val encryptedTransactionKey: ByteArray, + val pubKeyDigest: ByteArray, + val encryptedData: ByteArray, + /** + * This key needs to be reused between different upload phases. + */ + val plainTransactionKey: SecretKey? = null + ) + + 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.toUnsignedHexString().lowercase().trimStart('0').toByteArray()) + keyBytes.write(' '.code) + keyBytes.writeBytes(publicKey.modulus.toUnsignedHexString().lowercase().trimStart('0').toByteArray()) + // println("buffer before hashing: '${keyBytes.toString(Charsets.UTF_8)}'") + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(keyBytes.toByteArray()) + } + + fun encryptEbicsE002(data: ByteArray, encryptionPublicKey: RSAPublicKey): EncryptionResult { + val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) + keygen.init(128) + val transactionKey = keygen.generateKey() + return encryptEbicsE002withTransactionKey( + data, + encryptionPublicKey, + transactionKey + ) + } + /** + * Encrypt data according to the EBICS E002 encryption process. + */ + fun encryptEbicsE002withTransactionKey( + data: ByteArray, + encryptionPublicKey: RSAPublicKey, + transactionKey: SecretKey + ): EncryptionResult { + 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, + transactionKey + ) + } + + fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { + return decryptEbicsE002( + enc.encryptedTransactionKey, + enc.encryptedData, + privateKey + ) + } + + fun decryptEbicsE002( + encryptedTransactionKey: ByteArray, + encryptedData: ByteArray, + privateKey: RSAPrivateCrtKey + ): ByteArray { + val asymmetricCipher = Cipher.getInstance( + "RSA/None/PKCS1Padding", + bouncyCastleProvider + ) + asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) + val transactionKeyBytes = asymmetricCipher.doFinal(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(encryptedData) + return data + } + + /** + * Signing algorithm corresponding to the EBICS A006 signing process. + * + * Note that while [data] can be arbitrary-length data, in EBICS, the order + * data is *always* hashed *before* passing it to the signing algorithm, which again + * uses a hash internally. + */ + fun signEbicsA006(data: ByteArray, privateKey: RSAPrivateCrtKey): ByteArray { + val signature = Signature.getInstance("SHA256withRSA/PSS", bouncyCastleProvider) + signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) + signature.initSign(privateKey) + signature.update(data) + return signature.sign() + } + + fun verifyEbicsA006(sig: ByteArray, data: ByteArray, publicKey: RSAPublicKey): Boolean { + val signature = Signature.getInstance("SHA256withRSA/PSS", bouncyCastleProvider) + signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) + signature.initVerify(publicKey) + signature.update(data) + return signature.verify(sig) + } + + fun digestEbicsOrderA006(orderData: ByteArray): ByteArray { + val digest = MessageDigest.getInstance("SHA-256") + for (b in orderData) { + when (b) { + '\r'.code.toByte(), '\n'.code.toByte(), (26).toByte() -> Unit + else -> digest.update(b) + } + } + return digest.digest() + } + + fun decryptKey(data: EncryptedPrivateKeyInfo, passphrase: String): RSAPrivateCrtKey { + /* make key out of passphrase */ + val pbeKeySpec = PBEKeySpec(passphrase.toCharArray()) + val keyFactory = SecretKeyFactory.getInstance(data.algName) + val secretKey = keyFactory.generateSecret(pbeKeySpec) + /* Make a cipher */ + val cipher = Cipher.getInstance(data.algName) + cipher.init( + Cipher.DECRYPT_MODE, + secretKey, + data.algParameters // has hash count and salt + ) + /* Ready to decrypt */ + val decryptedKeySpec: PKCS8EncodedKeySpec = data.getKeySpec(cipher) + val priv = KeyFactory.getInstance("RSA").generatePrivate(decryptedKeySpec) + if (priv !is RSAPrivateCrtKey) + throw Exception("wrong encoding") + return priv + } + + fun encryptKey(data: ByteArray, passphrase: String): ByteArray { + /* Cipher parameters: salt and hash count */ + val hashIterations = 30 + val salt = ByteArray(8) + SecureRandom().nextBytes(salt) + val pbeParameterSpec = PBEParameterSpec(salt, hashIterations) + /* *Other* cipher parameters: symmetric key (from password) */ + val pbeAlgorithm = "PBEWithSHA1AndDESede" + val pbeKeySpec = PBEKeySpec(passphrase.toCharArray()) + val keyFactory = SecretKeyFactory.getInstance(pbeAlgorithm) + val secretKey = keyFactory.generateSecret(pbeKeySpec) + /* Make a cipher */ + val cipher = Cipher.getInstance(pbeAlgorithm) + cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec) + /* ready to encrypt now */ + val cipherText = cipher.doFinal(data) + /* Must now bundle a PKCS#8-compatible object, that contains + * algorithm, salt and hash count information */ + val bundleAlgorithmParams = AlgorithmParameters.getInstance(pbeAlgorithm) + bundleAlgorithmParams.init(pbeParameterSpec) + val bundle = EncryptedPrivateKeyInfo(bundleAlgorithmParams, cipherText) + return bundle.encoded + } + + fun checkValidEddsaPublicKey(enc: String): Boolean { + val data = try { + Base32Crockford.decode(enc) + } catch (e: Exception) { + return false + } + if (data.size != 32) { + return false + } + return true + } + + fun hashStringSHA256(input: String): ByteArray { + return MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8)) + } + + fun hashpw(pw: String): String { + val saltBytes = ByteArray(8) + SecureRandom().nextBytes(saltBytes) + val salt = bytesToBase64(saltBytes) + val pwh = bytesToBase64(CryptoUtil.hashStringSHA256("$salt|$pw")) + return "sha256-salted\$$salt\$$pwh" + } + + fun checkpw(pw: String, storedPwHash: String): Boolean { + val components = storedPwHash.split('$') + when (val algo = components[0]) { + "sha256" -> { // Support legacy unsalted passwords + if (components.size != 2) throw Exception("bad password hash") + val hash = components[1] + val pwh = bytesToBase64(CryptoUtil.hashStringSHA256(pw)) + return pwh == hash + } + "sha256-salted" -> { + if (components.size != 3) throw Exception("bad password hash") + val salt = components[1] + val hash = components[2] + val pwh = bytesToBase64(CryptoUtil.hashStringSHA256("$salt|$pw")) + return pwh == hash + } + else -> throw Exception("unsupported hash algo: '$algo'") + } + } +} diff --git a/common/src/main/kotlin/DB.kt b/common/src/main/kotlin/DB.kt @@ -0,0 +1,339 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + * + * 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.common + +import org.postgresql.ds.PGSimpleDataSource +import org.postgresql.jdbc.PgConnection +import org.postgresql.util.PSQLState +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.net.URI +import java.sql.PreparedStatement +import java.sql.ResultSet +import java.sql.SQLException +import kotlinx.coroutines.* +import com.zaxxer.hikari.* + +fun getCurrentUser(): String = System.getProperty("user.name") + +private val logger: Logger = LoggerFactory.getLogger("libeufin-db") + +// Check GANA (https://docs.gnunet.org/gana/index.html) for numbers allowance. + +/** + * This function converts postgresql:// URIs to JDBC URIs. + * + * URIs that are already jdbc: URIs are passed through. + * + * This avoids the user having to create complex JDBC URIs for postgres connections. + * They are especially complex when using unix domain sockets, as they're not really + * supported natively by JDBC. + */ +fun getJdbcConnectionFromPg(pgConn: String): String { + // Pass through jdbc URIs. + if (pgConn.startsWith("jdbc:")) { + return pgConn + } + if (!pgConn.startsWith("postgresql://") && !pgConn.startsWith("postgres://")) { + logger.info("Not a Postgres connection string: $pgConn") + throw Exception("Not a Postgres connection string: $pgConn") + } + var maybeUnixSocket = false + val parsed = URI(pgConn) + val hostAsParam: String? = if (parsed.query != null) { + getQueryParam(parsed.query, "host") + } else { + null + } + /** + * In some cases, it is possible to leave the hostname empty + * and specify it via a query param, therefore a "postgresql:///"-starting + * connection string does NOT always mean Unix domain socket. + * https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING + */ + if (parsed.host == null && + (hostAsParam == null || hostAsParam.startsWith('/')) + ) { + maybeUnixSocket = true + } + if (maybeUnixSocket) { + // Check whether the database user should differ from the process user. + var pgUser = getCurrentUser() + if (parsed.query != null) { + val maybeUserParam = getQueryParam(parsed.query, "user") + if (maybeUserParam != null) pgUser = maybeUserParam + } + // Check whether the Unix domain socket location was given non-standard. + val socketLocation = hostAsParam ?: "/var/run/postgresql/.s.PGSQL.5432" + if (!socketLocation.startsWith('/')) { + throw Exception("PG connection wants Unix domain socket, but non-null host doesn't start with slash") + } + return "jdbc:postgresql://localhost${parsed.path}?user=$pgUser&socketFactory=org.newsclub.net.unix." + + "AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=$socketLocation" + } + if (pgConn.startsWith("postgres://")) { + // The JDBC driver doesn't like postgres://, only postgresql://. + // For consistency with other components, we normalize the postgres:// URI + // into one that the JDBC driver likes. + return "jdbc:postgresql://" + pgConn.removePrefix("postgres://") + } + return "jdbc:$pgConn" +} + +data class DatabaseConfig( + val dbConnStr: String, + val sqlDir: String +) + +fun pgDataSource(dbConfig: String): PGSimpleDataSource { + val jdbcConnStr = getJdbcConnectionFromPg(dbConfig) + logger.info("connecting to database via JDBC string '$jdbcConnStr'") + val pgSource = PGSimpleDataSource() + pgSource.setUrl(jdbcConnStr) + pgSource.prepareThreshold = 1 + return pgSource +} + +fun PGSimpleDataSource.pgConnection(): PgConnection { + val conn = connection.unwrap(PgConnection::class.java) + // FIXME: bring the DB schema to a function argument. + conn.execSQLUpdate("SET search_path TO libeufin_bank;") + return conn +} + +fun <R> PgConnection.transaction(lambda: (PgConnection) -> R): R { + try { + setAutoCommit(false); + val result = lambda(this) + commit(); + setAutoCommit(true); + return result + } catch(e: Exception){ + rollback(); + setAutoCommit(true); + throw e; + } +} + +fun <T> PreparedStatement.oneOrNull(lambda: (ResultSet) -> T): T? { + executeQuery().use { + if (!it.next()) return null + return lambda(it) + } +} + +fun <T> PreparedStatement.all(lambda: (ResultSet) -> T): List<T> { + executeQuery().use { + val ret = mutableListOf<T>() + while (it.next()) { + ret.add(lambda(it)) + } + return ret + } +} + +fun PreparedStatement.executeQueryCheck(): Boolean { + executeQuery().use { + return it.next() + } +} + +fun PreparedStatement.executeUpdateCheck(): Boolean { + executeUpdate() + return updateCount > 0 +} + +/** + * Helper that returns false if the row to be inserted + * hits a unique key constraint violation, true when it + * succeeds. Any other error (re)throws exception. + */ +fun PreparedStatement.executeUpdateViolation(): Boolean { + return try { + executeUpdateCheck() + } catch (e: SQLException) { + logger.debug(e.message) + if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false + throw e // rethrowing, not to hide other types of errors. + } +} + +fun PreparedStatement.executeProcedureViolation(): Boolean { + val savepoint = connection.setSavepoint(); + return try { + executeUpdate() + connection.releaseSavepoint(savepoint) + true + } catch (e: SQLException) { + connection.rollback(savepoint); + if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false + throw e // rethrowing, not to hide other types of errors. + } +} + +// TODO comment +fun PgConnection.dynamicUpdate( + table: String, + fields: Sequence<String>, + filter: String, + bind: Sequence<Any?>, +) { + val sql = fields.joinToString() + if (sql.isEmpty()) return + prepareStatement("UPDATE $table SET $sql $filter").run { + for ((idx, value) in bind.withIndex()) { + setObject(idx+1, value) + } + executeUpdate() + } +} + +/** + * Only runs versioning.sql if the _v schema is not found. + * + * @param conn database connection + * @param cfg database configuration + */ +fun maybeApplyV(conn: PgConnection, cfg: DatabaseConfig) { + conn.transaction { + val checkVSchema = conn.prepareStatement( + "SELECT schema_name FROM information_schema.schemata WHERE schema_name = '_v'" + ) + if (!checkVSchema.executeQueryCheck()) { + logger.debug("_v schema not found, applying versioning.sql") + val sqlVersioning = File("${cfg.sqlDir}/versioning.sql").readText() + conn.execSQLUpdate(sqlVersioning) + } + } +} + +// sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash). +fun initializeDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { + logger.info("doing DB initialization, sqldir ${cfg.sqlDir}") + maybeApplyV(conn, cfg) + conn.transaction { + val checkStmt = conn.prepareStatement("SELECT count(*) as n FROM _v.patches where patch_name = ?") + + for (n in 1..9999) { + val numStr = n.toString().padStart(4, '0') + val patchName = "$sqlFilePrefix-$numStr" + + checkStmt.setString(1, patchName) + val patchCount = checkStmt.oneOrNull { it.getInt(1) } ?: throw Exception("unable to query patches"); + if (patchCount >= 1) { + logger.info("patch $patchName already applied") + continue + } + + val path = File("${cfg.sqlDir}/$sqlFilePrefix-$numStr.sql") + if (!path.exists()) { + logger.info("path $path doesn't exist anymore, stopping") + break + } + logger.info("applying patch $path") + val sqlPatchText = path.readText() + conn.execSQLUpdate(sqlPatchText) + } + val sqlProcedures = File("${cfg.sqlDir}/$sqlFilePrefix-procedures.sql") + if (!sqlProcedures.exists()) { + logger.info("no procedures.sql for the SQL collection: $sqlFilePrefix") + return@transaction + } + logger.info("run procedure.sql") + conn.execSQLUpdate(sqlProcedures.readText()) + } +} + +// sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash). +fun resetDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { + logger.info("reset DB, sqldir ${cfg.sqlDir}") + val isInitialized = conn.prepareStatement(""" + SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name='_v') AND + EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name='${sqlFilePrefix.replace("-", "_")}') + """).oneOrNull { + it.getBoolean(1) + }!! + if (!isInitialized) { + logger.info("versioning schema not present, not running drop sql") + return + } + + val sqlDrop = File("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText() + conn.execSQLUpdate(sqlDrop) +} + +abstract class DbPool(cfg: String, schema: String): java.io.Closeable { + val pgSource = pgDataSource(cfg) + private val pool: HikariDataSource + + init { + val config = HikariConfig(); + config.dataSource = pgSource + config.schema = schema + config.transactionIsolation = "TRANSACTION_SERIALIZABLE" + pool = HikariDataSource(config) + pool.getConnection().use { con -> + val meta = con.getMetaData(); + val majorVersion = meta.getDatabaseMajorVersion() + val minorVersion = meta.getDatabaseMinorVersion() + if (majorVersion < MIN_VERSION) { + throw Exception("postgres version must be at least $MIN_VERSION.0 got $majorVersion.$minorVersion") + } + } + } + + suspend fun <R> conn(lambda: suspend (PgConnection) -> R): R { + // Use a coroutine dispatcher that we can block as JDBC API is blocking + return withContext(Dispatchers.IO) { + val conn = pool.getConnection() + conn.use{ it -> lambda(it.unwrap(PgConnection::class.java)) } + } + } + + suspend fun <R> serializable(lambda: suspend (PgConnection) -> R): R = conn { conn -> + repeat(SERIALIZATION_RETRY) { + try { + return@conn lambda(conn); + } catch (e: SQLException) { + if (e.sqlState != PSQLState.SERIALIZATION_FAILURE.state) + throw e + } + } + try { + return@conn lambda(conn) + } catch(e: SQLException) { + logger.warn("Serialization failure after $SERIALIZATION_RETRY retry") + throw e + } + } + + override fun close() { + pool.close() + } +} + +fun ResultSet.getAmount(name: String, currency: String): TalerAmount{ + return TalerAmount( + getLong("${name}_val"), + getInt("${name}_frac"), + currency + ) +} +\ No newline at end of file diff --git a/common/src/main/kotlin/Encoding.kt b/common/src/main/kotlin/Encoding.kt @@ -0,0 +1,152 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import java.io.ByteArrayOutputStream + +class EncodingException : Exception("Invalid encoding") + +object Base32Crockford { + + private fun ByteArray.getIntAt(index: Int): Int { + val x = this[index].toInt() + return if (x >= 0) x else (x + 256) + } + + private var encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + + fun encode(data: ByteArray): String { + val sb = StringBuilder() + var inputChunkBuffer = 0 + var pendingBitsCount = 0 + var inputCursor = 0 + var inputChunkNumber = 0 + + while (inputCursor < data.size) { + // Read input + inputChunkNumber = data.getIntAt(inputCursor++) + inputChunkBuffer = (inputChunkBuffer shl 8) or inputChunkNumber + pendingBitsCount += 8 + // Write symbols + while (pendingBitsCount >= 5) { + val symbolIndex = inputChunkBuffer.ushr(pendingBitsCount - 5) and 31 + sb.append(encTable[symbolIndex]) + pendingBitsCount -= 5 + } + } + if (pendingBitsCount >= 5) + throw Exception("base32 encoder did not write all the symbols") + + if (pendingBitsCount > 0) { + val symbolIndex = (inputChunkNumber shl (5 - pendingBitsCount)) and 31 + sb.append(encTable[symbolIndex]) + } + val enc = sb.toString() + val oneMore = ((data.size * 8) % 5) > 0 + val expectedLength = if (oneMore) { + ((data.size * 8) / 5) + 1 + } else { + (data.size * 8) / 5 + } + if (enc.length != expectedLength) + throw Exception("base32 encoding has wrong length") + return enc + } + + /** + * Decodes the input to its binary representation, throws + * net.taler.wallet.crypto.EncodingException on invalid encodings. + */ + fun decode( + encoded: String, + out: ByteArrayOutputStream + ) { + var outBitsCount = 0 + var bitsBuffer = 0 + var inputCursor = 0 + + while (inputCursor < encoded.length) { + val decodedNumber = getValue(encoded[inputCursor++]) + bitsBuffer = (bitsBuffer shl 5) or decodedNumber + outBitsCount += 5 + while (outBitsCount >= 8) { + val outputChunk = (bitsBuffer ushr (outBitsCount - 8)) and 0xFF + out.write(outputChunk) + outBitsCount -= 8 // decrease of written bits. + } + } + if ((encoded.length * 5) / 8 != out.size()) + throw Exception("base32 decoder: wrong output size") + } + + fun decode(encoded: String): ByteArray { + val out = ByteArrayOutputStream() + decode(encoded, out) + val blob = out.toByteArray() + return blob + } + + private fun getValue(chr: Char): Int { + var a = chr + when (a) { + 'O', 'o' -> a = '0' + 'i', 'I', 'l', 'L' -> a = '1' + 'u', 'U' -> a = 'V' + } + if (a in '0'..'9') + return a - '0' + if (a in 'a'..'z') + a = Character.toUpperCase(a) + var dec = 0 + if (a in 'A'..'Z') { + if ('I' < a) dec++ + if ('L' < a) dec++ + if ('O' < a) dec++ + if ('U' < a) dec++ + return a - 'A' + 10 - dec + } + throw EncodingException() + } + + /** + * Compute the length of the resulting string when encoding data of the given size + * in bytes. + * + * @param dataSize size of the data to encode in bytes + * @return size of the string that would result from encoding + */ + @Suppress("unused") + fun calculateEncodedStringLength(dataSize: Int): Int { + return (dataSize * 8 + 4) / 5 + } + + /** + * Compute the length of the resulting data in bytes when decoding a (valid) string of the + * given size. + * + * @param stringSize size of the string to decode + * @return size of the resulting data in bytes + */ + @Suppress("unused") + fun calculateDecodedDataLength(stringSize: Int): Int { + return stringSize * 5 / 8 + } +} + diff --git a/common/src/main/kotlin/HTTP.kt b/common/src/main/kotlin/HTTP.kt @@ -0,0 +1,67 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import io.ktor.http.* +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.util.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +private val logger: Logger = LoggerFactory.getLogger("libeufin-common") + +// Get the base URL of a request, returns null if any problem occurs. +fun ApplicationRequest.getBaseUrl(): String? { + return if (this.headers.contains("X-Forwarded-Host")) { + logger.info("Building X-Forwarded- base URL") + // FIXME: should tolerate a missing X-Forwarded-Prefix. + var prefix: String = this.headers["X-Forwarded-Prefix"] + ?: run { + logger.error("Reverse proxy did not define X-Forwarded-Prefix") + return null + } + if (!prefix.endsWith("/")) + prefix += "/" + URLBuilder( + protocol = URLProtocol( + name = this.headers["X-Forwarded-Proto"] ?: run { + logger.error("Reverse proxy did not define X-Forwarded-Proto") + return null + }, + defaultPort = -1 // Port must be specified with X-Forwarded-Host. + ), + host = this.headers["X-Forwarded-Host"] ?: run { + logger.error("Reverse proxy did not define X-Forwarded-Host") + return null + } + ).apply { + encodedPath = prefix + // Gets dropped otherwise. + if (!encodedPath.endsWith("/")) + encodedPath += "/" + }.buildString() + } else { + this.call.url { + parameters.clear() + encodedPath = "/" + } + } +} +\ No newline at end of file diff --git a/common/src/main/kotlin/IbanPayto.kt b/common/src/main/kotlin/IbanPayto.kt @@ -0,0 +1,104 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.net.URI +import java.net.URLDecoder + +private val logger: Logger = LoggerFactory.getLogger("libeufin-common") + +// Payto information. +data class IbanPayto( + // represent query param "sender-name" or "receiver-name". + val receiverName: String?, + val iban: String, + val bic: String?, + // Typically, a wire transfer's subject. + val message: String?, + val amount: String? +) + +// Return the value of query string parameter 'name', or null if not found. +// 'params' is the list of key-value elements of all the query parameters found in the URI. +private fun getQueryParamOrNull(name: String, params: List<Pair<String, String>>?): String? { + if (params == null) return null + return params.firstNotNullOfOrNull { pair -> + URLDecoder.decode(pair.second, Charsets.UTF_8).takeIf { pair.first == name } + } +} + +// Parses a Payto URI, returning null if the input is invalid. +fun parsePayto(payto: String): IbanPayto? { + /** + * This check is due because URIs having a "payto:" prefix without + * slashes are correctly parsed by the Java 'URI' class. 'mailto' + * for example lacks the double-slash part. + */ + if (!payto.startsWith("payto://")) { + logger.error("Invalid payto URI: $payto") + return null + } + + val javaParsedUri = try { + URI(payto) + } catch (e: java.lang.Exception) { + logger.error("'${payto}' is not a valid URI") + return null + } + if (javaParsedUri.scheme != "payto") { + logger.error("'${payto}' is not payto") + return null + } + val wireMethod = javaParsedUri.host + if (wireMethod != "iban") { + logger.error("Only 'iban' is supported, not '$wireMethod'") + return null + } + val splitPath = javaParsedUri.path.split("/").filter { it.isNotEmpty() } + if (splitPath.size > 2) { + logger.error("too many path segments in iban payto URI: $payto") + return null + } + val (iban, bic) = if (splitPath.size == 1) { + Pair(splitPath[0], null) + } else Pair(splitPath[1], splitPath[0]) + + val params: List<Pair<String, String>>? = if (javaParsedUri.query != null) { + val queryString: List<String> = javaParsedUri.query.split("&") + queryString.map { + val split = it.split("="); + if (split.size != 2) { + logger.error("parameter '$it' was malformed") + return null + } + Pair(split[0], split[1]) + } + } else null + + return IbanPayto( + iban = iban, + bic = bic, + amount = getQueryParamOrNull("amount", params), + message = getQueryParamOrNull("message", params), + receiverName = getQueryParamOrNull("receiver-name", params) + ) +} +\ No newline at end of file diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt @@ -0,0 +1,100 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + * + * 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.common + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.json.* + +sealed class CommonError(msg: String): Exception(msg) { + class AmountFormat(msg: String): CommonError(msg) + class AmountNumberTooBig(msg: String): CommonError(msg) +} + +@Serializable(with = TalerAmount.Serializer::class) +class TalerAmount { + val value: Long + val frac: Int + val currency: String + + constructor(value: Long, frac: Int, currency: String) { + this.value = value + this.frac = frac + this.currency = currency + } + constructor(encoded: String) { + val match = PATTERN.matchEntire(encoded) ?: + throw CommonError.AmountFormat("Invalid amount format"); + val (currency, value, frac) = match.destructured + this.currency = currency + this.value = value.toLongOrNull() ?: + throw CommonError.AmountFormat("Invalid value") + if (this.value > MAX_VALUE) + throw CommonError.AmountNumberTooBig("Value specified in amount is too large") + this.frac = if (frac.isEmpty()) { + 0 + } else { + var tmp = frac.toIntOrNull() ?: + throw CommonError.AmountFormat("Invalid fractional value") + if (tmp > FRACTION_BASE) + throw CommonError.AmountFormat("Fractional calue specified in amount is too large") + repeat(8 - frac.length) { + tmp *= 10 + } + tmp + } + } + + override fun equals(other: Any?): Boolean { + return other is TalerAmount && + other.value == this.value && + other.frac == this.frac && + other.currency == this.currency + } + + override fun toString(): String { + if (frac == 0) { + return "$currency:$value" + } else { + return "$currency:$value.${frac.toString().padStart(8, '0')}" + .dropLastWhile { it == '0' } // Trim useless fractional trailing 0 + } + } + + internal object Serializer : KSerializer<TalerAmount> { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: TalerAmount) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): TalerAmount { + return TalerAmount(decoder.decodeString()) + } + } + + companion object { + const val FRACTION_BASE = 100000000 + const val MAX_VALUE = 4503599627370496L; // 2^52 + private val PATTERN = Regex("([A-Z]{1,11}):([0-9]+)(?:\\.([0-9]{1,8}))?"); + } +} +\ No newline at end of file diff --git a/common/src/main/kotlin/TalerConfig.kt b/common/src/main/kotlin/TalerConfig.kt @@ -0,0 +1,489 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + * + * 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.common + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.file.Paths +import kotlin.io.path.Path +import kotlin.io.path.isReadable +import kotlin.io.path.listDirectoryEntries + +private val logger: Logger = LoggerFactory.getLogger("libeufin-config") + +private data class Section( + val entries: MutableMap<String, String>, +) + +private val reEmptyLine = Regex("^\\s*$") +private val reComment = Regex("^\\s*#.*$") +private val reSection = Regex("^\\s*\\[\\s*([^]]*)\\s*]\\s*$") +private val reParam = Regex("^\\s*([^=]+?)\\s*=\\s*(.*?)\\s*$") +private val reDirective = Regex("^\\s*@([a-zA-Z-_]+)@\\s*(.*?)\\s*$") + +class TalerConfigError(m: String) : Exception(m) + +/** + * Information about how the configuration is loaded. + * + * The entry point for the configuration will be the first file from this list: + * - /etc/$projectName/$componentName.conf + * - /etc/$componentName.conf + */ +data class ConfigSource( + /** + * Name of the high-level project. + */ + val projectName: String = "taler", + /** + * Name of the component within the package. + */ + val componentName: String = "taler", + /** + * Name of the binary that will be located on $PATH to + * find the installation path of the package. + */ + val installPathBinary: String = "taler-config", +) + +/** + * Reader and writer for Taler-style configuration files. + * + * The configuration file format is similar to INI files + * and fully described in the taler.conf man page. + * + * @param configSource information about where to load configuration defaults from + */ +class TalerConfig( + private val configSource: ConfigSource, +) { + private val sectionMap: MutableMap<String, Section> = mutableMapOf() + + private val componentName = configSource.componentName + private val projectName = configSource.projectName + private val installPathBinary = configSource.installPathBinary + val sections: Set<String> get() = sectionMap.keys + + private fun internalLoadFromString(s: String, sourceFilename: String?) { + val lines = s.lines() + var lineNum = 0 + var currentSection: String? = null + for (line in lines) { + lineNum++ + if (reEmptyLine.matches(line)) { + continue + } + if (reComment.matches(line)) { + continue + } + + val directiveMatch = reDirective.matchEntire(line) + if (directiveMatch != null) { + if (sourceFilename == null) { + throw TalerConfigError("Directives are only supported when loading from file") + } + val directiveName = directiveMatch.groups[1]!!.value.lowercase() + val directiveArg = directiveMatch.groups[2]!!.value + when (directiveName) { + "inline" -> { + val innerFilename = normalizeInlineFilename(sourceFilename, directiveArg.trim()) + this.loadFromFilename(innerFilename) + } + + "inline-matching" -> { + val glob = directiveArg.trim() + this.loadFromGlob(sourceFilename, glob) + } + + "inline-secret" -> { + val arg = directiveArg.trim() + val sp = arg.split(" ") + if (sp.size != 2) { + throw TalerConfigError("invalid configuration, @inline-secret@ directive requires exactly two arguments") + } + val sectionName = sp[0] + val secretFilename = normalizeInlineFilename(sourceFilename, sp[1]) + loadSecret(sectionName, secretFilename) + } + + else -> { + throw TalerConfigError("unsupported directive '$directiveName'") + } + } + continue + } + + val secMatch = reSection.matchEntire(line) + if (secMatch != null) { + currentSection = secMatch.groups[1]!!.value.uppercase() + continue + } + if (currentSection == null) { + throw TalerConfigError("section expected") + } + + val paramMatch = reParam.matchEntire(line) + + if (paramMatch != null) { + val optName = paramMatch.groups[1]!!.value.uppercase() + var optVal = paramMatch.groups[2]!!.value + if (optVal.startsWith('"') && optVal.endsWith('"')) { + optVal = optVal.substring(1, optVal.length - 1) + } + val section = provideSection(currentSection) + section.entries[optName] = optVal + continue + } + throw TalerConfigError("expected section header, option assignment or directive in line $lineNum file ${sourceFilename ?: "<input>"}") + } + } + + private fun loadFromGlob(parentFilename: String, glob: String) { + val fullFileglob: String + val parentDir = Path(parentFilename).parent!!.toString() + if (glob.startsWith("/")) { + fullFileglob = glob + } else { + fullFileglob = Paths.get(parentDir, glob).toString() + } + + val head = Path(fullFileglob).parent.toString() + val tail = Path(fullFileglob).fileName.toString() + + // FIXME: Check that the Kotlin glob matches the glob from our spec + for (entry in Path(head).listDirectoryEntries(tail)) { + loadFromFilename(entry.toString()) + } + } + + private fun normalizeInlineFilename(parentFilename: String, f: String): String { + if (f[0] == '/') { + return f + } + val parentDirPath = Path(parentFilename).toRealPath().parent + if (parentDirPath == null) { + throw TalerConfigError("unable to normalize inline path, cannot resolve parent directory of $parentFilename") + } + val parentDir = parentDirPath.toString() + return Paths.get(parentDir, f).toRealPath().toString() + } + + private fun loadSecret(sectionName: String, secretFilename: String) { + if (!Path(secretFilename).isReadable()) { + logger.warn("unable to read secrets from $secretFilename") + } else { + this.loadFromFilename(secretFilename) + } + } + + private fun provideSection(name: String): Section { + val canonSecName = name.uppercase() + val existingSec = this.sectionMap[canonSecName] + if (existingSec != null) { + return existingSec + } + val newSection = Section(entries = mutableMapOf()) + this.sectionMap[canonSecName] = newSection + return newSection + } + + fun loadFromString(s: String) { + internalLoadFromString(s, null) + } + + private fun setSystemDefault(section: String, option: String, value: String) { + // FIXME: The value should be marked as a system default for diagnostics pretty printing + val sec = provideSection(section) + sec.entries[option.uppercase()] = value + } + + fun putValueString(section: String, option: String, value: String) { + val sec = provideSection(section) + sec.entries[option.uppercase()] = value + } + + /** + * Create a string representation of the loaded configuration. + */ + fun stringify(): String { + val outStr = StringBuilder() + this.sectionMap.forEach { (sectionName, section) -> + var headerWritten = false + section.entries.forEach { (optionName, entry) -> + if (!headerWritten) { + outStr.appendLine("[$sectionName]") + headerWritten = true + } + outStr.appendLine("$optionName = $entry") + } + if (headerWritten) { + outStr.appendLine() + } + } + return outStr.toString() + } + + /** + * Read values into the configuration from the given entry point + * filename. Defaults are *not* loaded automatically. + */ + fun loadFromFilename(filename: String) { + val f = File(filename) + val contents = f.readText() + internalLoadFromString(contents, filename) + } + + private fun loadDefaultsFromDir(dirname: String) { + for (filePath in Path(dirname).listDirectoryEntries()) { + loadFromFilename(filePath.toString()) + } + } + + /** + * Load configuration defaults from the file system + * and populate the PATHS section based on the installation path. + */ + fun loadDefaults() { + val installDir = getInstallPath() + val baseConfigDir = Paths.get(installDir, "share/$projectName/config.d").toString() + setSystemDefault("PATHS", "PREFIX", "${installDir}/") + setSystemDefault("PATHS", "BINDIR", "${installDir}/bin/") + setSystemDefault("PATHS", "LIBEXECDIR", "${installDir}/$projectName/libexec/") + setSystemDefault("PATHS", "DOCDIR", "${installDir}/share/doc/$projectName/") + setSystemDefault("PATHS", "ICONDIR", "${installDir}/share/icons/") + setSystemDefault("PATHS", "LOCALEDIR", "${installDir}/share/locale/") + setSystemDefault("PATHS", "LIBDIR", "${installDir}/lib/$projectName/") + setSystemDefault("PATHS", "DATADIR", "${installDir}/share/$projectName/") + loadDefaultsFromDir(baseConfigDir) + } + + private fun variableLookup(x: String, recursionDepth: Int = 0): String? { + val pathRes = this.lookupString("PATHS", x) + if (pathRes != null) { + return pathsub(pathRes, recursionDepth + 1) + } + val envVal = System.getenv(x) + if (envVal != null) { + return envVal + } + return null + } + + /** + * Substitute ${...} and $... placeholders in a string + * with values from the PATHS section in the + * configuration and environment variables + * + * This substitution is typically only done for paths. + */ + fun pathsub(x: String, recursionDepth: Int = 0): String { + if (recursionDepth > 128) { + throw TalerConfigError("recursion limit in path substitution exceeded") + } + val result = StringBuilder() + var l = 0 + val s = x + while (l < s.length) { + if (s[l] != '$') { + // normal character + result.append(s[l]) + l++; + continue + } + if (l + 1 < s.length && s[l + 1] == '{') { + // ${var} + var depth = 1 + val start = l + var p = start + 2; + var hasDefault = false + var insideNamePath = true + // Find end of the ${...} expression + while (p < s.length) { + if (s[p] == '}') { + insideNamePath = false + depth-- + } else if (s.length > p + 1 && s[p] == '$' && s[p + 1] == '{') { + depth++ + insideNamePath = false + } else if (s.length > p + 1 && insideNamePath && s[p] == ':' && s[p + 1] == '-') { + hasDefault = true + } + p++ + if (depth == 0) { + break + } + } + if (depth == 0) { + val inner = s.substring(start + 2, p - 1) + val varName: String + val varDefault: String? + if (hasDefault) { + val res = inner.split(":-", limit = 2) + varName = res[0] + varDefault = res[1] + } else { + varName = inner + varDefault = null + } + val r = variableLookup(varName, recursionDepth + 1) + if (r != null) { + result.append(r) + l = p + continue + } else if (varDefault != null) { + val resolvedDefault = pathsub(varDefault, recursionDepth + 1) + result.append(resolvedDefault) + l = p + continue + } else { + throw TalerConfigError("malformed variable expression can't resolve variable '$varName'") + } + } + throw TalerConfigError("malformed variable expression (unbalanced)") + } else { + // $var + var varEnd = l + 1 + while (varEnd < s.length && (s[varEnd].isLetterOrDigit() || s[varEnd] == '_')) { + varEnd++ + } + val varName = s.substring(l + 1, varEnd) + val res = variableLookup(varName) + if (res != null) { + result.append(res) + } + l = varEnd + } + } + return result.toString() + } + + /** + * Load configuration values from the file system. + * If no entrypoint is specified, the default entrypoint + * is used. + */ + fun load(entrypoint: String? = null) { + loadDefaults() + if (entrypoint != null) { + loadFromFilename(entrypoint) + } else { + val defaultFilename = findDefaultConfigFilename() + if (defaultFilename != null) { + loadFromFilename(defaultFilename) + } + } + } + + /** + * Determine the filename of the default configuration file. + * + * If no such file can be found, return null. + */ + private fun findDefaultConfigFilename(): String? { + val xdg = System.getenv("XDG_CONFIG_HOME") + val home = System.getenv("HOME") + + var filename: String? = null + if (xdg != null) { + filename = Paths.get(xdg, "$componentName.conf").toString() + } else if (home != null) { + filename = Paths.get(home, ".config/$componentName.conf").toString() + } + if (filename != null && File(filename).exists()) { + return filename + } + val etc1 = "/etc/$componentName.conf" + if (File(etc1).exists()) { + return etc1 + } + val etc2 = "/etc/$projectName/$componentName.conf" + if (File(etc2).exists()) { + return etc2 + } + return null + } + + /** + * Guess the path that the component was installed to. + */ + fun getInstallPath(): String { + // We use the location of the libeufin-bank + // binary to determine the installation prefix. + // If for some weird reason it's now found, we + // fall back to "/usr" as install prefix. + return getInstallPathFromBinary(installPathBinary) + } + + private fun getInstallPathFromBinary(name: String): String { + val pathEnv = System.getenv("PATH") + val paths = pathEnv.split(":") + for (p in paths) { + val possiblePath = Paths.get(p, name).toString() + if (File(possiblePath).exists()) { + return Paths.get(p, "..").toRealPath().toString() + } + } + return "/usr" + } + + /* ----- Lookup ----- */ + + /** + * Look up a string value from the configuration. + * + * Return null if the value was not found in the configuration. + */ + fun lookupString(section: String, option: String): String? { + val canonSection = section.uppercase() + val canonOption = option.uppercase() + return this.sectionMap[canonSection]?.entries?.get(canonOption) + } + + fun requireString(section: String, option: String): String = + lookupString(section, option) ?: + throw TalerConfigError("expected string in configuration section $section option $option") + + fun requireNumber(section: String, option: String): Int = + lookupString(section, option)?.toInt() ?: + throw TalerConfigError("expected number in configuration section $section option $option") + + fun lookupBoolean(section: String, option: String): Boolean? { + val entry = lookupString(section, option) ?: return null + return when (val v = entry.lowercase()) { + "yes" -> true + "no" -> false + else -> throw TalerConfigError("expected yes/no in configuration section $section option $option but got $v") + } + } + + fun requireBoolean(section: String, option: String): Boolean = + lookupBoolean(section, option) ?: + throw TalerConfigError("expected boolean in configuration section $section option $option") + + fun lookupPath(section: String, option: String): String? { + val entry = lookupString(section, option) ?: return null + return pathsub(entry) + } + + fun requirePath(section: String, option: String): String = + lookupPath(section, option) ?: + throw TalerConfigError("expected path for section $section option $option") +} diff --git a/common/src/main/kotlin/TalerErrorCode.kt b/common/src/main/kotlin/TalerErrorCode.kt @@ -0,0 +1,4564 @@ +/* + This file is part of GNU Taler + Copyright (C) 2012-2020 Taler Systems SA + + GNU Taler is free software: you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + GNU Taler 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + + SPDX-License-Identifier: LGPL3.0-or-later + + Note: the LGPL does not apply to all components of GNU Taler, + but it does apply to this file. + */ +package tech.libeufin.common + +enum class TalerErrorCode(val code: Int) { + + + /** + * Special code to indicate success (no error). + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + NONE(0), + + + /** + * A non-integer error code was returned in the JSON response. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + INVALID(1), + + + /** + * An internal failure happened on the client side. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_CLIENT_INTERNAL_ERROR(2), + + + /** + * The response we got from the server was not even in JSON format. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_INVALID_RESPONSE(10), + + + /** + * An operation timed out. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_TIMEOUT(11), + + + /** + * The version string given does not follow the expected CURRENT:REVISION:AGE Format. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_VERSION_MALFORMED(12), + + + /** + * The service responded with a reply that was in JSON but did not satsify the protocol. Note that invalid cryptographic signatures should have signature-specific error codes. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_REPLY_MALFORMED(13), + + + /** + * There is an error in the client-side configuration, for example the base URL specified is malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_CONFIGURATION_INVALID(14), + + + /** + * The client made a request to a service, but received an error response it does not know how to handle. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_UNEXPECTED_REQUEST_ERROR(15), + + + /** + * The token used by the client to authorize the request does not grant the required permissions for the request. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_TOKEN_PERMISSION_INSUFFICIENT(16), + + + /** + * The HTTP method used is invalid for this endpoint. + * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_METHOD_INVALID(20), + + + /** + * There is no endpoint defined for the URL provided by the client. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_ENDPOINT_UNKNOWN(21), + + + /** + * The JSON in the client's request was malformed (generic parse error). + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_JSON_INVALID(22), + + + /** + * Some of the HTTP headers provided by the client caused the server to not be able to handle the request. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_HTTP_HEADERS_MALFORMED(23), + + + /** + * The payto:// URI provided by the client is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_PAYTO_URI_MALFORMED(24), + + + /** + * A required parameter in the request was missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_PARAMETER_MISSING(25), + + + /** + * A parameter in the request was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_PARAMETER_MALFORMED(26), + + + /** + * The reserve public key given as part of a /reserves/ endpoint was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_RESERVE_PUB_MALFORMED(27), + + + /** + * The body in the request could not be decompressed by the server. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_COMPRESSION_INVALID(28), + + + /** + * The currency involved in the operation is not acceptable for this backend. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_CURRENCY_MISMATCH(30), + + + /** + * The URI is longer than the longest URI the HTTP server is willing to parse. + * Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_URI_TOO_LONG(31), + + + /** + * The body is too large to be permissible for the endpoint. + * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_UPLOAD_EXCEEDS_LIMIT(32), + + + /** + * The service refused the request due to lack of proper authorization. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_UNAUTHORIZED(40), + + + /** + * The service refused the request as the given authorization token is unknown. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_TOKEN_UNKNOWN(41), + + + /** + * The service refused the request as the given authorization token expired. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_TOKEN_EXPIRED(42), + + + /** + * The service refused the request as the given authorization token is malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_TOKEN_MALFORMED(43), + + + /** + * The service refused the request due to lack of proper rights on the resource. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_FORBIDDEN(44), + + + /** + * The service failed initialize its connection to the database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_SETUP_FAILED(50), + + + /** + * The service encountered an error event to just start the database transaction. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_START_FAILED(51), + + + /** + * The service failed to store information in its database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_STORE_FAILED(52), + + + /** + * The service failed to fetch information from its database. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_FETCH_FAILED(53), + + + /** + * The service encountered an error event to commit the database transaction (hard, unrecoverable error). + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_COMMIT_FAILED(54), + + + /** + * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.) + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_SOFT_FAILURE(55), + + + /** + * The service's database is inconsistent and violates service-internal invariants. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_DB_INVARIANT_FAILURE(56), + + + /** + * The HTTP server experienced an internal invariant failure (bug). + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_INTERNAL_INVARIANT_FAILURE(60), + + + /** + * The service could not compute a cryptographic hash over some JSON value. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_FAILED_COMPUTE_JSON_HASH(61), + + + /** + * The service could not compute an amount. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_FAILED_COMPUTE_AMOUNT(62), + + + /** + * The HTTP server had insufficient memory to parse the request. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_PARSER_OUT_OF_MEMORY(70), + + + /** + * The HTTP server failed to allocate memory. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_ALLOCATION_FAILURE(71), + + + /** + * The HTTP server failed to allocate memory for building JSON reply. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_JSON_ALLOCATION_FAILURE(72), + + + /** + * The HTTP server failed to allocate memory for making a CURL request. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_CURL_ALLOCATION_FAILURE(73), + + + /** + * The backend could not locate a required template to generate an HTML reply. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_FAILED_TO_LOAD_TEMPLATE(74), + + + /** + * The backend could not expand the template to generate an HTML reply. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + GENERIC_FAILED_TO_EXPAND_TEMPLATE(75), + + + /** + * Exchange is badly configured and thus cannot operate. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_BAD_CONFIGURATION(1000), + + + /** + * Operation specified unknown for this endpoint. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_OPERATION_UNKNOWN(1001), + + + /** + * The number of segments included in the URI does not match the number of segments expected by the endpoint. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS(1002), + + + /** + * The same coin was already used with a different denomination previously. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY(1003), + + + /** + * The public key of given to a "/coins/" endpoint of the exchange was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB(1004), + + + /** + * The exchange is not aware of the denomination key the wallet requested for the operation. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN(1005), + + + /** + * The signature of the denomination key over the coin is not valid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DENOMINATION_SIGNATURE_INVALID(1006), + + + /** + * The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request. + * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_KEYS_MISSING(1007), + + + /** + * Validity period of the denomination lies in the future. + * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE(1008), + + + /** + * Denomination key of the coin is past its expiration time for the requested operation. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_DENOMINATION_EXPIRED(1009), + + + /** + * Denomination key of the coin has been revoked. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_DENOMINATION_REVOKED(1010), + + + /** + * An operation where the exchange interacted with a security module timed out. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_SECMOD_TIMEOUT(1011), + + + /** + * The respective coin did not have sufficient residual value for the operation. The "history" in this response provides the "residual_value" of the coin, which may be less than its "original_value". + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_INSUFFICIENT_FUNDS(1012), + + + /** + * The exchange had an internal error reconstructing the transaction history of the coin that was being processed. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED(1013), + + + /** + * The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS(1014), + + + /** + * The same coin was already used with a different age hash previously. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH(1015), + + + /** + * The requested operation is not valid for the cipher used by the selected denomination. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION(1016), + + + /** + * The provided arguments for the operation use inconsistent ciphers. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_CIPHER_MISMATCH(1017), + + + /** + * The number of denominations specified in the request exceeds the limit of the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE(1018), + + + /** + * The coin is not known to the exchange (yet). + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_COIN_UNKNOWN(1019), + + + /** + * The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_CLOCK_SKEW(1020), + + + /** + * The specified amount for the coin is higher than the value of the denomination of the coin. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE(1021), + + + /** + * The exchange was not properly configured with global fees. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_GLOBAL_FEES_MISSING(1022), + + + /** + * The exchange was not properly configured with wire fees. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_WIRE_FEES_MISSING(1023), + + + /** + * The purse public key was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_PURSE_PUB_MALFORMED(1024), + + + /** + * The purse is unknown. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_PURSE_UNKNOWN(1025), + + + /** + * The purse has expired. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_PURSE_EXPIRED(1026), + + + /** + * The exchange has no information about the "reserve_pub" that was given. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_RESERVE_UNKNOWN(1027), + + + /** + * The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check. + * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_KYC_REQUIRED(1028), + + + /** + * Inconsistency between provided age commitment and attest: either none or both must be provided + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT(1029), + + + /** + * The provided attestation for the minimum age couldn't be verified by the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE(1030), + + + /** + * The purse was deleted. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_PURSE_DELETED(1031), + + + /** + * The public key of the AML officer in the URL was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED(1032), + + + /** + * The signature affirming the GET request of the AML officer is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID(1033), + + + /** + * The specified AML officer does not have access at this time. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED(1034), + + + /** + * The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later. + * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AML_PENDING(1035), + + + /** + * The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator. + * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_AML_FROZEN(1036), + + + /** + * The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GENERIC_KYC_CONVERTER_FAILED(1037), + + + /** + * The exchange did not find information about the specified transaction in the database. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_NOT_FOUND(1100), + + + /** + * The wire hash of given to a "/deposits/" handler was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE(1101), + + + /** + * The merchant key of given to a "/deposits/" handler was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB(1102), + + + /** + * The hash of the contract terms given to a "/deposits/" handler was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS(1103), + + + /** + * The coin public key of given to a "/deposits/" handler was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB(1104), + + + /** + * The signature returned by the exchange in a /deposits/ request was malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE(1105), + + + /** + * The signature of the merchant is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID(1106), + + + /** + * The provided policy data was not accepted + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED(1107), + + + /** + * The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS(1150), + + + /** + * The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS(1151), + + + /** + * The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW(1152), + + + /** + * The exchange failed to create the signature using the denomination key. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_SIGNATURE_FAILED(1153), + + + /** + * The signature of the reserve is not valid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID(1154), + + + /** + * When computing the reserve history, we ended up with a negative overall balance, which should be impossible. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS(1155), + + + /** + * The reserve did not have sufficient funds in it to pay for a full reserve history statement. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE(1156), + + + /** + * Withdraw period of the coin to be withdrawn is in the past. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST(1158), + + + /** + * The client failed to unblind the blind signature. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_UNBLIND_FAILURE(1159), + + + /** + * The client re-used a withdraw nonce, which is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_NONCE_REUSE(1160), + + + /** + * The client provided an unknown commitment for an age-withdraw request. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN(1161), + + + /** + * The total sum of amounts from the denominations did overflow. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW(1162), + + + /** + * The total sum of value and fees from the denominations differs from the committed amount with fees. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT(1163), + + + /** + * The original commitment differs from the calculated hash + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH(1164), + + + /** + * The maximum age in the commitment is too large for the reserve + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE(1165), + + + /** + * The batch withdraw included a planchet that was already withdrawn. This is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET(1175), + + + /** + * The signature made by the coin over the deposit permission is not valid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID(1205), + + + /** + * The same coin was already deposited for the same merchant and contract with other details. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT(1206), + + + /** + * The stated value of the coin after the deposit fee is subtracted would be negative. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE(1207), + + + /** + * The stated refund deadline is after the wire deadline. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE(1208), + + + /** + * The stated wire deadline is "never", which makes no sense. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER(1209), + + + /** + * The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON(1210), + + + /** + * The hash of the given wire address does not match the wire hash specified in the proposal data. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT(1211), + + + /** + * The signature provided by the exchange is not valid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE(1221), + + + /** + * The deposited amount is smaller than the deposit fee, which would result in a negative contribution. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT(1222), + + + /** + * The proof of policy fulfillment was invalid. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT(1240), + + + /** + * The coin history was requested with a bad signature. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_COIN_HISTORY_BAD_SIGNATURE(1251), + + + /** + * The reserve history was requested with a bad signature. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE(1252), + + + /** + * The exchange encountered melt fees exceeding the melted coin's contribution. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION(1302), + + + /** + * The signature made with the coin to be melted is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MELT_COIN_SIGNATURE_INVALID(1303), + + + /** + * The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup). + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE(1305), + + + /** + * The signature returned by the exchange in a melt request was malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE(1306), + + + /** + * The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION(1353), + + + /** + * Failed to produce the blinded signatures over the coins to be returned. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR(1354), + + + /** + * The exchange is unaware of the refresh session specified in the request. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN(1355), + + + /** + * The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID(1356), + + + /** + * The number of envelopes given does not match the number of denomination keys given. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH(1358), + + + /** + * The exchange encountered a numeric overflow totaling up the cost for the refresh operation. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW(1359), + + + /** + * The exchange's cost calculation shows that the melt amount is below the costs of the transaction. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT(1360), + + + /** + * The signature made with the coin over the link data is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID(1361), + + + /** + * The refresh session hash given to a /refreshes/ handler was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_INVALID_RCH(1362), + + + /** + * Operation specified invalid for this endpoint. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID(1363), + + + /** + * The client provided age commitment data, but age restriction is not supported on this server. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED(1364), + + + /** + * The client provided invalid age commitment data: missing, not an array, or array of invalid size. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID(1365), + + + /** + * The coin specified in the link request is unknown to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_LINK_COIN_UNKNOWN(1400), + + + /** + * The public key of given to a /transfers/ handler was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_TRANSFERS_GET_WTID_MALFORMED(1450), + + + /** + * The exchange did not find information about the specified wire transfer identifier in the database. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND(1451), + + + /** + * The exchange did not find information about the wire transfer fees it charged. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND(1452), + + + /** + * The exchange found a wire fee that was above the total transfer value (and thus could not have been charged). + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT(1453), + + + /** + * The wait target of the URL was not in the set of expected values. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSES_INVALID_WAIT_TARGET(1475), + + + /** + * The signature on the purse status returned by the exchange was invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE(1476), + + + /** + * The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_COIN_NOT_FOUND(1500), + + + /** + * We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The "history" in the response proves this. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT(1501), + + + /** + * The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund). + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_DEPOSIT_NOT_FOUND(1502), + + + /** + * The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.) + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_MERCHANT_ALREADY_PAID(1503), + + + /** + * The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_FEE_TOO_LOW(1504), + + + /** + * The refunded amount is smaller than the refund fee, which would result in a negative refund. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_FEE_ABOVE_AMOUNT(1505), + + + /** + * The signature of the merchant is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID(1506), + + + /** + * Merchant backend failed to create the refund confirmation signature. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED(1507), + + + /** + * The signature returned by the exchange in a refund request was malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE(1508), + + + /** + * The failure proof returned by the exchange is incorrect. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE(1509), + + + /** + * Conflicting refund granted before with different amount but same refund transaction ID. + * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_REFUND_INCONSISTENT_AMOUNT(1510), + + + /** + * The given coin signature is invalid for the request. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_SIGNATURE_INVALID(1550), + + + /** + * The exchange could not find the corresponding withdraw operation. The request is denied. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND(1551), + + + /** + * The coin's remaining balance is zero. The request is denied. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_COIN_BALANCE_ZERO(1552), + + + /** + * The exchange failed to reproduce the coin's blinding. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_BLINDING_FAILED(1553), + + + /** + * The coin's remaining balance is zero. The request is denied. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE(1554), + + + /** + * The coin's denomination has not been revoked yet. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_NOT_ELIGIBLE(1555), + + + /** + * The given coin signature is invalid for the request. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID(1575), + + + /** + * The exchange could not find the corresponding melt operation. The request is denied. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND(1576), + + + /** + * The exchange failed to reproduce the coin's blinding. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED(1578), + + + /** + * The coin's denomination has not been revoked yet. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE(1580), + + + /** + * This exchange does not allow clients to request /keys for times other than the current (exchange) time. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN(1600), + + + /** + * A signature in the server's response was malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WIRE_SIGNATURE_INVALID(1650), + + + /** + * No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED(1651), + + + /** + * The payto:// URI stored in the exchange database for its bank account is malformed. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED(1652), + + + /** + * No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_WIRE_FEES_NOT_CONFIGURED(1653), + + + /** + * This purse was previously created with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA(1675), + + + /** + * This purse was previously merged with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA(1676), + + + /** + * The reserve has insufficient funds to create another purse. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS(1677), + + + /** + * The purse fee specified for the request is lower than the purse fee charged by the exchange at this time. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW(1678), + + + /** + * The payment request cannot be deleted anymore, as it either already completed or timed out. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DELETE_ALREADY_DECIDED(1679), + + + /** + * The signature affirming the purse deletion is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID(1680), + + + /** + * Withdrawal from the reserve requires age restriction to be set. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED(1681), + + + /** + * The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE(1700), + + + /** + * The response from the denomination key helper process was malformed. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DENOMINATION_HELPER_BUG(1701), + + + /** + * The helper refuses to sign with the key, because it is too early: the validity period has not yet started. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_DENOMINATION_HELPER_TOO_EARLY(1702), + + + /** + * The signature of the exchange on the reply was invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID(1725), + + + /** + * The exchange failed to talk to the process responsible for its private signing keys. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE(1750), + + + /** + * The response from the online signing key helper process was malformed. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_SIGNKEY_HELPER_BUG(1751), + + + /** + * The helper refuses to sign with the key, because it is too early: the validity period has not yet started. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_SIGNKEY_HELPER_TOO_EARLY(1752), + + + /** + * The purse expiration time is in the past at the time of its creation. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW(1775), + + + /** + * The purse expiration time is set to never, which is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER(1776), + + + /** + * The signature affirming the merge of the purse is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID(1777), + + + /** + * The signature by the reserve affirming the merge is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID(1778), + + + /** + * The signature by the reserve affirming the open operation is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE(1785), + + + /** + * The signature by the reserve affirming the close operation is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE(1786), + + + /** + * The signature by the reserve affirming the attestion request is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE(1787), + + + /** + * The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT(1788), + + + /** + * The reserve balance is insufficient to pay for the open operation. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS(1789), + + + /** + * The auditor that was supposed to be disabled is unknown to this exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND(1800), + + + /** + * The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected). + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT(1801), + + + /** + * The signature to add or enable the auditor does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID(1802), + + + /** + * The signature to disable the auditor does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID(1803), + + + /** + * The signature to revoke the denomination does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID(1804), + + + /** + * The signature to revoke the online signing key does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID(1805), + + + /** + * The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected). + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT(1806), + + + /** + * The signingkey specified is unknown to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN(1807), + + + /** + * The signature to publish wire account does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID(1808), + + + /** + * The signature to add the wire account does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID(1809), + + + /** + * The signature to disable the wire account does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID(1810), + + + /** + * The wire account to be disabled is unknown to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND(1811), + + + /** + * The signature to affirm wire fees does not validate. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID(1812), + + + /** + * The signature conflicts with a previous signature affirming different fees. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH(1813), + + + /** + * The signature affirming the denomination key is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID(1814), + + + /** + * The signature affirming the signing key is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID(1815), + + + /** + * The signature conflicts with a previous signature affirming different fees. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH(1816), + + + /** + * The signature affirming the fee structure is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID(1817), + + + /** + * The signature affirming the profit drain is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID(1818), + + + /** + * The signature affirming the AML decision is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID(1825), + + + /** + * The AML officer specified is not allowed to make AML decisions right now. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AML_DECISION_INVALID_OFFICER(1826), + + + /** + * There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT(1827), + + + /** + * There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AML_DECISION_UNKNOWN_CHECK(1828), + + + /** + * The signature affirming the change in the AML officer status is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID(1830), + + + /** + * A more recent decision about the AML officer status is known to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT(1831), + + + /** + * The purse was previously created with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA(1850), + + + /** + * The purse was previously created with a different contract. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED(1851), + + + /** + * A coin signature for a deposit into the purse is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID(1852), + + + /** + * The purse expiration time is in the past. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW(1853), + + + /** + * The purse expiration time is "never". + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER(1854), + + + /** + * The purse signature over the purse meta data is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID(1855), + + + /** + * The signature over the encrypted contract is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID(1856), + + + /** + * The signature from the exchange over the confirmation is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID(1857), + + + /** + * The coin was previously deposited with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA(1858), + + + /** + * The encrypted contract was previously uploaded with different meta data. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA(1859), + + + /** + * The deposited amount is less than the purse fee. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE(1860), + + + /** + * The signature using the merge key is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE(1876), + + + /** + * The signature using the reserve key is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE(1877), + + + /** + * The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_NOT_FULL(1878), + + + /** + * The signature from the exchange over the confirmation is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID(1879), + + + /** + * The exchange of the target account is not a partner of this exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN(1880), + + + /** + * The signature affirming the new partner is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID(1890), + + + /** + * Conflicting data for the partner already exists with the exchange. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT(1891), + + + /** + * The auditor signature over the denomination meta data is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID(1900), + + + /** + * The auditor that was specified is unknown to this exchange. + * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AUDITORS_AUDITOR_UNKNOWN(1901), + + + /** + * The auditor that was specified is no longer used by this exchange. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_AUDITORS_AUDITOR_INACTIVE(1902), + + + /** + * The signature affirming the wallet's KYC request was invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_WALLET_SIGNATURE_INVALID(1925), + + + /** + * The exchange received an unexpected malformed response from its KYC backend. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE(1926), + + + /** + * The backend signaled an unexpected failure. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_PROOF_BACKEND_ERROR(1927), + + + /** + * The backend signaled an authorization failure. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED(1928), + + + /** + * The exchange is unaware of having made an the authorization request. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN(1929), + + + /** + * The payto-URI hash did not match. Hence the request was denied. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED(1930), + + + /** + * The request used a logic specifier that is not known to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN(1931), + + + /** + * The request requires a logic which is no longer configured at the exchange. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_LOGIC_GONE(1932), + + + /** + * The logic plugin had a bug in its interaction with the KYC provider. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_LOGIC_BUG(1933), + + + /** + * The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator. + * Returned with an HTTP status code of #MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED (511). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED(1934), + + + /** + * There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed. + * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT(1935), + + + /** + * The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY(1936), + + + /** + * The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work. + * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED(1937), + + + /** + * The request to the webhook lacked proper authorization or authentication data. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED(1938), + + + /** + * The exchange does not know a contract under the given contract public key. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CONTRACTS_UNKNOWN(1950), + + + /** + * The URL does not encode a valid exchange public key in its path. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB(1951), + + + /** + * The returned encrypted contract did not decrypt. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CONTRACTS_DECRYPTION_FAILED(1952), + + + /** + * The signature on the encrypted contract did not validate. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CONTRACTS_SIGNATURE_INVALID(1953), + + + /** + * The decrypted contract was malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_CONTRACTS_DECODING_FAILED(1954), + + + /** + * A coin signature for a deposit into the purse is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID(1975), + + + /** + * It is too late to deposit coins into the purse. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY(1976), + + + /** + * TOTP key is not valid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + EXCHANGE_TOTP_KEY_INVALID(1980), + + + /** + * The backend could not find the merchant instance specified in the request. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_INSTANCE_UNKNOWN(2000), + + + /** + * The start and end-times in the wire fee structure leave a hole. This is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE(2001), + + + /** + * The merchant was unable to obtain a valid answer to /wire from the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED(2002), + + + /** + * The proposal is not known to the backend. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_ORDER_UNKNOWN(2005), + + + /** + * The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_PRODUCT_UNKNOWN(2006), + + + /** + * The reward ID is unknown. This could happen if the reward has expired. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_REWARD_ID_UNKNOWN(2007), + + + /** + * The contract obtained from the merchant backend was malformed. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID(2008), + + + /** + * The order we found does not match the provided contract hash. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER(2009), + + + /** + * The exchange failed to provide a valid response to the merchant's /keys request. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE(2010), + + + /** + * The exchange failed to respond to the merchant on time. + * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXCHANGE_TIMEOUT(2011), + + + /** + * The merchant failed to talk to the exchange. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE(2012), + + + /** + * The exchange returned a maformed response. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED(2013), + + + /** + * The exchange returned an unexpected response status. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS(2014), + + + /** + * The merchant refused the request due to lack of authorization. + * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_UNAUTHORIZED(2015), + + + /** + * The merchant instance specified in the request was deleted. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_INSTANCE_DELETED(2016), + + + /** + * The backend could not find the inbound wire transfer specified in the request. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_TRANSFER_UNKNOWN(2017), + + + /** + * The backend could not find the template(id) because it is not exist. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_TEMPLATE_UNKNOWN(2018), + + + /** + * The backend could not find the webhook(id) because it is not exist. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_WEBHOOK_UNKNOWN(2019), + + + /** + * The backend could not find the webhook(serial) because it is not exist. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN(2020), + + + /** + * The backend could not find the OTP device(id) because it is not exist. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN(2021), + + + /** + * The account is not known to the backend. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_ACCOUNT_UNKNOWN(2022), + + + /** + * The wire hash was malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_H_WIRE_MALFORMED(2023), + + + /** + * The currency specified in the operation does not work with the current state of the given resource. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GENERIC_CURRENCY_MISMATCH(2024), + + + /** + * The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response. + * Returned with an HTTP status code of #MHD_HTTP_OK (200). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE(2100), + + + /** + * The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE(2103), + + + /** + * The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE(2104), + + + /** + * The claim token used to authenticate the client is invalid for this order. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GET_ORDERS_ID_INVALID_TOKEN(2105), + + + /** + * The contract terms hash used to authenticate the client is invalid for this order. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH(2106), + + + /** + * The exchange responded saying that funds were insufficient (for example, due to double-spending). + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS(2150), + + + /** + * The denomination key used for payment is not listed among the denomination keys of the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND(2151), + + + /** + * The denomination key used for payment is not audited by an auditor approved by the merchant. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE(2152), + + + /** + * There was an integer overflow totaling up the amounts or deposit fees in the payment. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW(2153), + + + /** + * The deposit fees exceed the total value of the payment. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT(2154), + + + /** + * After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover. + * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES(2155), + + + /** + * Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. + * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT(2156), + + + /** + * The signature over the contract of one of the coins was invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID(2157), + + + /** + * When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED(2158), + + + /** + * The refund deadline in the contract is after the transfer deadline. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE(2159), + + + /** + * The order was already paid (maybe by another wallet). + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID(2160), + + + /** + * The payment is too late, the offer has expired. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED(2161), + + + /** + * The "merchant" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING(2162), + + + /** + * Failed to locate merchant's account information matching the wire hash given in the proposal. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN(2163), + + + /** + * The deposit time for the denomination has expired. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED(2165), + + + /** + * The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high). + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED(2166), + + + /** + * The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds. + * Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_REFUNDED(2167), + + + /** + * According to our database, we have refunded more than we were paid (which should not be possible). + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS(2168), + + + /** + * Legacy stuff. Remove me with protocol v1. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + DEAD_QQQ_PAY_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2169), + + + /** + * The payment failed at the exchange. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED(2170), + + + /** + * The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING(2171), + + + /** + * The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH(2172), + + + /** + * The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED(2173), + + + /** + * The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING(2174), + + + /** + * The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED(2175), + + + /** + * The contract hash does not match the given order ID. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH(2200), + + + /** + * The signature of the merchant is not valid for the given contract hash. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID(2201), + + + /** + * The merchant failed to send the exchange the refund request. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED(2251), + + + /** + * The merchant failed to find the exchange to process the lookup. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED(2252), + + + /** + * The merchant could not find the contract. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND(2253), + + + /** + * The payment was already completed and thus cannot be aborted anymore. + * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2254), + + + /** + * The hash provided by the wallet does not match the order. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH(2255), + + + /** + * The array of coins cannot be empty. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY(2256), + + + /** + * We could not claim the order because the backend is unaware of it. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND(2300), + + + /** + * We could not claim the order because someone else claimed it first. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED(2301), + + + /** + * The client-side experienced an internal failure. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE(2302), + + + /** + * The backend failed to sign the refund request. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED(2350), + + + /** + * The client failed to unblind the signature returned by the merchant. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_REWARD_PICKUP_UNBLIND_FAILURE(2400), + + + /** + * The exchange returned a failure code for the withdraw operation. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_REWARD_PICKUP_EXCHANGE_ERROR(2403), + + + /** + * The merchant failed to add up the amounts to compute the pick up value. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_REWARD_PICKUP_SUMMATION_FAILED(2404), + + + /** + * The reward expired. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_REWARD_PICKUP_HAS_EXPIRED(2405), + + + /** + * The requested withdraw amount exceeds the amount remaining to be picked up. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_REWARD_PICKUP_AMOUNT_EXCEEDS_REWARD_REMAINING(2406), + + + /** + * The merchant did not find the specified denomination key in the exchange's key set. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_REWARD_PICKUP_DENOMINATION_UNKNOWN(2407), + + + /** + * The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE(2500), + + + /** + * The proposal had no timestamp and the merchant backend failed to obtain the current local time. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME(2501), + + + /** + * The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR(2502), + + + /** + * A conflicting order (sharing the same order identifier) already exists at this merchant backend instance. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS(2503), + + + /** + * The order creation request is invalid because the given wire deadline is before the refund deadline. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE(2504), + + + /** + * The order creation request is invalid because the delivery date given is in the past. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST(2505), + + + /** + * The order creation request is invalid because a wire deadline of "never" is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER(2506), + + + /** + * The order ceration request is invalid because the given payment deadline is in the past. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST(2507), + + + /** + * The order creation request is invalid because the given refund deadline is in the past. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST(2508), + + + /** + * The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD(2509), + + + /** + * One of the paths to forget is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT(2510), + + + /** + * One of the paths to forget was not marked as forgettable. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE(2511), + + + /** + * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT(2520), + + + /** + * The order provided to the backend could not be deleted as the order was already paid. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID(2521), + + + /** + * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT(2530), + + + /** + * Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID(2531), + + + /** + * The refund delay was set to 0 and thus no refunds are ever allowed for this order. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT(2532), + + + /** + * The exchange says it does not know this transfer. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN(2550), + + + /** + * We internally failed to execute the /track/transfer request. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR(2551), + + + /** + * The amount transferred differs between what was submitted and what the exchange claimed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS(2552), + + + /** + * The exchange gave conflicting information about a coin which has been wire transferred. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS(2553), + + + /** + * The exchange charged a different wire fee than what it originally advertised, and it is higher. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE(2554), + + + /** + * We did not find the account that the transfer was made to. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND(2555), + + + /** + * The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED(2556), + + + /** + * The backend was previously informed about a wire transfer with the same ID but a different amount. Multiple wire transfers with the same ID are not allowed. If the new amount is correct, the old transfer should first be deleted. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION(2557), + + + /** + * We are waiting for the exchange to provide us with key material before checking the wire transfer. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS(2258), + + + /** + * We are waiting for the exchange to provide us with the list of aggregated transactions. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST(2259), + + + /** + * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange. + * Returned with an HTTP status code of #MHD_HTTP_OK (200). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE(2260), + + + /** + * The exchange indicated in the wire transfer claims to know nothing about the wire transfer. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND(2261), + + + /** + * The interaction with the exchange is delayed due to rate limiting. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED(2262), + + + /** + * We experienced a transient failure in our interaction with the exchange. + * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE(2263), + + + /** + * The response from the exchange was unacceptable and should be reviewed with an auditor. + * Returned with an HTTP status code of #MHD_HTTP_OK (200). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE(2264), + + + /** + * The amount transferred differs between what was submitted and what the exchange claimed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS(2563), + + + /** + * The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS(2600), + + + /** + * The merchant backend cannot create an instance because the authentication configuration field is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH(2601), + + + /** + * The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH(2602), + + + /** + * The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED(2603), + + + /** + * The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED(2625), + + + /** + * The bank account referenced in the requested operation was not found. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT(2626), + + + /** + * The bank account specified in the request already exists at the merchant. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_ACCOUNT_EXISTS(2627), + + + /** + * The product ID exists. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS(2650), + + + /** + * The update would have reduced the total amount of product lost, which is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED(2660), + + + /** + * The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS(2661), + + + /** + * The update would have reduced the total amount of product in stock, which is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED(2662), + + + /** + * The update would have reduced the total amount of product sold, which is not allowed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED(2663), + + + /** + * The lock request is for more products than we have left (unlocked) in stock. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS(2670), + + + /** + * The deletion request is for a product that is locked. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK(2680), + + + /** + * The requested wire method is not supported by the exchange. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD(2700), + + + /** + * The requested exchange does not allow rewards. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_RESERVES_REWARDS_NOT_ALLOWED(2701), + + + /** + * The reserve could not be deleted because it is unknown. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE(2710), + + + /** + * The reserve that was used to fund the rewards has expired. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED(2750), + + + /** + * The reserve that was used to fund the rewards was not found in the DB. + * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_UNKNOWN(2751), + + + /** + * The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS(2752), + + + /** + * The backend failed to find a reserve needed to authorize the reward. + * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND(2753), + + + /** + * The merchant backend encountered a failure in computing the deposit total. + * Returned with an HTTP status code of #MHD_HTTP_OK (200). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE(2800), + + + /** + * The template ID already exists. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS(2850), + + + /** + * The OTP device ID already exists. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS(2851), + + + /** + * Amount given in the using template and in the template contract. There is a conflict. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT(2860), + + + /** + * Subject given in the using template and in the template contract. There is a conflict. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT(2861), + + + /** + * Amount not given in the using template and in the template contract. There is a conflict. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT(2862), + + + /** + * Subject not given in the using template and in the template contract. There is a conflict. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY(2863), + + + /** + * The webhook ID elready exists. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS(2900), + + + /** + * The webhook serial elready exists. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + MERCHANT_PRIVATE_POST_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS(2910), + + + /** + * The signature from the exchange on the deposit confirmation is invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID(3100), + + + /** + * The exchange key used for the signature on the deposit confirmation was revoked. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED(3101), + + + /** + * Wire transfer attempted with credit and debit party being the same bank account. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_SAME_ACCOUNT(5101), + + + /** + * Wire transfer impossible, due to financial limitation of the party that attempted the payment. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_UNALLOWED_DEBIT(5102), + + + /** + * Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NEGATIVE_NUMBER_AMOUNT(5103), + + + /** + * A too big number was used (as value and/or fraction) to instantiate an amount object. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NUMBER_TOO_BIG(5104), + + + /** + * The bank account referenced in the requested operation was not found. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_UNKNOWN_ACCOUNT(5106), + + + /** + * The transaction referenced in the requested operation (typically a reject operation), was not found. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TRANSACTION_NOT_FOUND(5107), + + + /** + * Bank received a malformed amount string. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_BAD_FORMAT_AMOUNT(5108), + + + /** + * The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_REJECT_NO_RIGHTS(5109), + + + /** + * This error code is returned when no known exception types captured the exception. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_UNMANAGED_EXCEPTION(5110), + + + /** + * This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_SOFT_EXCEPTION(5111), + + + /** + * The request UID for a request to transfer funds has already been used, but with different details for the transfer. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TRANSFER_REQUEST_UID_REUSED(5112), + + + /** + * The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT(5113), + + + /** + * The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_DUPLICATE_RESERVE_PUB_SUBJECT(5114), + + + /** + * The client requested a transaction that is so far in the past, that it has been forgotten by the bank. + * Returned with an HTTP status code of #MHD_HTTP_GONE (410). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_ANCIENT_TRANSACTION_GONE(5115), + + + /** + * The client attempted to abort a transaction that was already confirmed. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_ABORT_CONFIRM_CONFLICT(5116), + + + /** + * The client attempted to confirm a transaction that was already aborted. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_CONFIRM_ABORT_CONFLICT(5117), + + + /** + * The client attempted to register an account with the same name. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_REGISTER_CONFLICT(5118), + + + /** + * The client attempted to confirm a withdrawal operation before the wallet posted the required details. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_POST_WITHDRAWAL_OPERATION_REQUIRED(5119), + + + /** + * The client tried to register a new account under a reserved username (like 'admin' for example). + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_RESERVED_USERNAME_CONFLICT(5120), + + + /** + * The client tried to register a new account with an username already in use. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_REGISTER_USERNAME_REUSE(5121), + + + /** + * The client tried to register a new account with a payto:// URI already in use. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_REGISTER_PAYTO_URI_REUSE(5122), + + + /** + * The client tried to delete an account with a non null balance. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_ACCOUNT_BALANCE_NOT_ZERO(5123), + + + /** + * The client tried to create a transaction or an operation that credit an unknown account. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_UNKNOWN_CREDITOR(5124), + + + /** + * The client tried to create a transaction or an operation that debit an unknown account. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_UNKNOWN_DEBTOR(5125), + + + /** + * The client tried to perform an action prohibited for exchange accounts. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_ACCOUNT_IS_EXCHANGE(5126), + + + /** + * The client tried to perform an action reserved for exchange accounts. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_ACCOUNT_IS_NOT_EXCHANGE(5127), + + + /** + * Received currency conversion is wrong. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_BAD_CONVERSION(5128), + + + /** + * The account referenced in this operation is missing tan info for the chosen channel. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_MISSING_TAN_INFO(5129), + + + /** + * The client attempted to confirm a transaction with incomplete info. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_CONFIRM_INCOMPLETE(5130), + + + /** + * The request rate is too high. The server is refusing requests to guard against brute-force attacks. + * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_RATE_LIMITED(5131), + + + /** + * This TAN channel is not supported. + * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_CHANNEL_NOT_SUPPORTED(5132), + + + /** + * Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_CHANNEL_SCRIPT_FAILED(5133), + + + /** + * The client's response to the challenge was invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_CHALLENGE_FAILED(5134), + + + /** + * A non-admin user has tried to change their legal name. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_LEGAL_NAME(5135), + + + /** + * A non-admin user has tried to change their debt limit. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_DEBT_LIMIT(5136), + + + /** + * A non-admin user has tried to change their password whihout providing the current one. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD(5137), + + + /** + * Provided old password does not match current password. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_PATCH_BAD_OLD_PASSWORD(5138), + + + /** + * An admin user has tried to become an exchange. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_PATCH_ADMIN_EXCHANGE(5139), + + + /** + * A non-admin user has tried to change their cashout account. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_CASHOUT(5140), + + + /** + * A non-admin user has tried to change their contact info. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_PATCH_CONTACT(5141), + + + /** + * The client tried to create a transaction that credit the admin account. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_ADMIN_CREDITOR(5142), + + + /** + * The referenced challenge was not found. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_CHALLENGE_NOT_FOUND(5143), + + + /** + * The referenced challenge has expired. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_TAN_CHALLENGE_EXPIRED(5144), + + + /** + * A non-admin user has tried to create an account with 2fa. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + BANK_NON_ADMIN_SET_TAN_CHANNEL(5145), + + + /** + * The sync service failed find the account in its database. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_ACCOUNT_UNKNOWN(6100), + + + /** + * The SHA-512 hash provided in the If-None-Match header is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_BAD_IF_NONE_MATCH(6101), + + + /** + * The SHA-512 hash provided in the If-Match header is malformed or missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_BAD_IF_MATCH(6102), + + + /** + * The signature provided in the "Sync-Signature" header is malformed or missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_BAD_SYNC_SIGNATURE(6103), + + + /** + * The signature provided in the "Sync-Signature" header does not match the account, old or new Etags. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_INVALID_SIGNATURE(6104), + + + /** + * The "Content-length" field for the upload is not a number. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_MALFORMED_CONTENT_LENGTH(6105), + + + /** + * The "Content-length" field for the upload is too big based on the server's terms of service. + * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_EXCESSIVE_CONTENT_LENGTH(6106), + + + /** + * The server is out of memory to handle the upload. Trying again later may succeed. + * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH(6107), + + + /** + * The uploaded data does not match the Etag. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_INVALID_UPLOAD(6108), + + + /** + * HTTP server experienced a timeout while awaiting promised payment. + * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_PAYMENT_GENERIC_TIMEOUT(6109), + + + /** + * Sync could not setup the payment request with its own backend. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_PAYMENT_CREATE_BACKEND_ERROR(6110), + + + /** + * The sync service failed find the backup to be updated in its database. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_PREVIOUS_BACKUP_UNKNOWN(6111), + + + /** + * The "Content-length" field for the upload is missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_MISSING_CONTENT_LENGTH(6112), + + + /** + * Sync had problems communicating with its payment backend. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_GENERIC_BACKEND_ERROR(6113), + + + /** + * Sync experienced a timeout communicating with its payment backend. + * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). + * (A value of 0 indicates that the error is generated client-side). + */ + SYNC_GENERIC_BACKEND_TIMEOUT(6114), + + + /** + * The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange. + * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE(7000), + + + /** + * The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_UNEXPECTED_EXCEPTION(7001), + + + /** + * The wallet received a response from a server, but the response can't be parsed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_RECEIVED_MALFORMED_RESPONSE(7002), + + + /** + * The wallet tried to make a network request, but it received no response. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_NETWORK_ERROR(7003), + + + /** + * The wallet tried to make a network request, but it was throttled. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_HTTP_REQUEST_THROTTLED(7004), + + + /** + * The wallet made a request to a service, but received an error response it does not know how to handle. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_UNEXPECTED_REQUEST_ERROR(7005), + + + /** + * The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT(7006), + + + /** + * The wallet does not support the operation requested by a client. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CORE_API_OPERATION_UNKNOWN(7007), + + + /** + * The given taler://pay URI is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_INVALID_TALER_PAY_URI(7008), + + + /** + * The signature on a coin by the exchange's denomination key is invalid after unblinding it. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_COIN_SIGNATURE_INVALID(7009), + + + /** + * The exchange does not know about the reserve (yet), and thus withdrawal can't progress. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE(7010), + + + /** + * The wallet core service is not available. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CORE_NOT_AVAILABLE(7011), + + + /** + * The bank has aborted a withdrawal operation, and thus a withdrawal can't complete. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK(7012), + + + /** + * An HTTP request made by the wallet timed out. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_HTTP_REQUEST_GENERIC_TIMEOUT(7013), + + + /** + * The order has already been claimed by another wallet. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_ORDER_ALREADY_CLAIMED(7014), + + + /** + * A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_WITHDRAWAL_GROUP_INCOMPLETE(7015), + + + /** + * The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_REWARD_COIN_SIGNATURE_INVALID(7016), + + + /** + * The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE(7017), + + + /** + * The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH(7018), + + + /** + * The merchant's signature on the contract terms is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CONTRACT_TERMS_SIGNATURE_INVALID(7019), + + + /** + * The contract terms given by the merchant are malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CONTRACT_TERMS_MALFORMED(7020), + + + /** + * A pending operation failed, and thus the request can't be completed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_PENDING_OPERATION_FAILED(7021), + + + /** + * A payment was attempted, but the merchant had an internal server error (5xx). + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_PAY_MERCHANT_SERVER_ERROR(7022), + + + /** + * The crypto worker failed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CRYPTO_WORKER_ERROR(7023), + + + /** + * The crypto worker received a bad request. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_CRYPTO_WORKER_BAD_REQUEST(7024), + + + /** + * A KYC step is required before withdrawal can proceed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_WITHDRAWAL_KYC_REQUIRED(7025), + + + /** + * The wallet does not have sufficient balance to create a deposit group. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE(7026), + + + /** + * The wallet does not have sufficient balance to create a peer push payment. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE(7027), + + + /** + * The wallet does not have sufficient balance to pay for an invoice. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE(7028), + + + /** + * A group of refresh operations has errors and will be tried again later. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_REFRESH_GROUP_INCOMPLETE(7029), + + + /** + * The exchange's self-reported base URL does not match the one that the wallet is using. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_EXCHANGE_BASE_URL_MISMATCH(7030), + + + /** + * The order has already been paid by another wallet. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + WALLET_ORDER_ALREADY_PAID(7031), + + + /** + * We encountered a timeout with our payment backend. + * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_BACKEND_TIMEOUT(8000), + + + /** + * The backend requested payment, but the request is malformed. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST(8001), + + + /** + * The backend got an unexpected reply from the payment processor. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_BACKEND_ERROR(8002), + + + /** + * The "Content-length" field for the upload is missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH(8003), + + + /** + * The "Content-length" field for the upload is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH(8004), + + + /** + * The backend failed to setup an order with the payment processor. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR(8005), + + + /** + * The backend was not authorized to check for payment with the payment processor. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED(8006), + + + /** + * The backend could not check payment status with the payment processor. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED(8007), + + + /** + * The Anastasis provider could not be reached. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_GENERIC_PROVIDER_UNREACHABLE(8008), + + + /** + * HTTP server experienced a timeout while awaiting promised payment. + * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_PAYMENT_GENERIC_TIMEOUT(8009), + + + /** + * The key share is unknown to the provider. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_UNKNOWN(8108), + + + /** + * The authorization method used for the key share is no longer supported by the provider. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED(8109), + + + /** + * The client needs to respond to the challenge. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED(8110), + + + /** + * The client's response to the challenge was invalid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_CHALLENGE_FAILED(8111), + + + /** + * The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_CHALLENGE_UNKNOWN(8112), + + + /** + * The backend failed to initiate the authorization process. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED(8114), + + + /** + * The authorization succeeded, but the key share is no longer available. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_KEY_SHARE_GONE(8115), + + + /** + * The backend forgot the order we asked the client to pay for + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_ORDER_DISAPPEARED(8116), + + + /** + * The backend itself reported a bad exchange interaction. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD(8117), + + + /** + * The backend reported a payment status we did not expect. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS(8118), + + + /** + * The backend failed to setup the order for payment. + * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR(8119), + + + /** + * The decryption of the key share failed with the provided key. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_DECRYPTION_FAILED(8120), + + + /** + * The request rate is too high. The server is refusing requests to guard against brute-force attacks. + * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_RATE_LIMITED(8121), + + + /** + * A request to issue a challenge is not valid for this authentication method. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD(8123), + + + /** + * The backend failed to store the key share because the UUID is already in use. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS(8150), + + + /** + * The backend failed to store the key share because the authorization method is not supported. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED(8151), + + + /** + * The provided phone number is not an acceptable number. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_SMS_PHONE_INVALID(8200), + + + /** + * Failed to run the SMS transmission helper process. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_SMS_HELPER_EXEC_FAILED(8201), + + + /** + * Provider failed to send SMS. Helper terminated with a non-successful result. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_SMS_HELPER_COMMAND_FAILED(8202), + + + /** + * The provided email address is not an acceptable address. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_EMAIL_INVALID(8210), + + + /** + * Failed to run the E-mail transmission helper process. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_EMAIL_HELPER_EXEC_FAILED(8211), + + + /** + * Provider failed to send E-mail. Helper terminated with a non-successful result. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_EMAIL_HELPER_COMMAND_FAILED(8212), + + + /** + * The provided postal address is not an acceptable address. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POST_INVALID(8220), + + + /** + * Failed to run the mail transmission helper process. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POST_HELPER_EXEC_FAILED(8221), + + + /** + * Provider failed to send mail. Helper terminated with a non-successful result. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POST_HELPER_COMMAND_FAILED(8222), + + + /** + * The provided IBAN address is not an acceptable IBAN. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_IBAN_INVALID(8230), + + + /** + * The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_IBAN_MISSING_TRANSFER(8231), + + + /** + * The backend did not find a TOTP key in the data provided. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TOTP_KEY_MISSING(8240), + + + /** + * The key provided does not satisfy the format restrictions for an Anastasis TOTP key. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_TOTP_KEY_INVALID(8241), + + + /** + * The given if-none-match header is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POLICY_BAD_IF_NONE_MATCH(8301), + + + /** + * The server is out of memory to handle the upload. Trying again later may succeed. + * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH(8304), + + + /** + * The signature provided in the "Anastasis-Policy-Signature" header is malformed or missing. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POLICY_BAD_SIGNATURE(8305), + + + /** + * The given if-match header is malformed. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POLICY_BAD_IF_MATCH(8306), + + + /** + * The uploaded data does not match the Etag. + * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POLICY_INVALID_UPLOAD(8307), + + + /** + * The provider is unaware of the requested policy. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_POLICY_NOT_FOUND(8350), + + + /** + * The given action is invalid for the current state of the reducer. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_ACTION_INVALID(8400), + + + /** + * The given state of the reducer is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_STATE_INVALID(8401), + + + /** + * The given input to the reducer is invalid. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_INPUT_INVALID(8402), + + + /** + * The selected authentication method does not work for the Anastasis provider. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED(8403), + + + /** + * The given input and action do not work for the current state. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE(8404), + + + /** + * We experienced an unexpected failure interacting with the backend. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_BACKEND_FAILURE(8405), + + + /** + * The contents of a resource file did not match our expectations. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_RESOURCE_MALFORMED(8406), + + + /** + * A required resource file is missing. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_RESOURCE_MISSING(8407), + + + /** + * An input did not match the regular expression. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_INPUT_REGEX_FAILED(8408), + + + /** + * An input did not match the custom validation logic. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED(8409), + + + /** + * Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED(8410), + + + /** + * Anastasis provider reported a fatal failure. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED(8411), + + + /** + * Anastasis provider failed to respond to the configuration request. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED(8412), + + + /** + * The policy we downloaded is malformed. Must have been a client error while creating the backup. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_POLICY_MALFORMED(8413), + + + /** + * We failed to obtain the policy, likely due to a network issue. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_NETWORK_FAILED(8414), + + + /** + * The recovered secret did not match the required syntax. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_SECRET_MALFORMED(8415), + + + /** + * The challenge data provided is too large for the available providers. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG(8416), + + + /** + * The provided core secret is too large for some of the providers. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_SECRET_TOO_BIG(8417), + + + /** + * The provider returned in invalid configuration. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG(8418), + + + /** + * The reducer encountered an internal error, likely a bug that needs to be reported. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_INTERNAL_ERROR(8419), + + + /** + * The reducer already synchronized with all providers. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED(8420), + + + /** + * A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + LIBEUFIN_NEXUS_GENERIC_ERROR(9000), + + + /** + * An uncaught exception happened in the LibEuFin nexus service. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION(9001), + + + /** + * A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + LIBEUFIN_SANDBOX_GENERIC_ERROR(9500), + + + /** + * An uncaught exception happened in the LibEuFin sandbox service. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION(9501), + + + /** + * This validation method is not supported by the service. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + TALDIR_METHOD_NOT_SUPPORTED(9600), + + + /** + * Number of allowed attempts for initiating a challenge exceeded. + * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). + * (A value of 0 indicates that the error is generated client-side). + */ + TALDIR_REGISTER_RATE_LIMITED(9601), + + + /** + * The client is unknown or unauthorized. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_GENERIC_CLIENT_UNKNOWN(9750), + + + /** + * The client is not authorized to use the given redirect URI. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI(9751), + + + /** + * The service failed to execute its helper process to send the challenge. + * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_HELPER_EXEC_FAILED(9752), + + + /** + * The grant is unknown to the service (it could also have expired). + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_GRANT_UNKNOWN(9753), + + + /** + * The code given is not even well-formed. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE(9754), + + + /** + * The service is not aware of the referenced validation process. + * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_GENERIC_VALIDATION_UNKNOWN(9755), + + + /** + * The code given is not valid. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_CLIENT_FORBIDDEN_INVALID_CODE(9756), + + + /** + * Too many attempts have been made, validation is temporarily disabled for this address. + * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_TOO_MANY_ATTEMPTS(9757), + + + /** + * The PIN code provided is incorrect. + * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_INVALID_PIN(9758), + + + /** + * The token cannot be valid as no address was ever provided by the client. + * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). + * (A value of 0 indicates that the error is generated client-side). + */ + CHALLENGER_MISSING_ADDRESS(9759), + + + /** + * End of error code range. + * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). + * (A value of 0 indicates that the error is generated client-side). + */ + END(9999), + + +} diff --git a/common/src/main/kotlin/iban.kt b/common/src/main/kotlin/iban.kt @@ -0,0 +1,46 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +fun getIban(): String { + val ccNoCheck = "131400" // DE00 + val bban = (0..10).map { + (0..9).random() + }.joinToString("") // 10 digits account number + var checkDigits: String = "98".toBigInteger().minus("$bban$ccNoCheck".toBigInteger().mod("97".toBigInteger())).toString() + if (checkDigits.length == 1) { + checkDigits = "0${checkDigits}" + } + return "DE$checkDigits$bban" +} + +// Taken from the ISO20022 XSD schema +private val bicRegex = Regex("^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$") + +fun validateBic(bic: String): Boolean { + return bicRegex.matches(bic) +} + +// Taken from the ISO20022 XSD schema +private val ibanRegex = Regex("^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$") + +fun validateIban(iban: String): Boolean { + return ibanRegex.matches(iban) +} +\ No newline at end of file diff --git a/common/src/main/kotlin/strings.kt b/common/src/main/kotlin/strings.kt @@ -0,0 +1,89 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import java.math.BigInteger +import java.util.* + +fun ByteArray.toHexString(): String { + return this.joinToString("") { + java.lang.String.format("%02X", it) + } +} + +private fun toDigit(hexChar: Char): Int { + val digit = Character.digit(hexChar, 16) + require(digit != -1) { "Invalid Hexadecimal Character: $hexChar" } + return digit +} + +private fun hexToByte(hexString: String): Byte { + val firstDigit: Int = toDigit(hexString[0]) + val secondDigit: Int = toDigit(hexString[1]) + return ((firstDigit shl 4) + secondDigit).toByte() +} + +fun decodeHexString(hexString: String): ByteArray { + val hs = hexString.replace(" ", "").replace("\n", "") + require(hs.length % 2 != 1) { "Invalid hexadecimal String supplied." } + val bytes = ByteArray(hs.length / 2) + var i = 0 + while (i < hs.length) { + bytes[i / 2] = hexToByte(hs.substring(i, i + 2)) + i += 2 + } + return bytes +} + +fun bytesToBase64(bytes: ByteArray): String { + return Base64.getEncoder().encodeToString(bytes) +} + +fun base64ToBytes(encoding: String): ByteArray { + return Base64.getDecoder().decode(encoding) +} + +// used mostly in RSA math, never as amount. +fun BigInteger.toUnsignedHexString(): String { + val signedValue = this.toByteArray() + require(this.signum() > 0) { "number must be positive" } + val start = if (signedValue[0] == 0.toByte()) { + 1 + } else { + 0 + } + val bytes = Arrays.copyOfRange(signedValue, start, signedValue.size) + return bytes.toHexString() +} + +fun getQueryParam(uriQueryString: String, param: String): String? { + uriQueryString.split('&').forEach { + val kv = it.split('=') + if (kv[0] == param) + return kv[1] + } + return null +} + +fun String.splitOnce(pat: String): Pair<String, String>? { + val split = split(pat, limit=2); + if (split.size != 2) return null + return Pair(split[0], split[1]) +} +\ No newline at end of file diff --git a/common/src/main/kotlin/time.kt b/common/src/main/kotlin/time.kt @@ -0,0 +1,138 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.common + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.* +import java.time.format.DateTimeFormatter +import java.time.temporal.ChronoUnit + +private val logger: Logger = LoggerFactory.getLogger("libeufin-common") + +/** + * Converts the 'this' Instant to the number of nanoseconds + * since the Epoch. It returns the result as Long, or null + * if one arithmetic overflow occurred. + */ +private fun Instant.toNanos(): Long? { + val oneSecNanos = ChronoUnit.SECONDS.duration.toNanos() + val nanoBase: Long = this.epochSecond * oneSecNanos + if (nanoBase != 0L && nanoBase / this.epochSecond != oneSecNanos) { + logger.error("Multiplication overflow: could not convert Instant to nanos.") + return null + } + val res = nanoBase + this.nano + if (res < nanoBase) { + logger.error("Addition overflow: could not convert Instant to nanos.") + return null + } + return res +} + +/** + * This function converts an Instant input to the + * number of microseconds since the Epoch, except that + * it yields Long.MAX if the Input is Instant.MAX. + * + * Takes the name after the way timestamps are designed + * in the database: micros since Epoch, or Long.MAX for + * "never". + * + * Returns the Long representation of 'this' or null + * if that would overflow. + */ +fun Instant.toDbMicros(): Long? { + if (this == Instant.MAX) + return Long.MAX_VALUE + val nanos = this.toNanos() ?: run { + logger.error("Could not obtain micros to store to database, convenience conversion to nanos overflew.") + return null + } + return nanos / 1000L +} + +/** + * This helper is typically used to convert a timestamp expressed + * in microseconds from the DB back to the Web application. In case + * of _any_ error, it logs it and returns null. + */ +fun Long.microsToJavaInstant(): Instant? { + if (this == Long.MAX_VALUE) + return Instant.MAX + return try { + Instant.EPOCH.plus(this, ChronoUnit.MICROS) + } catch (e: Exception) { + logger.error(e.message) + return null + } +} + +/** + * Parses timestamps found in camt.054 documents. They have + * the following format: yyy-MM-ddThh:mm:ss, without any timezone. + * + * @param timeFromXml input time string from the XML + * @return [Instant] in the UTC timezone + */ +fun parseCamtTime(timeFromCamt: String): Instant { + val t = LocalDateTime.parse(timeFromCamt) + val utc = ZoneId.of("UTC") + return t.toInstant(utc.rules.getOffset(t)) +} + +/** + * Parses a date string as found in the booking date of + * camt.054 reports. They have this format: yyyy-MM-dd. + * + * @param bookDate input to parse + * @return [Instant] to the UTC. + */ +fun parseBookDate(bookDate: String): Instant { + val l = LocalDate.parse(bookDate) + return Instant.from(l.atStartOfDay(ZoneId.of("UTC"))) +} + +/** + * Returns the minimum instant between two. + * + * @param a input [Instant] + * @param b input [Instant] + * @return the minimum [Instant] or null if even one is null. + */ +fun minTimestamp(a: Instant?, b: Instant?): Instant? { + if (a == null || b == null) return null + if (a.isBefore(b)) return a + return b // includes the case where a == b. +} + +/** + * Returns the max instant between two. + * + * @param a input [Instant] + * @param b input [Instant] + * @return the max [Instant] or null if both are null + */ +fun maxTimestamp(a: Instant?, b: Instant?): Instant? { + if (a == null) return b + if (b == null) return a + if (a.isAfter(b)) return a + return b // includes the case where a == b +} +\ No newline at end of file diff --git a/util/src/main/resources/xsd/camt.052.001.02.xsd b/common/src/main/resources/xsd/camt.052.001.02.xsd diff --git a/util/src/main/resources/xsd/camt.053.001.02.xsd b/common/src/main/resources/xsd/camt.053.001.02.xsd diff --git a/util/src/main/resources/xsd/camt.054.001.02.xsd b/common/src/main/resources/xsd/camt.054.001.02.xsd diff --git a/util/src/main/resources/xsd/ebics_H004.xsd b/common/src/main/resources/xsd/ebics_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_H005.xsd b/common/src/main/resources/xsd/ebics_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_hev.xsd b/common/src/main/resources/xsd/ebics_hev.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd b/common/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd b/common/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd b/common/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd b/common/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_orders_H004.xsd b/common/src/main/resources/xsd/ebics_orders_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_orders_H005.xsd b/common/src/main/resources/xsd/ebics_orders_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_request_H004.xsd b/common/src/main/resources/xsd/ebics_request_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_request_H005.xsd b/common/src/main/resources/xsd/ebics_request_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_response_H004.xsd b/common/src/main/resources/xsd/ebics_response_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_response_H005.xsd b/common/src/main/resources/xsd/ebics_response_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_signature_S002.xsd b/common/src/main/resources/xsd/ebics_signature_S002.xsd diff --git a/util/src/main/resources/xsd/ebics_signatures.xsd b/common/src/main/resources/xsd/ebics_signatures.xsd diff --git a/util/src/main/resources/xsd/ebics_types_H004.xsd b/common/src/main/resources/xsd/ebics_types_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_types_H005.xsd b/common/src/main/resources/xsd/ebics_types_H005.xsd diff --git a/util/src/main/resources/xsd/pain.001.001.03.ch.02.xsd b/common/src/main/resources/xsd/pain.001.001.03.ch.02.xsd diff --git a/util/src/main/resources/xsd/pain.001.001.03.xsd b/common/src/main/resources/xsd/pain.001.001.03.xsd diff --git a/util/src/main/resources/xsd/pain.001.001.09.ch.03.xsd b/common/src/main/resources/xsd/pain.001.001.09.ch.03.xsd diff --git a/util/src/main/resources/xsd/pain.002.001.13.xsd b/common/src/main/resources/xsd/pain.002.001.13.xsd diff --git a/util/src/main/resources/xsd/xmldsig-core-schema.xsd b/common/src/main/resources/xsd/xmldsig-core-schema.xsd diff --git a/common/src/test/kotlin/AmountTest.kt b/common/src/test/kotlin/AmountTest.kt @@ -0,0 +1,62 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import java.time.Instant +import java.util.* +import kotlin.test.* +import org.junit.Test +import tech.libeufin.common.* + +class AmountTest { + @Test + fun parse() { + assertEquals(TalerAmount("EUR:4"), TalerAmount(4L, 0, "EUR")) + assertEquals(TalerAmount("EUR:0.02"), TalerAmount(0L, 2000000, "EUR")) + assertEquals(TalerAmount("EUR:4.12"), TalerAmount(4L, 12000000, "EUR")) + assertEquals(TalerAmount("LOCAL:4444.1000"), TalerAmount(4444L, 10000000, "LOCAL")) + assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999"), TalerAmount(TalerAmount.MAX_VALUE, 99999999, "EUR")) + + assertException("Invalid amount format") {TalerAmount("")} + assertException("Invalid amount format") {TalerAmount("EUR")} + assertException("Invalid amount format") {TalerAmount("eur:12")} + assertException("Invalid amount format") {TalerAmount(" EUR:12")} + assertException("Invalid amount format") {TalerAmount("EUR:1.")} + assertException("Invalid amount format") {TalerAmount("EUR:.1")} + assertException("Invalid amount format") {TalerAmount("AZERTYUIOPQSD:12")} + assertException("Value specified in amount is too large") {TalerAmount("EUR:${Long.MAX_VALUE}")} + assertException("Invalid amount format") {TalerAmount("EUR:4.000000000")} + assertException("Invalid amount format") {TalerAmount("EUR:4.4a")} + } + + @Test + fun parseRoundTrip() { + for (amount in listOf("EUR:4", "EUR:0.02", "EUR:4.12")) { + assertEquals(amount, TalerAmount(amount).toString()) + } + } + + fun assertException(msg: String, lambda: () -> Unit) { + try { + lambda() + throw Exception("Expected failure") + } catch (e: Exception) { + assert(e.message!!.startsWith(msg)) { "${e.message}" } + } + } +} +\ No newline at end of file diff --git a/common/src/test/kotlin/CryptoUtilTest.kt b/common/src/test/kotlin/CryptoUtilTest.kt @@ -0,0 +1,216 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.junit.Ignore +import org.junit.Test +import tech.libeufin.common.* +import java.io.File +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPrivateCrtKey +import java.util.* +import javax.crypto.EncryptedPrivateKeyInfo +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CryptoUtilTest { + + @Test + fun loadFromModulusAndExponent() { + val keyPair = CryptoUtil.generateRsaKeyPair(1024) + val pub2 = CryptoUtil.loadRsaPublicKeyFromComponents( + keyPair.public.modulus.toByteArray(), + keyPair.public.publicExponent.toByteArray() + ) + assertEquals(keyPair.public, pub2) + } + + @Test + fun keyGeneration() { + val gen: KeyPairGenerator = KeyPairGenerator.getInstance("RSA") + gen.initialize(2048) + val pair = gen.genKeyPair() + println(pair.private) + assertTrue(pair.private is RSAPrivateCrtKey) + } + + @Test + fun testCryptoUtilBasics() { + val keyPair = CryptoUtil.generateRsaKeyPair(1024) + val encodedPriv = keyPair.private.encoded + val encodedPub = keyPair.public.encoded + val otherKeyPair = + CryptoUtil.RsaCrtKeyPair(CryptoUtil.loadRsaPrivateKey(encodedPriv), CryptoUtil.loadRsaPublicKey(encodedPub)) + assertEquals(keyPair.private, otherKeyPair.private) + assertEquals(keyPair.public, otherKeyPair.public) + } + + @Test + fun testEbicsE002() { + val data = "Hello, World!".toByteArray() + val keyPair = CryptoUtil.generateRsaKeyPair(1024) + val enc = CryptoUtil.encryptEbicsE002(data, keyPair.public) + val dec = CryptoUtil.decryptEbicsE002(enc, keyPair.private) + assertTrue(data.contentEquals(dec)) + } + + @Test + fun testEbicsA006() { + val keyPair = CryptoUtil.generateRsaKeyPair(1024) + val data = "Hello, World".toByteArray(Charsets.UTF_8) + val sig = CryptoUtil.signEbicsA006(data, keyPair.private) + assertTrue(CryptoUtil.verifyEbicsA006(sig, data, keyPair.public)) + } + + @Test + fun testPassphraseEncryption() { + + val keyPair = CryptoUtil.generateRsaKeyPair(1024) + + /* encrypt with original key */ + val data = "Hello, World!".toByteArray(Charsets.UTF_8) + val secret = CryptoUtil.encryptEbicsE002(data, keyPair.public) + + /* encrypt and decrypt private key */ + val encPriv = CryptoUtil.encryptKey(keyPair.private.encoded, "secret") + val plainPriv = CryptoUtil.decryptKey(EncryptedPrivateKeyInfo(encPriv), "secret") + + /* decrypt with decrypted private key */ + val revealed = CryptoUtil.decryptEbicsE002(secret, plainPriv) + + assertEquals( + String(revealed, charset = Charsets.UTF_8), + String(data, charset = Charsets.UTF_8) + ) + } + + @Test + fun testEbicsPublicKeyHashing() { + val exponentStr = "01 00 01" + val moduloStr = """ + EB BD B8 E3 73 45 60 06 44 A1 AD 6A 25 33 65 F5 + 9C EB E5 93 E0 51 72 77 90 6B F0 58 A8 89 EB 00 + C6 0B 37 38 F3 3C 55 F2 4D 83 D0 33 C3 A8 F0 3C + 82 4E AF 78 51 D6 F4 71 6A CC 9C 10 2A 58 C9 5F + 3D 30 B4 31 D7 1B 79 6D 43 AA F9 75 B5 7E 0B 4A + 55 52 1D 7C AC 8F 92 B0 AE 9F CF 5F 16 5C 6A D1 + 88 DB E2 48 E7 78 43 F9 18 63 29 45 ED 6C 08 6C + 16 1C DE F3 02 01 23 8A 58 35 43 2B 2E C5 3F 6F + 33 B7 A3 46 E1 75 BD 98 7C 6D 55 DE 71 11 56 3D + 7A 2C 85 42 98 42 DF 94 BF E8 8B 76 84 13 3E CA + 0E 8D 12 57 D6 8A CF 82 DE B7 D7 BB BC 45 AE 25 + 95 76 00 19 08 AA D2 C8 A7 D8 10 37 88 96 B9 98 + 14 B4 B0 65 F3 36 CE 93 F7 46 12 58 9F E7 79 33 + D5 BE 0D 0E F8 E7 E0 A9 C3 10 51 A1 3E A4 4F 67 + 5E 75 8C 9D E6 FE 27 B6 3C CF 61 9B 31 D4 D0 22 + B9 2E 4C AF 5F D6 4B 1F F0 4D 06 5F 68 EB 0B 71 + """.trimIndent() + val expectedHashStr = """ + 72 71 D5 83 B4 24 A6 DA 0B 7B 22 24 3B E2 B8 8C + 6E A6 0F 9F 76 11 FD 18 BE 2C E8 8B 21 03 A9 41 + """.trimIndent() + + val expectedHash = expectedHashStr.replace(" ", "").replace("\n", "").toByteArray(Charsets.UTF_8) + + val pub = CryptoUtil.loadRsaPublicKeyFromComponents(decodeHexString(moduloStr), decodeHexString(exponentStr)) + + println("echoed pub exp: ${pub.publicExponent.toUnsignedHexString()}") + println("echoed pub mod: ${pub.modulus.toUnsignedHexString()}") + + val pubHash = CryptoUtil.getEbicsPublicKeyHash(pub) + + println("our pubHash: ${pubHash.toHexString()}") + println("expected pubHash: ${expectedHash.toString(Charsets.UTF_8)}") + + assertEquals(expectedHash.toString(Charsets.UTF_8), pubHash.toHexString()) + } + + @Test + fun checkEddsaPublicKey() { + val givenEnc = "XZH3P6NF9DSG3BH0C082X38N2RVK1RV2H24KF76028QBKDM24BCG" + val non32bytes = "N2RVK1RV2H24KF76028QBKDM24BCG" + assertTrue(CryptoUtil.checkValidEddsaPublicKey(givenEnc)) + assertFalse(CryptoUtil.checkValidEddsaPublicKey(non32bytes)) + } + + @Test + fun base32Test() { + val validKey = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" + val enc = validKey + val obj = Base32Crockford.decode(enc) + assertTrue(obj.size == 32) + val roundTrip = Base32Crockford.encode(obj) + assertEquals(enc, roundTrip) + val invalidShorterKey = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCE" + val shorterBlob = Base32Crockford.decode(invalidShorterKey) + assertTrue(shorterBlob.size < 32) // See #7980 + } + + @Test + fun blobRoundTrip() { + val blob = ByteArray(30) + Random().nextBytes(blob) + val enc = Base32Crockford.encode(blob) + val blobAgain = Base32Crockford.decode(enc) + assertTrue(blob.contentEquals(blobAgain)) + } + + /** + * Manual test: tests that gnunet-base32 and + * libeufin encode to the same string. + */ + @Ignore + fun gnunetEncodeCheck() { + val blob = ByteArray(30) + Random().nextBytes(blob) + val b = File("/tmp/libeufin-blob.bin") + b.writeBytes(blob) + val enc = Base32Crockford.encode(blob) + // The following output needs to match the one from + // "gnunet-base32 /tmp/libeufin-blob.bin" + println(enc) + } + + /** + * Manual test: tests that gnunet-base32 and + * libeufin decode to the same value + */ + @Ignore + fun gnunetDecodeCheck() { + // condition: "gnunet-base32 -d /tmp/blob.enc" needs to decode to /tmp/blob.bin + val blob = File("/tmp/blob.bin").readBytes() + val blobEnc = File("/tmp/blob.enc").readText(Charsets.UTF_8) + val dec = Base32Crockford.decode(blobEnc) + assertTrue(blob.contentEquals(dec)) + } + + @Test + fun emptyBase32Test() { + val enc = Base32Crockford.encode(ByteArray(0)) + assert(enc.isEmpty()) + val blob = Base32Crockford.decode("") + assert(blob.isEmpty()) + } + + @Test + fun passwordHashing() { + val x = CryptoUtil.hashpw("myinsecurepw") + assertTrue(CryptoUtil.checkpw("myinsecurepw", x)) + } +} diff --git a/common/src/test/kotlin/PaytoTest.kt b/common/src/test/kotlin/PaytoTest.kt @@ -0,0 +1,51 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.junit.Test +import tech.libeufin.common.IbanPayto +import tech.libeufin.common.parsePayto + +class PaytoTest { + + @Test + fun wrongCases() { + assert(parsePayto("http://iban/BIC123/IBAN123?receiver-name=The%20Name") == null) + assert(parsePayto("payto:iban/BIC123/IBAN123?receiver-name=The%20Name&address=house") == null) + assert(parsePayto("payto://wrong/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo") == null) + } + + @Test + fun parsePaytoTest() { + val withBic: IbanPayto = parsePayto("payto://iban/BIC123/IBAN123?receiver-name=The%20Name")!! + assert(withBic.iban == "IBAN123") + assert(withBic.bic == "BIC123") + assert(withBic.receiverName == "The Name") + val complete = parsePayto("payto://iban/BIC123/IBAN123?sender-name=The%20Name&amount=EUR:1&message=donation")!! + assert(withBic.iban == "IBAN123") + assert(withBic.bic == "BIC123") + assert(withBic.receiverName == "The Name") + assert(complete.message == "donation") + assert(complete.amount == "EUR:1") + val withoutOptionals = parsePayto("payto://iban/IBAN123")!! + assert(withoutOptionals.bic == null) + assert(withoutOptionals.message == null) + assert(withoutOptionals.receiverName == null) + assert(withoutOptionals.amount == null) + } +} +\ No newline at end of file diff --git a/common/src/test/kotlin/TalerConfigTest.kt b/common/src/test/kotlin/TalerConfigTest.kt @@ -0,0 +1,63 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + * + * 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/> + */ + +import org.junit.Test +import kotlin.test.assertEquals +import tech.libeufin.common.* + +class TalerConfigTest { + + @Test + fun parsing() { + // We assume that libeufin-bank is installed. We could also try to locate the source tree here. + val conf = TalerConfig(ConfigSource("libeufin", "libeufin-bank", "libeufin-bank")) + conf.loadDefaults() + conf.loadFromString( + """ + + [foo] + + bar = baz + + """.trimIndent() + ) + + println(conf.stringify()) + + assertEquals("baz", conf.lookupString("foo", "bar")) + + println(conf.getInstallPath()) + } + + @Test + fun substitution() { + // We assume that libeufin-bank is installed. We could also try to locate the source tree here. + val conf = TalerConfig(ConfigSource("libeufin", "libeufin-bank", "libeufin-bank")) + conf.putValueString("PATHS", "DATADIR", "mydir") + conf.putValueString("foo", "bar", "baz") + conf.putValueString("foo", "bar2", "baz") + + assertEquals("baz", conf.lookupString("foo", "bar")) + assertEquals("baz", conf.lookupPath("foo", "bar")) + + conf.putValueString("foo", "dir1", "foo/\$DATADIR/bar") + + assertEquals("foo/mydir/bar", conf.lookupPath("foo", "dir1")) + } +} diff --git a/common/src/test/kotlin/TimeTest.kt b/common/src/test/kotlin/TimeTest.kt @@ -0,0 +1,48 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.junit.Test +import tech.libeufin.common.maxTimestamp +import tech.libeufin.common.minTimestamp +import java.time.Instant +import java.time.temporal.ChronoUnit +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class TimeTest { + @Test + fun cmp() { + val now = Instant.now() + val inOneMinute = now.plus(1, ChronoUnit.MINUTES) + + // testing the "min" function + assertNull(minTimestamp(null, null)) + assertEquals(now, minTimestamp(now, inOneMinute)) + assertNull(minTimestamp(now, null)) + assertNull(minTimestamp(null, now)) + assertEquals(inOneMinute, minTimestamp(inOneMinute, inOneMinute)) + + // testing the "max" function + assertNull(maxTimestamp(null, null)) + assertEquals(inOneMinute, maxTimestamp(now, inOneMinute)) + assertEquals(now, maxTimestamp(now, null)) + assertEquals(now, maxTimestamp(null, now)) + assertEquals(now, minTimestamp(now, now)) + } +} +\ No newline at end of file diff --git a/util/src/test/resources/ebics_hev.xml b/common/src/test/resources/ebics_hev.xml diff --git a/util/src/test/resources/ebics_ini_inner_key.xml b/common/src/test/resources/ebics_ini_inner_key.xml diff --git a/util/src/test/resources/ebics_ini_request_sample.xml b/common/src/test/resources/ebics_ini_request_sample.xml diff --git a/util/src/test/resources/hia_request.xml b/common/src/test/resources/hia_request.xml diff --git a/util/src/test/resources/hia_request_order_data.xml b/common/src/test/resources/hia_request_order_data.xml diff --git a/util/src/test/resources/hpb_request.xml b/common/src/test/resources/hpb_request.xml diff --git a/util/src/test/resources/signature1/doc.xml b/common/src/test/resources/signature1/doc.xml diff --git a/util/src/test/resources/signature1/public_key.txt b/common/src/test/resources/signature1/public_key.txt diff --git a/ebics/build.gradle b/ebics/build.gradle @@ -0,0 +1,29 @@ +plugins { + id("java") + id("kotlin") +} + +version = rootProject.version + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +compileKotlin.kotlinOptions.jvmTarget = "17" +compileTestKotlin.kotlinOptions.jvmTarget = "17" + +sourceSets.main.java.srcDirs = ["src/main/kotlin"] + +dependencies { + implementation(project(":common")) + + implementation("ch.qos.logback:logback-classic:1.4.5") + // XML Stuff + implementation("javax.xml.bind:jaxb-api:2.3.1") + implementation("org.glassfish.jaxb:jaxb-runtime:2.3.1") + implementation("org.apache.santuario:xmlsec:2.2.2") + + implementation("io.ktor:ktor-http:$ktor_version") + implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") +} +\ No newline at end of file diff --git a/ebics/import.py b/ebics/import.py @@ -0,0 +1,66 @@ +# Update EBICS constants file using latest external code sets files + +import requests +from zipfile import ZipFile +from io import BytesIO +import polars as pl + +# Get XLSX zip file from server +r = requests.get( + "https://www.iso20022.org/sites/default/files/media/file/ExternalCodeSets_XLSX.zip" +) +assert r.status_code == 200 + +# Unzip the XLSX file +zip = ZipFile(BytesIO(r.content)) +files = zip.namelist() +assert len(files) == 1 +file = zip.open(files[0]) + +# Parse excel +df = pl.read_excel(file, sheet_name="AllCodeSets") + +def extractCodeSet(setName: str, className: str) -> str: + out = f"enum class {className}(val isoCode: String, val description: String) {{" + + for row in df.filter(pl.col("Code Set") == setName).sort("Code Value").rows(named=True): + (value, isoCode, description) = ( + row["Code Value"], + row["Code Name"], + row["Code Definition"].split("\n", 1)[0].strip(), + ) + out += f'\n\t{value}("{isoCode}", "{description}"),' + + out += "\n}" + return out + +# Write kotlin file +kt = f"""/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +// THIS FILE IS GENERATED, DO NOT EDIT + +package tech.libeufin.ebics + +{extractCodeSet("ExternalStatusReason1Code", "ExternalStatusReasonCode")} + +{extractCodeSet("ExternalPaymentGroupStatus1Code", "ExternalPaymentGroupStatusCode")} +""" +with open("src/main/kotlin/EbicsCodeSets.kt", "w") as file1: + file1.write(kt) diff --git a/ebics/src/main/kotlin/Ebics.kt b/ebics/src/main/kotlin/Ebics.kt @@ -0,0 +1,431 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +/** + * This is the main "EBICS library interface". Functions here are stateless helpers + * used to implement both an EBICS server and EBICS client. + */ + +package tech.libeufin.ebics + +import tech.libeufin.common.CryptoUtil +import io.ktor.http.HttpStatusCode +import tech.libeufin.ebics.ebics_h004.* +import tech.libeufin.ebics.ebics_h005.Ebics3Response +import tech.libeufin.ebics.ebics_s001.UserSignatureData +import java.security.SecureRandom +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.util.* +import javax.xml.bind.JAXBElement +import javax.xml.datatype.DatatypeFactory +import javax.xml.datatype.XMLGregorianCalendar + +data class EbicsProtocolError( + val httpStatusCode: HttpStatusCode, + val reason: String, + /** + * This class is also used when Nexus finds itself + * in an inconsistent state, without interacting with the + * bank. In this case, the EBICS code below can be left + * null. + */ + val ebicsTechnicalCode: EbicsReturnCode? = null +) : Exception(reason) + +data class EbicsDateRange( + val start: Instant, + val end: Instant +) + +sealed class EbicsOrderParams +data class EbicsStandardOrderParams( + val dateRange: EbicsDateRange? = null +) : EbicsOrderParams() + +data class EbicsGenericOrderParams( + val params: Map<String, String> = mapOf() +) : EbicsOrderParams() + +enum class EbicsInitState { + SENT, NOT_SENT, UNKNOWN +} + +/** + * This class is a mere container that keeps data found + * in the database and that is further needed to sign / verify + * / make messages. And not all the values are needed all + * the time. + */ +data class EbicsClientSubscriberDetails( + val partnerId: String, + val userId: String, + var bankAuthPub: RSAPublicKey?, + var bankEncPub: RSAPublicKey?, + val ebicsUrl: String, + val hostId: String, + val customerEncPriv: RSAPrivateCrtKey, + val customerAuthPriv: RSAPrivateCrtKey, + val customerSignPriv: RSAPrivateCrtKey, + val ebicsIniState: EbicsInitState, + val ebicsHiaState: EbicsInitState, + var dialect: String? = null +) + +/** + * @param size in bits + */ +fun getNonce(size: Int): ByteArray { + val sr = SecureRandom() + val ret = ByteArray(size / 8) + sr.nextBytes(ret) + return ret +} + +fun getXmlDate(i: Instant): XMLGregorianCalendar { + val zonedTimestamp = ZonedDateTime.ofInstant(i, ZoneId.of("UTC")) + return getXmlDate(zonedTimestamp) +} +fun getXmlDate(d: ZonedDateTime): XMLGregorianCalendar { + return DatatypeFactory.newInstance() + .newXMLGregorianCalendar( + d.year, + d.monthValue, + d.dayOfMonth, + 0, + 0, + 0, + 0, + d.offset.totalSeconds / 60 + ) +} + +fun makeOrderParams(orderParams: EbicsOrderParams): EbicsRequest.OrderParams { + return when (orderParams) { + is EbicsStandardOrderParams -> { + EbicsRequest.StandardOrderParams().apply { + val r = orderParams.dateRange + if (r != null) { + this.dateRange = EbicsRequest.DateRange().apply { + this.start = getXmlDate(r.start) + this.end = getXmlDate(r.end) + } + } + } + } + is EbicsGenericOrderParams -> { + EbicsRequest.GenericOrderParams().apply { + this.parameterList = orderParams.params.map { entry -> + EbicsTypes.Parameter().apply { + this.name = entry.key + this.value = entry.value + this.type = "string" + } + } + } + } + } +} + +fun signOrder( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): UserSignatureData { + val ES_signature = CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ) + val userSignatureData = UserSignatureData().apply { + orderSignatureList = listOf( + UserSignatureData.OrderSignatureData().apply { + signatureVersion = "A006" + signatureValue = ES_signature + partnerID = partnerId + userID = userId + } + ) + } + return userSignatureData +} + +fun signOrderEbics3( + orderBlob: ByteArray, + signKey: RSAPrivateCrtKey, + partnerId: String, + userId: String +): tech.libeufin.ebics.ebics_s002.UserSignatureDataEbics3 { + val ES_signature = CryptoUtil.signEbicsA006( + CryptoUtil.digestEbicsOrderA006(orderBlob), + signKey + ) + val userSignatureData = tech.libeufin.ebics.ebics_s002.UserSignatureDataEbics3().apply { + orderSignatureList = listOf( + tech.libeufin.ebics.ebics_s002.UserSignatureDataEbics3.OrderSignatureData().apply { + signatureVersion = "A006" + signatureValue = ES_signature + partnerID = partnerId + userID = userId + } + ) + } + return userSignatureData +} + +data class PreparedUploadData( + val transactionKey: ByteArray, + val userSignatureDataEncrypted: ByteArray, + val dataDigest: ByteArray, + val encryptedPayloadChunks: List<String> +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PreparedUploadData + + if (!transactionKey.contentEquals(other.transactionKey)) return false + if (!userSignatureDataEncrypted.contentEquals(other.userSignatureDataEncrypted)) return false + if (encryptedPayloadChunks != other.encryptedPayloadChunks) return false + + return true + } + + override fun hashCode(): Int { + var result = transactionKey.contentHashCode() + result = 31 * result + userSignatureDataEncrypted.contentHashCode() + result = 31 * result + encryptedPayloadChunks.hashCode() + return result + } +} + +data class DataEncryptionInfo( + val transactionKey: ByteArray, + val bankPubDigest: ByteArray +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as DataEncryptionInfo + + if (!transactionKey.contentEquals(other.transactionKey)) return false + if (!bankPubDigest.contentEquals(other.bankPubDigest)) return false + + return true + } + + override fun hashCode(): Int { + var result = transactionKey.contentHashCode() + result = 31 * result + bankPubDigest.contentHashCode() + return result + } +} + + +// TODO import missing using a script +@Suppress("SpellCheckingInspection") +enum class EbicsReturnCode(val errorCode: String) { + EBICS_OK("000000"), + EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), + EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), + EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), + EBICS_AUTHENTICATION_FAILED("061001"), + EBICS_INVALID_REQUEST("061002"), + EBICS_INTERNAL_ERROR("061099"), + EBICS_TX_RECOVERY_SYNC("061101"), + EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), + EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"), + EBICS_INVALID_USER_OR_USER_STATE("091002"), + EBICS_USER_UNKNOWN("091003"), + EBICS_EBICS_INVALID_USER_STATE("091004"), + EBICS_INVALID_ORDER_IDENTIFIER("091005"), + EBICS_UNSUPPORTED_ORDER_TYPE("091006"), + EBICS_INVALID_XML("091010"), + EBICS_TX_MESSAGE_REPLAY("091103"), + EBICS_PROCESSING_ERROR("091116"), + EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), + EBICS_AMOUNT_CHECK_FAILED("091303"); + + companion object { + fun lookup(errorCode: String): EbicsReturnCode { + for (x in values()) { + if (x.errorCode == errorCode) { + return x + } + } + throw Exception( + "Unknown EBICS status code: $errorCode" + ) + } + } +} + +data class EbicsResponseContent( + val transactionID: String?, + val dataEncryptionInfo: DataEncryptionInfo?, + val orderDataEncChunk: String?, + val technicalReturnCode: EbicsReturnCode, + val bankReturnCode: EbicsReturnCode, + val reportText: String, + val segmentNumber: Int?, + // Only present in init phase + val numSegments: Int? +) + +data class EbicsKeyManagementResponseContent( + val technicalReturnCode: EbicsReturnCode, + val bankReturnCode: EbicsReturnCode?, + val orderData: ByteArray? +) + + +class HpbResponseData( + val hostID: String, + val encryptionPubKey: RSAPublicKey, + val encryptionVersion: String, + val authenticationPubKey: RSAPublicKey, + val authenticationVersion: String +) + +fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData { + val resp = try { + XMLUtil.convertStringToJaxb<HPBResponseOrderData>(orderDataRaw.toString(Charsets.UTF_8)) + } catch (e: Exception) { + throw EbicsProtocolError(HttpStatusCode.InternalServerError, "Invalid XML (as HPB response) received from bank") + } + val encPubKey = CryptoUtil.loadRsaPublicKeyFromComponents( + resp.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + resp.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + val authPubKey = CryptoUtil.loadRsaPublicKeyFromComponents( + resp.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, + resp.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent + ) + return HpbResponseData( + hostID = resp.value.hostID, + encryptionPubKey = encPubKey, + encryptionVersion = resp.value.encryptionPubKeyInfo.encryptionVersion, + authenticationPubKey = authPubKey, + authenticationVersion = resp.value.authenticationPubKeyInfo.authenticationVersion + ) +} + +fun ebics3toInternalRepr(response: String): EbicsResponseContent { + // logger.debug("Converting bank resp to internal repr.: $response") + val resp: JAXBElement<Ebics3Response> = try { + XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Could not transform string-response from bank into JAXB" + ) + } + val bankReturnCodeStr = resp.value.body.returnCode.value + val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) + + val techReturnCodeStr = resp.value.header.mutable.returnCode + val techReturnCode = EbicsReturnCode.lookup(techReturnCodeStr) + + val reportText = resp.value.header.mutable.reportText + + val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo + val dataEncryptionInfo = if (daeXml == null) { + null + } else { + DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) + } + + return EbicsResponseContent( + transactionID = resp.value.header._static.transactionID, + bankReturnCode = bankReturnCode, + technicalReturnCode = techReturnCode, + reportText = reportText, + orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, + dataEncryptionInfo = dataEncryptionInfo, + numSegments = resp.value.header._static.numSegments?.toInt(), + segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() + ) +} + +fun ebics25toInternalRepr(response: String): EbicsResponseContent { + val resp: JAXBElement<EbicsResponse> = try { + XMLUtil.convertStringToJaxb(response) + } catch (e: Exception) { + throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Could not transform string-response from bank into JAXB" + ) + } + val bankReturnCodeStr = resp.value.body.returnCode.value + val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) + + val techReturnCodeStr = resp.value.header.mutable.returnCode + val techReturnCode = EbicsReturnCode.lookup(techReturnCodeStr) + + val reportText = resp.value.header.mutable.reportText + + val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo + val dataEncryptionInfo = if (daeXml == null) { + null + } else { + DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) + } + + return EbicsResponseContent( + transactionID = resp.value.header._static.transactionID, + bankReturnCode = bankReturnCode, + technicalReturnCode = techReturnCode, + reportText = reportText, + orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, + dataEncryptionInfo = dataEncryptionInfo, + numSegments = resp.value.header._static.numSegments?.toInt(), + segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() + ) +} + +/** + * Get the private key that matches the given public key digest. + */ +fun getDecryptionKey(subscriberDetails: EbicsClientSubscriberDetails, pubDigest: ByteArray): RSAPrivateCrtKey { + val authPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerAuthPriv) + val encPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv) + val authPubDigest = CryptoUtil.getEbicsPublicKeyHash(authPub) + val encPubDigest = CryptoUtil.getEbicsPublicKeyHash(encPub) + if (pubDigest.contentEquals(authPubDigest)) { + return subscriberDetails.customerAuthPriv + } + if (pubDigest.contentEquals(encPubDigest)) { + return subscriberDetails.customerEncPriv + } + throw EbicsProtocolError(HttpStatusCode.NotFound, "Could not find customer's public key") +} + +data class EbicsVersionSpec( + val protocol: String, + val version: String +) + +data class EbicsHevDetails( + val versions: List<EbicsVersionSpec> +) +\ No newline at end of file diff --git a/ebics/src/main/kotlin/EbicsCodeSets.kt b/ebics/src/main/kotlin/EbicsCodeSets.kt @@ -0,0 +1,309 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +// THIS FILE IS GENERATED, DO NOT EDIT + +package tech.libeufin.ebics + +enum class ExternalStatusReasonCode(val isoCode: String, val description: String) { + AB01("AbortedClearingTimeout", "Clearing process aborted due to timeout."), + AB02("AbortedClearingFatalError", "Clearing process aborted due to a fatal error."), + AB03("AbortedSettlementTimeout", "Settlement aborted due to timeout."), + AB04("AbortedSettlementFatalError", "Settlement process aborted due to a fatal error."), + AB05("TimeoutCreditorAgent", "Transaction stopped due to timeout at the Creditor Agent."), + AB06("TimeoutInstructedAgent", "Transaction stopped due to timeout at the Instructed Agent."), + AB07("OfflineAgent", "Agent of message is not online."), + AB08("OfflineCreditorAgent", "Creditor Agent is not online."), + AB09("ErrorCreditorAgent", "Transaction stopped due to error at the Creditor Agent."), + AB10("ErrorInstructedAgent", "Transaction stopped due to error at the Instructed Agent."), + AB11("TimeoutDebtorAgent", "Transaction stopped due to timeout at the Debtor Agent."), + AC01("IncorrectAccountNumber", "Account number is invalid or missing."), + AC02("InvalidDebtorAccountNumber", "Debtor account number invalid or missing"), + AC03("InvalidCreditorAccountNumber", "Creditor account number invalid or missing"), + AC04("ClosedAccountNumber", "Account number specified has been closed on the bank of account's books."), + AC05("ClosedDebtorAccountNumber", "Debtor account number closed"), + AC06("BlockedAccount", "Account specified is blocked, prohibiting posting of transactions against it."), + AC07("ClosedCreditorAccountNumber", "Creditor account number closed"), + AC08("InvalidBranchCode", "Branch code is invalid or missing"), + AC09("InvalidAccountCurrency", "Account currency is invalid or missing"), + AC10("InvalidDebtorAccountCurrency", "Debtor account currency is invalid or missing"), + AC11("InvalidCreditorAccountCurrency", "Creditor account currency is invalid or missing"), + AC12("InvalidAccountType", "Account type missing or invalid."), + AC13("InvalidDebtorAccountType", "Debtor account type missing or invalid"), + AC14("InvalidCreditorAccountType", "Creditor account type missing or invalid"), + AC15("AccountDetailsChanged", "The account details for the counterparty have changed."), + AC16("CardNumberInvalid", "Credit or debit card number is invalid."), + AEXR("AlreadyExpiredRTP", "Request-to-pay Expiry Date and Time has already passed."), + AG01("TransactionForbidden", "Transaction forbidden on this type of account (formerly NoAgreement)"), + AG02("InvalidBankOperationCode", "Bank Operation code specified in the message is not valid for receiver"), + AG03("TransactionNotSupported", "Transaction type not supported/authorized on this account"), + AG04("InvalidAgentCountry", "Agent country code is missing or invalid."), + AG05("InvalidDebtorAgentCountry", "Debtor agent country code is missing or invalid"), + AG06("InvalidCreditorAgentCountry", "Creditor agent country code is missing or invalid"), + AG07("UnsuccesfulDirectDebit", "Debtor account cannot be debited for a generic reason."), + AG08("InvalidAccessRights", "Transaction failed due to invalid or missing user or access right"), + AG09("PaymentNotReceived", "Original payment never received."), + AG10("AgentSuspended", "Agent of message is suspended from the Real Time Payment system."), + AG11("CreditorAgentSuspended", "Creditor Agent of message is suspended from the Real Time Payment system."), + AG12("NotAllowedBookTransfer", "Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed."), + AG13("ForbiddenReturnPayment", "Returned payments derived from previously returned transactions are not allowed."), + AGNT("IncorrectAgent", "Agent in the payment workflow is incorrect"), + ALAC("AlreadyAcceptedRTP", "Request-to-pay has already been accepted by the Debtor."), + AM01("ZeroAmount", "Specified message amount is equal to zero"), + AM02("NotAllowedAmount", "Specific transaction/message amount is greater than allowed maximum"), + AM03("NotAllowedCurrency", "Specified message amount is an non processable currency outside of existing agreement"), + AM04("InsufficientFunds", "Amount of funds available to cover specified message amount is insufficient."), + AM05("Duplication", "Duplication"), + AM06("TooLowAmount", "Specified transaction amount is less than agreed minimum."), + AM07("BlockedAmount", "Amount specified in message has been blocked by regulatory authorities."), + AM09("WrongAmount", "Amount received is not the amount agreed or expected"), + AM10("InvalidControlSum", "Sum of instructed amounts does not equal the control sum."), + AM11("InvalidTransactionCurrency", "Transaction currency is invalid or missing"), + AM12("InvalidAmount", "Amount is invalid or missing"), + AM13("AmountExceedsClearingSystemLimit", "Transaction amount exceeds limits set by clearing system"), + AM14("AmountExceedsAgreedLimit", "Transaction amount exceeds limits agreed between bank and client"), + AM15("AmountBelowClearingSystemMinimum", "Transaction amount below minimum set by clearing system"), + AM16("InvalidGroupControlSum", "Control Sum at the Group level is invalid"), + AM17("InvalidPaymentInfoControlSum", "Control Sum at the Payment Information level is invalid"), + AM18("InvalidNumberOfTransactions", "Number of transactions is invalid or missing."), + AM19("InvalidGroupNumberOfTransactions", "Number of transactions at the Group level is invalid or missing"), + AM20("InvalidPaymentInfoNumberOfTransactions", "Number of transactions at the Payment Information level is invalid"), + AM21("LimitExceeded", "Transaction amount exceeds limits agreed between bank and client."), + AM22("ZeroAmountNotApplied", "Unable to apply zero amount to designated account. For example, where the rules of a service allow the use of zero amount payments, however the back-office system is unable to apply the funds to the account. If the rules of a service prohibit the use of zero amount payments, then code AM01 is used to report the error condition."), + AM23("AmountExceedsSettlementLimit", "Transaction amount exceeds settlement limit."), + APAR("AlreadyPaidRTP", "Request To Pay has already been paid by the Debtor."), + ARFR("AlreadyRefusedRTP", "Request-to-pay has already been refused by the Debtor."), + ARJR("AlreadyRejectedRTP", "Request-to-pay has already been rejected."), + ATNS("AttachementsNotSupported", "Attachments to the request-to-pay are not supported."), + BE01("InconsistenWithEndCustomer", "Identification of end customer is not consistent with associated account number. (formerly CreditorConsistency)."), + BE04("MissingCreditorAddress", "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)."), + BE05("UnrecognisedInitiatingParty", "Party who initiated the message is not recognised by the end customer"), + BE06("UnknownEndCustomer", "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books"), + BE07("MissingDebtorAddress", "Specification of debtor's address, which is required for payment, is missing/not correct."), + BE08("MissingDebtorName", "Debtor name is missing"), + BE09("InvalidCountry", "Country code is missing or Invalid."), + BE10("InvalidDebtorCountry", "Debtor country code is missing or invalid"), + BE11("InvalidCreditorCountry", "Creditor country code is missing or invalid"), + BE12("InvalidCountryOfResidence", "Country code of residence is missing or Invalid."), + BE13("InvalidDebtorCountryOfResidence", "Country code of debtor's residence is missing or Invalid"), + BE14("InvalidCreditorCountryOfResidence", "Country code of creditor's residence is missing or Invalid"), + BE15("InvalidIdentificationCode", "Identification code missing or invalid."), + BE16("InvalidDebtorIdentificationCode", "Debtor or Ultimate Debtor identification code missing or invalid"), + BE17("InvalidCreditorIdentificationCode", "Creditor or Ultimate Creditor identification code missing or invalid"), + BE18("InvalidContactDetails", "Contact details missing or invalid"), + BE19("InvalidChargeBearerCode", "Charge bearer code for transaction type is invalid"), + BE20("InvalidNameLength", "Name length exceeds local rules for payment type."), + BE21("MissingName", "Name missing or invalid. Generic usage if cannot specifically identify debtor or creditor."), + BE22("MissingCreditorName", "Creditor name is missing"), + BE23("AccountProxyInvalid", "Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid."), + CERI("CheckERI", "Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI."), + CH03("RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture", "Value in Requested Execution Date or Requested Collection Date is too far in the future"), + CH04("RequestedExecutionDateOrRequestedCollectionDateTooFarInPast", "Value in Requested Execution Date or Requested Collection Date is too far in the past"), + CH07("ElementIsNotToBeUsedAtB-andC-Level", "Element is not to be used at B- and C-Level"), + CH09("MandateChangesNotAllowed", "Mandate changes are not allowed"), + CH10("InformationOnMandateChangesMissing", "Information on mandate changes are missing"), + CH11("CreditorIdentifierIncorrect", "Value in Creditor Identifier is incorrect"), + CH12("CreditorIdentifierNotUnambiguouslyAtTransaction-Level", "Creditor Identifier is ambiguous at Transaction Level"), + CH13("OriginalDebtorAccountIsNotToBeUsed", "Original Debtor Account is not to be used"), + CH14("OriginalDebtorAgentIsNotToBeUsed", "Original Debtor Agent is not to be used"), + CH15("ElementContentIncludesMoreThan140Characters", "Content Remittance Information/Structured includes more than 140 characters"), + CH16("ElementContentFormallyIncorrect", "Content is incorrect"), + CH17("ElementNotAdmitted", "Element is not allowed"), + CH19("ValuesWillBeSetToNextTARGETday", "Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day"), + CH20("DecimalPointsNotCompatibleWithCurrency", "Number of decimal points not compatible with the currency"), + CH21("RequiredCompulsoryElementMissing", "Mandatory element is missing"), + CH22("COREandB2BwithinOnemessage", "SDD CORE and B2B not permitted within one message"), + CHQC("ChequeSettledOnCreditorAccount", "Cheque has been presented in cheque clearing and settled on the creditor’s account."), + CN01("AuthorisationCancelled", "Authorisation is cancelled."), + CNOR("CreditorBankIsNotRegistered", "Creditor bank is not registered under this BIC in the CSM"), + CURR("IncorrectCurrency", "Currency of the payment is incorrect"), + CUST("RequestedByCustomer", "Cancellation requested by the Debtor"), + DC02("SettlementNotReceived", "Rejection of a payment due to covering FI settlement not being received."), + DNOR("DebtorBankIsNotRegistered", "Debtor bank is not registered under this BIC in the CSM"), + DS01("ElectronicSignaturesCorrect", "The electronic signature(s) is/are correct"), + DS02("OrderCancelled", "An authorized user has cancelled the order"), + DS03("OrderNotCancelled", "The user’s attempt to cancel the order was not successful"), + DS04("OrderRejected", "The order was rejected by the bank side (for reasons concerning content)"), + DS05("OrderForwardedForPostprocessing", "The order was correct and could be forwarded for postprocessing"), + DS06("TransferOrder", "The order was transferred to VEU"), + DS07("ProcessingOK", "All actions concerning the order could be done by the EBICS bank server"), + DS08("DecompressionError", "The decompression of the file was not successful"), + DS09("DecryptionError", "The decryption of the file was not successful"), + DS0A("DataSignRequested", "Data signature is required."), + DS0B("UnknownDataSignFormat", "Data signature for the format is not available or invalid."), + DS0C("SignerCertificateRevoked", "The signer certificate is revoked."), + DS0D("SignerCertificateNotValid", "The signer certificate is not valid (revoked or not active)."), + DS0E("IncorrectSignerCertificate", "The signer certificate is not present."), + DS0F("SignerCertificationAuthoritySignerNotValid", "The authority of the signer certification sending the certificate is unknown."), + DS0G("NotAllowedPayment", "Signer is not allowed to sign this operation type."), + DS0H("NotAllowedAccount", "Signer is not allowed to sign for this account."), + DS0K("NotAllowedNumberOfTransaction", "The number of transaction is over the number allowed for this signer."), + DS10("Signer1CertificateRevoked", "The certificate is revoked for the first signer."), + DS11("Signer1CertificateNotValid", "The certificate is not valid (revoked or not active) for the first signer."), + DS12("IncorrectSigner1Certificate", "The certificate is not present for the first signer."), + DS13("SignerCertificationAuthoritySigner1NotValid", "The authority of signer certification sending the certificate is unknown for the first signer."), + DS14("UserDoesNotExist", "The user is unknown on the server"), + DS15("IdenticalSignatureFound", "The same signature has already been sent to the bank"), + DS16("PublicKeyVersionIncorrect", "The public key version is not correct. This code is returned when a customer sends signature files to the financial institution after conversion from an older program version (old ES format) to a new program version (new ES format) without having carried out re-initialisation with regard to a public key change."), + DS17("DifferentOrderDataInSignatures", "Order data and signatures don’t match"), + DS18("RepeatOrder", "File cannot be tested, the complete order has to be repeated. This code is returned in the event of a malfunction during the signature check, e.g. not enough storage space."), + DS19("ElectronicSignatureRightsInsufficient", "The user’s rights (concerning his signature) are insufficient to execute the order"), + DS20("Signer2CertificateRevoked", "The certificate is revoked for the second signer."), + DS21("Signer2CertificateNotValid", "The certificate is not valid (revoked or not active) for the second signer."), + DS22("IncorrectSigner2Certificate", "The certificate is not present for the second signer."), + DS23("SignerCertificationAuthoritySigner2NotValid", "The authority of signer certification sending the certificate is unknown for the second signer."), + DS24("WaitingTimeExpired", "Waiting time expired due to incomplete order"), + DS25("OrderFileDeleted", "The order file was deleted by the bank server"), + DS26("UserSignedMultipleTimes", "The same user has signed multiple times"), + DS27("UserNotYetActivated", "The user is not yet activated (technically)"), + DT01("InvalidDate", "Invalid date (eg, wrong or missing settlement date)"), + DT02("InvalidCreationDate", "Invalid creation date and time in Group Header (eg, historic date)"), + DT03("InvalidNonProcessingDate", "Invalid non bank processing date (eg, weekend or local public holiday)"), + DT04("FutureDateNotSupported", "Future date not supported"), + DT05("InvalidCutOffDate", "Associated message, payment information block or transaction was received after agreed processing cut-off date, i.e., date in the past."), + DT06("ExecutionDateChanged", "Execution Date has been modified in order for transaction to be processed"), + DU01("DuplicateMessageID", "Message Identification is not unique."), + DU02("DuplicatePaymentInformationID", "Payment Information Block is not unique."), + DU03("DuplicateTransaction", "Transaction is not unique."), + DU04("DuplicateEndToEndID", "End To End ID is not unique."), + DU05("DuplicateInstructionID", "Instruction ID is not unique."), + DUPL("DuplicatePayment", "Payment is a duplicate of another payment"), + ED01("CorrespondentBankNotPossible", "Correspondent bank not possible."), + ED03("BalanceInfoRequest", "Balance of payments complementary info is requested"), + ED05("SettlementFailed", "Settlement of the transaction has failed."), + ED06("SettlementSystemNotAvailable", "Interbank settlement system not available."), + EDTL("ExpiryDateTooLong", "Expiry date time of the request-to-pay is too far in the future."), + EDTR("ExpiryDateTimeReached", "Expiry date time of the request-to-pay is already reached."), + ERIN("ERIOptionNotSupported", "Extended Remittance Information (ERI) option is not supported."), + FF01("InvalidFileFormat", "File Format incomplete or invalid"), + FF02("SyntaxError", "Syntax error reason is provided as narrative information in the additional reason information."), + FF03("InvalidPaymentTypeInformation", "Payment Type Information is missing or invalid."), + FF04("InvalidServiceLevelCode", "Service Level code is missing or invalid"), + FF05("InvalidLocalInstrumentCode", "Local Instrument code is missing or invalid"), + FF06("InvalidCategoryPurposeCode", "Category Purpose code is missing or invalid"), + FF07("InvalidPurpose", "Purpose is missing or invalid"), + FF08("InvalidEndToEndId", "End to End Id missing or invalid"), + FF09("InvalidChequeNumber", "Cheque number missing or invalid"), + FF10("BankSystemProcessingError", "File or transaction cannot be processed due to technical issues at the bank side"), + FF11("ClearingRequestAborted", "Clearing request rejected due it being subject to an abort operation."), + FF12("OriginalTransactionNotEligibleForRequestedReturn", "Original payment is not eligible to be returned given its current status."), + FF13("RequestForCancellationNotFound", "No record of request for cancellation found."), + FOCR("FollowingCancellationRequest", "Return following a cancellation request."), + FR01("Fraud", "Returned as a result of fraud."), + FRAD("FraudulentOrigin", "Cancellation requested following a transaction that was originated fraudulently. The use of the FraudulentOrigin code should be governed by jurisdictions."), + G000("PaymentTransferredAndTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is tracked. No further updates will follow from the Status Originator."), + G001("PaymentTransferredAndNotTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is not tracked. No further updates will follow from the Status Originator."), + G002("CreditDebitNotConfirmed", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day. Update will follow from the Status Originator."), + G003("CreditPendingDocuments", "In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents. The Status Originator has requested creditor to provide additional documentation. Update will follow from the Status Originator."), + G004("CreditPendingFunds", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover. Update will follow from the Status Originator."), + G005("DeliveredWithServiceLevel", "Payment has been delivered to creditor agent with service level."), + G006("DeliveredWIthoutServiceLevel", "Payment has been delivered to creditor agent without service level."), + ID01("CorrespondingOriginalFileStillNotSent", "Signature file was sent to the bank but the corresponding original file has not been sent yet."), + IEDT("IncorrectExpiryDateTime", "Expiry date time of the request-to-pay is incorrect."), + IRNR("InitialRTPNeverReceived", "No initial request-to-pay has been received."), + MD01("NoMandate", "No Mandate"), + MD02("MissingMandatoryInformationInMandate", "Mandate related information data required by the scheme is missing."), + MD05("CollectionNotDue", "Creditor or creditor's agent should not have collected the direct debit"), + MD06("RefundRequestByEndCustomer", "Return of funds requested by end customer"), + MD07("EndCustomerDeceased", "End customer is deceased."), + MS02("NotSpecifiedReasonCustomerGenerated", "Reason has not been specified by end customer"), + MS03("NotSpecifiedReasonAgentGenerated", "Reason has not been specified by agent."), + NARR("Narrative", "Reason is provided as narrative information in the additional reason information."), + NERI("NoERI", "Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI."), + NOAR("NonAgreedRTP", "No existing agreement for receiving request-to-pay messages."), + NOAS("NoAnswerFromCustomer", "No response from Beneficiary."), + NOCM("NotCompliantGeneric", "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing."), + NOPG("NoPaymentGuarantee", "Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided."), + NRCH("PayerOrPayerRTPSPNotReachable", "Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable."), + PINS("TypeOfPaymentInstrumentNotSupported", "Type of payment requested in the request-to-pay is not supported by the payer."), + RC01("BankIdentifierIncorrect", "Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)."), + RC02("InvalidBankIdentifier", "Bank identifier is invalid or missing."), + RC03("InvalidDebtorBankIdentifier", "Debtor bank identifier is invalid or missing"), + RC04("InvalidCreditorBankIdentifier", "Creditor bank identifier is invalid or missing"), + RC05("InvalidBICIdentifier", "BIC identifier is invalid or missing."), + RC06("InvalidDebtorBICIdentifier", "Debtor BIC identifier is invalid or missing"), + RC07("InvalidCreditorBICIdentifier", "Creditor BIC identifier is invalid or missing"), + RC08("InvalidClearingSystemMemberIdentifier", "ClearingSystemMemberidentifier is invalid or missing."), + RC09("InvalidDebtorClearingSystemMemberIdentifier", "Debtor ClearingSystemMember identifier is invalid or missing"), + RC10("InvalidCreditorClearingSystemMemberIdentifier", "Creditor ClearingSystemMember identifier is invalid or missing"), + RC11("InvalidIntermediaryAgent", "Intermediary Agent is invalid or missing"), + RC12("MissingCreditorSchemeId", "Creditor Scheme Id is invalid or missing"), + RCON("RMessageConflict", "Conflict with R-Message"), + RECI("ReceiverCustomerInformation", "Further information regarding the intended recipient."), + REPR("RTPReceivedCanBeProcessed", "Request-to-pay has been received and can be processed further."), + RF01("NotUniqueTransactionReference", "Transaction reference is not unique within the message."), + RR01("MissingDebtorAccountOrIdentification", "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing"), + RR02("MissingDebtorNameOrAddress", "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing."), + RR03("MissingCreditorNameOrAddress", "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing."), + RR04("RegulatoryReason", "Regulatory Reason"), + RR05("RegulatoryInformationInvalid", "Regulatory or Central Bank Reporting information missing, incomplete or invalid."), + RR06("TaxInformationInvalid", "Tax information missing, incomplete or invalid."), + RR07("RemittanceInformationInvalid", "Remittance information structure does not comply with rules for payment type."), + RR08("RemittanceInformationTruncated", "Remittance information truncated to comply with rules for payment type."), + RR09("InvalidStructuredCreditorReference", "Structured creditor reference invalid or missing."), + RR10("InvalidCharacterSet", "Character set supplied not valid for the country and payment type."), + RR11("InvalidDebtorAgentServiceID", "Invalid or missing identification of a bank proprietary service."), + RR12("InvalidPartyID", "Invalid or missing identification required within a particular country or payment type."), + RTNS("RTPNotSupportedForDebtor", "Debtor does not support request-to-pay transactions."), + RUTA("ReturnUponUnableToApply", "Return following investigation request and no remediation possible."), + S000("ValidRequestForCancellationAcknowledged", "Request for Cancellation is acknowledged following validation."), + S001("UETRFlaggedForCancellation", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation."), + S002("NetworkStopOfUETR", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network."), + S003("RequestForCancellationForwarded", "Request for Cancellation has been forwarded to the payment processing/last payment processing agent."), + S004("RequestForCancellationDeliveryAcknowledgement", "Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent."), + SL01("SpecificServiceOfferedByDebtorAgent", "Due to specific service offered by the Debtor Agent."), + SL02("SpecificServiceOfferedByCreditorAgent", "Due to specific service offered by the Creditor Agent."), + SL03("ServiceofClearingSystem", "Due to a specific service offered by the clearing system."), + SL11("CreditorNotOnWhitelistOfDebtor", "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet). In the Whitelist the Debtor may list all allowed Creditors to debit Debtor bank account."), + SL12("CreditorOnBlacklistOfDebtor", "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”. In the Blacklist the Debtor may list all Creditors not allowed to debit Debtor bank account."), + SL13("MaximumNumberOfDirectDebitTransactionsExceeded", "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent."), + SL14("MaximumDirectDebitTransactionAmountExceeded", "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent."), + SPII("RTPServiceProviderIdentifierIncorrect", "Identifier of the request-to-pay service provider is incorrect."), + TA01("TransmissonAborted", "The transmission of the file was not successful – it had to be aborted (for technical reasons)"), + TD01("NoDataAvailable", "There is no data available (for download)"), + TD02("FileNonReadable", "The file cannot be read (e.g. unknown format)"), + TD03("IncorrectFileStructure", "The file format is incomplete or invalid"), + TK01("TokenInvalid", "Token is invalid."), + TK02("SenderTokenNotFound", "Token used for the sender does not exist."), + TK03("ReceiverTokenNotFound", "Token used for the receiver does not exist."), + TK09("TokenMissing", "Token required for request is missing."), + TKCM("TokenCounterpartyMismatch", "Token found with counterparty mismatch."), + TKSG("TokenSingleUse", "Single Use Token already used."), + TKSP("TokenSuspended", "Token found with suspended status."), + TKVE("TokenValueLimitExceeded", "Token found with value limit rule violation."), + TKXP("TokenExpired", "Token expired."), + TM01("InvalidCutOffTime", "Associated message, payment information block, or transaction was received after agreed processing cut-off time."), + TS01("TransmissionSuccessful", "The (technical) transmission of the file was successful."), + TS04("TransferToSignByHand", "The order was transferred to pass by accompanying note signed by hand"), + UCRD("UnknownCreditor", "Unknown Creditor."), + UPAY("UnduePayment", "Payment is not justified."), +} + +enum class ExternalPaymentGroupStatusCode(val isoCode: String, val description: String) { + ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."), + ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."), + ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement on the debtor's account has been completed."), + ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution."), + ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"), + ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."), + PART("PartiallyAccepted", "A number of transactions have been accepted, whereas another number of transactions have not yet achieved"), + PDNG("Pending", "Payment initiation or individual transaction included in the payment initiation is pending. Further checks and status update will be performed."), + RCVD("Received", "Payment initiation has been received by the receiving agent"), + RJCT("Rejected", "Payment initiation or individual transaction included in the payment initiation has been rejected."), +} diff --git a/ebics/src/main/kotlin/EbicsOrderUtil.kt b/ebics/src/main/kotlin/EbicsOrderUtil.kt @@ -0,0 +1,98 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.ebics + +import java.lang.IllegalArgumentException +import java.security.SecureRandom +import java.util.zip.DeflaterInputStream +import java.util.zip.InflaterInputStream + +/** + * Helpers for dealing with order compression, encryption, decryption, chunking and re-assembly. + */ +object EbicsOrderUtil { + + // Decompression only, no XML involved. + fun decodeOrderData(encodedOrderData: ByteArray): ByteArray { + return InflaterInputStream(encodedOrderData.inputStream()).use { + it.readAllBytes() + } + } + + 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() + } + } + + @kotlin.ExperimentalStdlibApi + fun generateTransactionId(): String { + val rng = SecureRandom() + val res = ByteArray(16) + rng.nextBytes(res) + return res.toHexString().uppercase() + } + + /** + * 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 + } + + 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/ebics/src/main/kotlin/XMLUtil.kt b/ebics/src/main/kotlin/XMLUtil.kt @@ -0,0 +1,560 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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.ebics + +import com.sun.xml.bind.marshaller.NamespacePrefixMapper +import io.ktor.http.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.w3c.dom.Document +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import org.w3c.dom.ls.LSInput +import org.w3c.dom.ls.LSResourceResolver +import org.xml.sax.ErrorHandler +import org.xml.sax.InputSource +import org.xml.sax.SAXException +import org.xml.sax.SAXParseException +import tech.libeufin.ebics.ebics_h004.EbicsResponse +import java.io.* +import java.security.PrivateKey +import java.security.PublicKey +import java.security.interfaces.RSAPrivateCrtKey +import javax.xml.XMLConstants +import javax.xml.bind.JAXBContext +import javax.xml.bind.JAXBElement +import javax.xml.bind.Marshaller +import javax.xml.crypto.* +import javax.xml.crypto.dom.DOMURIReference +import javax.xml.crypto.dsig.* +import javax.xml.crypto.dsig.dom.DOMSignContext +import javax.xml.crypto.dsig.dom.DOMValidateContext +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec +import javax.xml.crypto.dsig.spec.TransformParameterSpec +import javax.xml.namespace.NamespaceContext +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.Source +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.transform.stream.StreamSource +import javax.xml.validation.SchemaFactory +import javax.xml.validation.Validator +import javax.xml.xpath.XPath +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + +private val logger: Logger = LoggerFactory.getLogger("libeufin-xml") + +class DefaultNamespaces : NamespacePrefixMapper() { + override fun getPreferredPrefix(namespaceUri: String?, suggestion: String?, requirePrefix: Boolean): String? { + if (namespaceUri == "http://www.w3.org/2000/09/xmldsig#") return "ds" + if (namespaceUri == XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI) return "xsi" + return null + } +} + +class DOMInputImpl : LSInput { + var fPublicId: String? = null + var fSystemId: String? = null + var fBaseSystemId: String? = null + var fByteStream: InputStream? = null + var fCharStream: Reader? = null + var fData: String? = null + var fEncoding: String? = null + var fCertifiedText = false + + override fun getByteStream(): InputStream? { + return fByteStream + } + + override fun setByteStream(byteStream: InputStream) { + fByteStream = byteStream + } + + override fun getCharacterStream(): Reader? { + return fCharStream + } + + override fun setCharacterStream(characterStream: Reader) { + fCharStream = characterStream + } + + override fun getStringData(): String? { + return fData + } + + override fun setStringData(stringData: String) { + fData = stringData + } + + override fun getEncoding(): String? { + return fEncoding + } + + override fun setEncoding(encoding: String) { + fEncoding = encoding + } + + override fun getPublicId(): String? { + return fPublicId + } + + override fun setPublicId(publicId: String) { + fPublicId = publicId + } + + override fun getSystemId(): String? { + return fSystemId + } + + override fun setSystemId(systemId: String) { + fSystemId = systemId + } + + override fun getBaseURI(): String? { + return fBaseSystemId + } + + override fun setBaseURI(baseURI: String) { + fBaseSystemId = baseURI + } + + override fun getCertifiedText(): Boolean { + return fCertifiedText + } + + override fun setCertifiedText(certifiedText: Boolean) { + fCertifiedText = certifiedText + } +} + + +/** + * Helpers for dealing with XML in EBICS. + */ +class XMLUtil private constructor() { + /** + * This URI dereferencer allows handling the resource reference used for + * XML signatures in EBICS. + */ + private class EbicsSigUriDereferencer : URIDereferencer { + override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { + val ebicsXpathExpr = "//*[@authenticate='true']" + if (myRef !is DOMURIReference) + throw Exception("invalid type") + 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 + ) + if (nodeSet !is NodeList) + throw Exception("invalid type") + if (nodeSet.length <= 0) { + throw Exception("no nodes to sign") + } + val nodeList = ArrayList<Node>() + for (i in 0 until nodeSet.length) { + val node = nodeSet.item(i) + nodeList.add(node) + } + return NodeSetData { nodeList.iterator() } + } + } + + companion object { + private var cachedEbicsValidator: Validator? = null + private fun getEbicsValidator(): Validator { + val currentValidator = cachedEbicsValidator + if (currentValidator != null) + return currentValidator + val classLoader = ClassLoader.getSystemClassLoader() + val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") + sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") + sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) + sf.errorHandler = object : ErrorHandler { + override fun warning(p0: SAXParseException?) { + println("Warning: $p0") + } + + override fun error(p0: SAXParseException?) { + println("Error: $p0") + } + + override fun fatalError(p0: SAXParseException?) { + println("Fatal error: $p0") + } + } + sf.resourceResolver = object : LSResourceResolver { + override fun resolveResource( + type: String?, + namespaceURI: String?, + publicId: String?, + systemId: String?, + baseUri: String? + ): LSInput? { + if (type != "http://www.w3.org/2001/XMLSchema") { + return null + } + val res = classLoader.getResourceAsStream("xsd/$systemId") ?: return null + return DOMInputImpl().apply { + fPublicId = publicId + fSystemId = systemId + fBaseSystemId = baseUri + fByteStream = res + fEncoding = "UTF-8" + } + } + } + val schemaInputs: Array<Source> = listOf( + "xsd/ebics_H004.xsd", + "xsd/ebics_H005.xsd", + "xsd/ebics_hev.xsd", + "xsd/camt.052.001.02.xsd", + "xsd/camt.053.001.02.xsd", + "xsd/camt.054.001.02.xsd", + "xsd/pain.001.001.03.xsd", + // "xsd/pain.001.001.03.ch.02.xsd", // Swiss 2013 version. + "xsd/pain.001.001.09.ch.03.xsd" // Swiss 2019 version. + ).map { + val stream = + classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") + StreamSource(stream) + }.toTypedArray() + val bundle = sf.newSchema(schemaInputs) + val newValidator = bundle.newValidator() + cachedEbicsValidator = newValidator + return newValidator + } + + /** + * + * @param xmlDoc the XML document to validate + * @return true when validation passes, false otherwise + */ + @Synchronized fun validate(xmlDoc: StreamSource): Boolean { + try { + getEbicsValidator().validate(xmlDoc) + } catch (e: Exception) { + /** + * Would be convenient to return also the error + * message to the caller, so that it can link it + * to a document ID in the logs. + */ + logger.warn("Validation failed: ${e}") + return false + } + return true; + } + + /** + * Validates the DOM against the Schema(s) of this object. + * @param domDocument DOM to validate + * @return true/false if the document is valid/invalid + */ + @Synchronized fun validateFromDom(domDocument: Document): Boolean { + try { + getEbicsValidator().validate(DOMSource(domDocument)) + } catch (e: SAXException) { + e.printStackTrace() + return false + } + return true + } + + /** + * Craft object to be passed to the XML validator. + * @param xmlString XML body, as read from the POST body. + * @return InputStream object, as wanted by the validator. + */ + fun validateFromString(xmlString: String): Boolean { + val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) + val xmlSource = StreamSource(xmlInputStream) + return validate(xmlSource) + } + + inline fun <reified T> convertJaxbToString( + obj: T, + withSchemaLocation: String? = null + ): String { + val sw = StringWriter() + val jc = JAXBContext.newInstance(T::class.java) + val m = jc.createMarshaller() + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + if (withSchemaLocation != null) { + m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) + } + m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) + m.marshal(obj, sw) + return sw.toString() + } + + inline fun <reified T> convertJaxbToDocument( + obj: T, + withSchemaLocation: String? = null + ): Document { + val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() + dbf.isNamespaceAware = true + val doc = dbf.newDocumentBuilder().newDocument() + val jc = JAXBContext.newInstance(T::class.java) + val m = jc.createMarshaller() + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) + if (withSchemaLocation != null) { + m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) + } + m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) + m.marshal(obj, doc) + return doc + } + + /** + * Convert a XML string to the JAXB representation. + * + * @param documentString the string to convert into JAXB. + * @return the JAXB object reflecting the original XML document. + */ + inline fun <reified T> convertStringToJaxb(documentString: String): JAXBElement<T> { + val jc = JAXBContext.newInstance(T::class.java) + val u = jc.createUnmarshaller() + return u.unmarshal( /* Marshalling the object into the document. */ + StreamSource(StringReader(documentString)), + T::class.java + ) + } + + /** + * Extract String from DOM. + * + * @param document the DOM to extract the string from. + * @return the final String, or null if errors occur. + */ + fun convertDomToString(document: Document): String { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + + /* Make string writer. */ + val sw = StringWriter() + + /* Extract string. */ + t.transform(DOMSource(document), StreamResult(sw)) + return sw.toString() + } + + /** + * Convert a node to a string without the XML declaration or + * indentation. + */ + fun convertNodeToString(node: Node): String { + /* Make Transformer. */ + val tf = TransformerFactory.newInstance() + val t = tf.newTransformer() + t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + /* Make string writer. */ + val sw = StringWriter() + /* Extract string. */ + t.transform(DOMSource(node), StreamResult(sw)) + return sw.toString() + } + + /** + * Convert a DOM document to the JAXB representation. + * + * @param finalType class type of the output + * @param document the document to convert into JAXB. + * @return the JAXB object reflecting the original XML document. + */ + fun <T> convertDomToJaxb(finalType: Class<T>, document: Document): JAXBElement<T> { + val jc = JAXBContext.newInstance(finalType) + /* Marshalling the object into the document. */ + val m = jc.createUnmarshaller() + return m.unmarshal(document, finalType) // document "went" into Jaxb + } + + /** + * Parse string into XML DOM. + * @param xmlString the string to parse. + * @return the DOM representing @a xmlString + */ + fun parseStringIntoDom(xmlString: String): Document { + val factory = DocumentBuilderFactory.newInstance().apply { + isNamespaceAware = true + } + val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) + val builder = factory.newDocumentBuilder() + return builder.parse(InputSource(xmlInputStream)) + } + + fun signEbicsResponse(ebicsResponse: EbicsResponse, privateKey: RSAPrivateCrtKey): String { + val doc = convertJaxbToDocument(ebicsResponse) + signEbicsDocument(doc, privateKey) + val signedDoc = XMLUtil.convertDomToString(doc) + // logger.debug("response: $signedDoc") + return signedDoc + } + + /** + * Sign an EBICS document with the authentication and identity signature. + */ + fun signEbicsDocument( + doc: Document, + signingPriv: PrivateKey, + withEbics3: Boolean = false + ) { + val xpath = XPathFactory.newInstance().newXPath() + xpath.namespaceContext = object : NamespaceContext { + override fun getNamespaceURI(p0: String?): String { + return when (p0) { + "ebics" -> if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" + else -> throw IllegalArgumentException() + } + } + + override fun getPrefix(p0: String?): String { + throw UnsupportedOperationException() + } + + override fun getPrefixes(p0: String?): MutableIterator<String> { + throw UnsupportedOperationException() + } + } + val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val fac = XMLSignatureFactory.getInstance("DOM") + val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) + val ref: Reference = + fac.newReference( + "#xpointer(//*[@authenticate='true'])", + fac.newDigestMethod(DigestMethod.SHA256, null), + listOf(c14n), + null, + null + ) + val canon: CanonicalizationMethod = + fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) + val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null) + val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) + val sig: XMLSignature = fac.newXMLSignature(si, null) + val dsc = DOMSignContext(signingPriv, authSigNode) + dsc.defaultNamespacePrefix = "ds" + dsc.uriDereferencer = EbicsSigUriDereferencer() + dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + sig.sign(dsc) + val innerSig = authSigNode.firstChild + while (innerSig.hasChildNodes()) { + authSigNode.appendChild(innerSig.firstChild) + } + authSigNode.removeChild(innerSig) + } + + fun verifyEbicsDocument( + doc: Document, + signingPub: PublicKey, + withEbics3: Boolean = false + ): Boolean { + val xpath = XPathFactory.newInstance().newXPath() + xpath.namespaceContext = object : NamespaceContext { + override fun getNamespaceURI(p0: String?): String { + return when (p0) { + "ebics" -> if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" + else -> throw IllegalArgumentException() + } + } + + override fun getPrefix(p0: String?): String { + throw UnsupportedOperationException() + } + + override fun getPrefixes(p0: String?): MutableIterator<String> { + throw UnsupportedOperationException() + } + } + val doc2: Document = doc.cloneNode(true) as Document + val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc2, XPathConstants.NODE) + if (authSigNode !is Node) + throw java.lang.Exception("no AuthSignature") + val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") + authSigNode.parentNode.insertBefore(sigEl, authSigNode) + while (authSigNode.hasChildNodes()) { + sigEl.appendChild(authSigNode.firstChild) + } + authSigNode.parentNode.removeChild(authSigNode) + val fac = XMLSignatureFactory.getInstance("DOM") + val dvc = DOMValidateContext(signingPub, sigEl) + dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) + dvc.uriDereferencer = EbicsSigUriDereferencer() + val sig = fac.unmarshalXMLSignature(dvc) + // FIXME: check that parameters are okay! + val valResult = sig.validate(dvc) + sig.signedInfo.references[0].validate(dvc) + return valResult + } + + fun getNodeFromXpath(doc: Document, query: String): Node { + val xpath = XPathFactory.newInstance().newXPath() + val ret = xpath.evaluate(query, doc, XPathConstants.NODE) + ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") + return ret as Node + } + + fun getStringFromXpath(doc: Document, query: String): String { + val xpath = XPathFactory.newInstance().newXPath() + val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String + if (ret.isEmpty()) { + throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") + } + return ret + } + } +} + +fun Document.pickString(xpath: String): String { + return XMLUtil.getStringFromXpath(this, xpath) +} + +fun Document.pickStringWithRootNs(xpathQuery: String): String { + val doc = this + val xpath = XPathFactory.newInstance().newXPath() + xpath.namespaceContext = object : NamespaceContext { + override fun getNamespaceURI(p0: String?): String { + return when (p0) { + "root" -> doc.documentElement.namespaceURI + else -> throw IllegalArgumentException() + } + } + + override fun getPrefix(p0: String?): String { + throw UnsupportedOperationException() + } + + override fun getPrefixes(p0: String?): MutableIterator<String> { + throw UnsupportedOperationException() + } + } + val ret = xpath.evaluate(xpathQuery, this, XPathConstants.STRING) as String + if (ret.isEmpty()) { + throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $xpathQuery") + } + return ret +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/XmlCombinators.kt b/ebics/src/main/kotlin/XmlCombinators.kt @@ -0,0 +1,184 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2020 Taler Systems S.A. + * + * 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.ebics + +import com.sun.xml.txw2.output.IndentingXMLStreamWriter +import org.w3c.dom.Document +import org.w3c.dom.Element +import java.io.StringWriter +import javax.xml.stream.XMLOutputFactory +import javax.xml.stream.XMLStreamWriter + +class XmlElementBuilder(val w: XMLStreamWriter) { + /** + * First consumes all the path's components, and _then_ starts applying f. + */ + fun element(path: MutableList<String>, f: XmlElementBuilder.() -> Unit = {}) { + /* the wanted path got constructed, go on with f's logic now. */ + if (path.isEmpty()) { + f() + return + } + w.writeStartElement(path.removeAt(0)) + 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) + } +} + +class XmlDocumentBuilder { + + private var maybeWriter: XMLStreamWriter? = null + internal var writer: XMLStreamWriter + get() { + val w = maybeWriter + return w ?: throw AssertionError("no writer set") + } + set(w: XMLStreamWriter) { + maybeWriter = w + } + + fun namespace(uri: String) { + writer.setDefaultNamespace(uri) + } + + 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) + elementBuilder.f() + writer.writeEndElement() + } +} + +fun constructXml(indent: Boolean = false, f: XmlDocumentBuilder.() -> Unit): String { + val b = XmlDocumentBuilder() + val factory = XMLOutputFactory.newFactory() + factory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true) + val stream = StringWriter() + var writer = factory.createXMLStreamWriter(stream) + if (indent) { + writer = IndentingXMLStreamWriter(writer) + } + b.writer = writer + /** + * NOTE: commenting out because it wasn't obvious how to output the + * "standalone = 'yes' directive". Manual forge was therefore preferred. + */ + // writer.writeStartDocument() + f(b) + writer.writeEndDocument() + return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n${stream.buffer.toString()}" +} + +class DestructionError(m: String) : Exception(m) + +private fun Element.getChildElements(ns: String, tag: String): List<Element> { + val elements = mutableListOf<Element>() + for (i in 0..this.childNodes.length) { + val el = this.childNodes.item(i) + if (el !is Element) { + continue + } + if (ns != "*" && el.namespaceURI != ns) { + continue + } + if (tag != "*" && el.localName != tag) { + continue + } + elements.add(el) + } + return elements +} + +class XmlElementDestructor internal constructor(val focusElement: Element) { + fun <T> requireOnlyChild(f: XmlElementDestructor.(e: Element) -> T): T { + val children = focusElement.getChildElements("*", "*") + if (children.size != 1) throw DestructionError("expected singleton child tag") + val destr = XmlElementDestructor(children[0]) + return f(destr, children[0]) + } + + fun <T> mapEachChildNamed(s: String, f: XmlElementDestructor.() -> T): List<T> { + val res = mutableListOf<T>() + val els = focusElement.getChildElements("*", s) + for (child in els) { + val destr = XmlElementDestructor(child) + res.add(f(destr)) + } + return res + } + + fun <T> requireUniqueChildNamed(s: String, f: XmlElementDestructor.() -> T): T { + val cl = focusElement.getChildElements("*", s) + if (cl.size != 1) { + throw DestructionError("expected exactly one unique $s child, got ${cl.size} instead at ${focusElement}") + } + val el = cl[0] + val destr = XmlElementDestructor(el) + return f(destr) + } + + fun <T> maybeUniqueChildNamed(s: String, f: XmlElementDestructor.() -> T): T? { + val cl = focusElement.getChildElements("*", s) + if (cl.size > 1) { + throw DestructionError("expected at most one unique $s child, got ${cl.size} instead") + } + if (cl.size == 1) { + val el = cl[0] + val destr = XmlElementDestructor(el) + return f(destr) + } + return null + } +} + +class XmlDocumentDestructor internal constructor(val d: Document) { + fun <T> requireRootElement(name: String, f: XmlElementDestructor.() -> T): T { + if (this.d.documentElement.tagName != name) { + throw DestructionError("expected '$name' tag") + } + val destr = XmlElementDestructor(d.documentElement) + return f(destr) + } +} + +fun <T> destructXml(d: Document, f: XmlDocumentDestructor.() -> T): T { + return f(XmlDocumentDestructor(d)) +} diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsKeyManagementResponse.kt b/ebics/src/main/kotlin/ebics_h004/EbicsKeyManagementResponse.kt @@ -0,0 +1,102 @@ +package tech.libeufin.ebics.ebics_h004 + +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter + + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "body"]) +@XmlRootElement(name = "ebicsKeyManagementResponse") +class EbicsKeyManagementResponse { + @get:XmlElement(required = true) + lateinit var header: Header + + @get:XmlElement(required = true) + lateinit var body: Body + + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["_static", "mutable"]) + class Header { + @get:XmlElement(name = "static", required = true) + lateinit var _static: EmptyStaticHeader + + @get:XmlElement(required = true) + lateinit var mutable: MutableHeaderType + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderID", "returnCode", "reportText"]) + class MutableHeaderType { + @get:XmlElement(name = "OrderID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + var orderID: String? = null + + @get:XmlElement(name = "ReturnCode", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var returnCode: String + + @get:XmlElement(name = "ReportText", required = true) + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + @get:XmlSchemaType(name = "normalizedString") + lateinit var reportText: String + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "") + class EmptyStaticHeader + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) + class Body { + @get:XmlElement(name = "DataTransfer") + var dataTransfer: DataTransfer? = null + + @get:XmlElement(name = "ReturnCode", required = true) + lateinit var returnCode: ReturnCode + + @get:XmlElement(name = "TimestampBankParameter") + var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + class ReturnCode { + @get:XmlValue + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dataEncryptionInfo", "orderData"]) + class DataTransfer { + @get:XmlElement(name = "DataEncryptionInfo") + var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null + + @get:XmlElement(name = "OrderData", required = true) + lateinit var orderData: OrderData + } + + @XmlAccessorType(XmlAccessType.NONE) + class OrderData { + @get:XmlValue + lateinit var value: String + } +} diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsNpkdRequest.kt b/ebics/src/main/kotlin/ebics_h004/EbicsNpkdRequest.kt @@ -0,0 +1,135 @@ +package tech.libeufin.ebics.ebics_h004 + +import org.apache.xml.security.binding.xmldsig.SignatureType +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.XMLGregorianCalendar + + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) +@XmlRootElement(name = "ebicsNoPubKeyDigestsRequest") +class EbicsNpkdRequest { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(name = "header", required = true) + lateinit var header: Header + + @get:XmlElement(name = "AuthSignature", required = true) + lateinit var authSignature: SignatureType + + @get:XmlElement(required = true) + lateinit var body: EmptyBody + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["static", "mutable"]) + class Header { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlElement(name = "static", required = true) + lateinit var static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: EmptyMutableHeader + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "StaticHeader", + propOrder = ["hostID", "nonce", "timestamp", "partnerID", "userID", "systemID", "product", "orderDetails", "securityMedium"] + ) + class StaticHeaderType { + @get:XmlElement(name = "HostID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var hostID: String + + @get:XmlElement(name = "Nonce", type = String::class) + @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) + @get:XmlSchemaType(name = "hexBinary") + lateinit var nonce: ByteArray + + @get:XmlElement(name = "Timestamp") + @get:XmlSchemaType(name = "dateTime") + var timestamp: XMLGregorianCalendar? = null + + @get:XmlElement(name = "PartnerID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var partnerID: String + + @get:XmlElement(name = "UserID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var userID: String + + @get:XmlElement(name = "SystemID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var systemID: String? = null + + @get:XmlElement(name = "Product") + val product: EbicsTypes.Product? = null + + @get:XmlElement(name = "OrderDetails", required = true) + lateinit var orderDetails: OrderDetails + + @get:XmlElement(name = "SecurityMedium", required = true) + lateinit var securityMedium: String + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderType", "orderAttribute"]) + class OrderDetails { + @get:XmlElement(name = "OrderType", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var orderType: String + + @get:XmlElement(name = "OrderAttribute", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var orderAttribute: String + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "") + class EmptyMutableHeader + + @XmlAccessorType(XmlAccessType.NONE) + class EmptyBody + + companion object { + fun createRequest( + hostId: String, + partnerId: String, + userId: String, + aNonce: ByteArray, + date: XMLGregorianCalendar + ): EbicsNpkdRequest { + return EbicsNpkdRequest().apply { + version = "H004" + revision = 1 + header = Header().apply { + authenticate = true + mutable = EmptyMutableHeader() + static = StaticHeaderType().apply { + hostID = hostId + partnerID = partnerId + userID = userId + securityMedium = "0000" + orderDetails = OrderDetails() + orderDetails.orderType = "HPB" + orderDetails.orderAttribute = "DZHNN" + nonce = aNonce + timestamp = date + } + } + body = EmptyBody() + authSignature = SignatureType() + } + } + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt b/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -0,0 +1,505 @@ +package tech.libeufin.ebics.ebics_h004 + +import org.apache.xml.security.binding.xmldsig.SignatureType +import tech.libeufin.common.CryptoUtil +import java.math.BigInteger +import java.security.interfaces.RSAPublicKey +import java.util.* +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.XMLGregorianCalendar + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) +@XmlRootElement(name = "ebicsRequest") +class EbicsRequest { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(name = "header", required = true) + lateinit var header: Header + + @get:XmlElement(name = "AuthSignature", required = true) + lateinit var authSignature: SignatureType + + @get:XmlElement(name = "body") + lateinit var body: Body + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["static", "mutable"]) + class Header { + @get:XmlElement(name = "static", required = true) + lateinit var static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: MutableHeader + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = [ + "hostID", "nonce", "timestamp", "partnerID", "userID", "systemID", + "product", "orderDetails", "bankPubKeyDigests", "securityMedium", + "numSegments", "transactionID" + ] + ) + class StaticHeaderType { + @get:XmlElement(name = "HostID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var hostID: String + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "Nonce", type = String::class) + @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) + @get:XmlSchemaType(name = "hexBinary") + var nonce: ByteArray? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "Timestamp") + @get:XmlSchemaType(name = "dateTime") + var timestamp: XMLGregorianCalendar? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "PartnerID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var partnerID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "UserID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var userID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "SystemID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var systemID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "Product") + var product: EbicsTypes.Product? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "OrderDetails") + var orderDetails: OrderDetails? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "BankPubKeyDigests") + var bankPubKeyDigests: BankPubKeyDigests? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "SecurityMedium") + var securityMedium: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "NumSegments") + var numSegments: BigInteger? = null + + /** + * Present only in the transaction / finalization phase. + */ + @get:XmlElement(name = "TransactionID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var transactionID: String? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["transactionPhase", "segmentNumber"]) + class MutableHeader { + @get:XmlElement(name = "TransactionPhase", required = true) + @get:XmlSchemaType(name = "token") + lateinit var transactionPhase: EbicsTypes.TransactionPhaseType + + /** + * Number of the currently transmitted segment, if this message + * contains order data. + */ + @get:XmlElement(name = "SegmentNumber") + var segmentNumber: EbicsTypes.SegmentNumber? = null + + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["orderType", "orderID", "orderAttribute", "orderParams"] + ) + class OrderDetails { + @get:XmlElement(name = "OrderType", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var orderType: String + + /** + * Only present if this ebicsRequest is an upload order + * relating to an already existing order. + */ + @get:XmlElement(name = "OrderID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var orderID: String? = null + + @get:XmlElement(name = "OrderAttribute", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var orderAttribute: String + + /** + * Present only in the initialization phase. + */ + @get:XmlElements( + XmlElement( + name = "StandardOrderParams", + type = StandardOrderParams::class + ), + XmlElement( + name = "GenericOrderParams", + type = GenericOrderParams::class + ) + ) + var orderParams: OrderParams? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["preValidation", "dataTransfer", "transferReceipt"]) + class Body { + @get:XmlElement(name = "PreValidation") + var preValidation: PreValidation? = null + + @get:XmlElement(name = "DataTransfer") + var dataTransfer: DataTransfer? = null + + @get:XmlElement(name = "TransferReceipt") + var transferReceipt: TransferReceipt? = null + } + + /** + * FIXME: not implemented yet + */ + @XmlAccessorType(XmlAccessType.NONE) + class PreValidation { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + class SignatureData { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlValue + var value: ByteArray? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "orderData", "hostId"]) + class DataTransfer { + + @get:XmlElement(name = "DataEncryptionInfo") + var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null + + @get:XmlElement(name = "SignatureData") + var signatureData: SignatureData? = null + + @get:XmlElement(name = "OrderData") + var orderData: String? = null + + @get:XmlElement(name = "HostID") + var hostId: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["receiptCode"]) + class TransferReceipt { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlElement(name = "ReceiptCode") + var receiptCode: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + abstract class OrderParams + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dateRange"]) + class StandardOrderParams : OrderParams() { + @get:XmlElement(name = "DateRange") + var dateRange: DateRange? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["parameterList"]) + class GenericOrderParams : OrderParams() { + @get:XmlElement(type = EbicsTypes.Parameter::class) + var parameterList: List<EbicsTypes.Parameter> = LinkedList() + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["start", "end"]) + class DateRange { + @get:XmlElement(name = "Start") + @get:XmlSchemaType(name = "date") + lateinit var start: XMLGregorianCalendar + + @get:XmlElement(name = "End") + @get:XmlSchemaType(name = "date") + lateinit var end: XMLGregorianCalendar + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["authentication", "encryption"]) + class BankPubKeyDigests { + @get:XmlElement(name = "Authentication") + lateinit var authentication: EbicsTypes.PubKeyDigest + + @get:XmlElement(name = "Encryption") + lateinit var encryption: EbicsTypes.PubKeyDigest + } + + companion object { + + fun createForDownloadReceiptPhase( + transactionId: String?, + hostId: String + + ): EbicsRequest { + return EbicsRequest().apply { + header = Header().apply { + version = "H004" + revision = 1 + authenticate = true + static = StaticHeaderType().apply { + hostID = hostId + transactionID = transactionId + } + mutable = MutableHeader().apply { + transactionPhase = EbicsTypes.TransactionPhaseType.RECEIPT + } + } + authSignature = SignatureType() + + body = Body().apply { + transferReceipt = TransferReceipt().apply { + authenticate = true + receiptCode = 0 // always true at this point. + } + } + } + } + + fun createForDownloadInitializationPhase( + userId: String, + partnerId: String, + hostId: String, + nonceArg: ByteArray, + date: XMLGregorianCalendar, + bankEncPub: RSAPublicKey, + bankAuthPub: RSAPublicKey, + myOrderType: String, + myOrderParams: OrderParams + ): EbicsRequest { + return EbicsRequest().apply { + version = "H004" + revision = 1 + authSignature = SignatureType() + body = Body() + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + userID = userId + partnerID = partnerId + hostID = hostId + nonce = nonceArg + timestamp = date + partnerID = partnerId + orderDetails = OrderDetails().apply { + orderType = myOrderType + orderAttribute = "DZHNN" + orderParams = myOrderParams + } + bankPubKeyDigests = BankPubKeyDigests().apply { + authentication = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "X002" + value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) + } + encryption = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) + } + securityMedium = "0000" + } + } + mutable = MutableHeader().apply { + transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + } + } + } + } + + fun createForUploadInitializationPhase( + encryptedTransactionKey: ByteArray, + encryptedSignatureData: ByteArray, + hostId: String, + nonceArg: ByteArray, + partnerId: String, + userId: String, + date: XMLGregorianCalendar, + bankAuthPub: RSAPublicKey, + bankEncPub: RSAPublicKey, + segmentsNumber: BigInteger, + aOrderType: String, + aOrderParams: OrderParams + ): EbicsRequest { + + return EbicsRequest().apply { + header = Header().apply { + version = "H004" + revision = 1 + authenticate = true + static = StaticHeaderType().apply { + hostID = hostId + nonce = nonceArg + timestamp = date + partnerID = partnerId + userID = userId + orderDetails = OrderDetails().apply { + orderType = aOrderType + orderAttribute = "OZHNN" + orderParams = aOrderParams + } + bankPubKeyDigests = BankPubKeyDigests().apply { + authentication = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "X002" + value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) + } + encryption = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) + } + } + securityMedium = "0000" + numSegments = segmentsNumber + } + mutable = MutableHeader().apply { + transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + } + } + authSignature = SignatureType() + body = Body().apply { + dataTransfer = DataTransfer().apply { + signatureData = SignatureData().apply { + authenticate = true + value = encryptedSignatureData + } + dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + transactionKey = encryptedTransactionKey + authenticate = true + encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) + } + } + } + } + } + } + + fun createForUploadTransferPhase( + hostId: String, + transactionId: String?, + segNumber: BigInteger, + encryptedData: String + ): EbicsRequest { + return EbicsRequest().apply { + header = Header().apply { + version = "H004" + revision = 1 + authenticate = true + static = StaticHeaderType().apply { + hostID = hostId + transactionID = transactionId + } + mutable = MutableHeader().apply { + transactionPhase = EbicsTypes.TransactionPhaseType.TRANSFER + segmentNumber = EbicsTypes.SegmentNumber().apply { + lastSegment = true + value = segNumber + } + } + } + + authSignature = SignatureType() + body = Body().apply { + dataTransfer = DataTransfer().apply { + orderData = encryptedData + } + } + } + } + + fun createForDownloadTransferPhase( + hostID: String, + transactionID: String?, + segmentNumber: Int, + numSegments: Int + ): EbicsRequest { + return EbicsRequest().apply { + version = "H004" + revision = 1 + authSignature = SignatureType() + body = Body() + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + this.hostID = hostID + this.transactionID = transactionID + } + mutable = MutableHeader().apply { + transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + this.lastSegment = segmentNumber == numSegments + } + } + } + } + } + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsResponse.kt b/ebics/src/main/kotlin/ebics_h004/EbicsResponse.kt @@ -0,0 +1,350 @@ +package tech.libeufin.ebics.ebics_h004 + +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.apache.xml.security.binding.xmldsig.SignedInfoType +import tech.libeufin.common.CryptoUtil +import tech.libeufin.ebics.XMLUtil +import java.math.BigInteger +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import kotlin.math.min + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) +@XmlRootElement(name = "ebicsResponse") +class EbicsResponse { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(required = true) + lateinit var header: Header + + @get:XmlElement(name = "AuthSignature", required = true) + lateinit var authSignature: SignatureType + + @get:XmlElement(required = true) + lateinit var body: Body + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["_static", "mutable"]) + class Header { + @get:XmlElement(name = "static", required = true) + lateinit var _static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: MutableHeaderType + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) + class Body { + @get:XmlElement(name = "DataTransfer") + var dataTransfer: DataTransferResponseType? = null + + @get:XmlElement(name = "ReturnCode", required = true) + lateinit var returnCode: ReturnCode + + @get:XmlElement(name = "TimestampBankParameter") + var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["transactionPhase", "segmentNumber", "orderID", "returnCode", "reportText"] + ) + class MutableHeaderType { + @get:XmlElement(name = "TransactionPhase", required = true) + @get:XmlSchemaType(name = "token") + lateinit var transactionPhase: EbicsTypes.TransactionPhaseType + + @get:XmlElement(name = "SegmentNumber") + var segmentNumber: EbicsTypes.SegmentNumber? = null + + @get:XmlElement(name = "OrderID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + var orderID: String? = null + + @get:XmlElement(name = "ReturnCode", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var returnCode: String + + @get:XmlElement(name = "ReportText", required = true) + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + @get:XmlSchemaType(name = "normalizedString") + lateinit var reportText: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class OrderData { + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class ReturnCode { + @get:XmlValue + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"]) + class DataTransferResponseType { + @get:XmlElement(name = "DataEncryptionInfo") + var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null + + @get:XmlElement(name = "OrderData", required = true) + lateinit var orderData: OrderData + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) + class StaticHeaderType { + @get:XmlElement(name = "TransactionID") + var transactionID: String? = null + + @get:XmlElement(name = "NumSegments") + @get:XmlSchemaType(name = "positiveInteger") + var numSegments: BigInteger? = null + } + + companion object { + + fun createForUploadWithError( + errorText: String, errorCode: String, phase: EbicsTypes.TransactionPhaseType + ): EbicsResponse { + val resp = EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = EbicsResponse.Header().apply { + this.authenticate = true + this.mutable = EbicsResponse.MutableHeaderType().apply { + this.reportText = errorText + this.returnCode = errorCode + this.transactionPhase = phase + } + _static = EbicsResponse.StaticHeaderType() + } + this.authSignature = SignatureType() + this.body = EbicsResponse.Body().apply { + this.returnCode = EbicsResponse.ReturnCode().apply { + this.authenticate = true + this.value = errorCode + } + } + } + return resp + } + + fun createForUploadInitializationPhase(transactionID: String, orderID: String): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = 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 = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForUploadTransferPhase( + transactionID: String, + segmentNumber: Int, + lastSegment: Boolean, + orderID: String + ): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + if (lastSegment) { + this.lastSegment = true + } + } + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + /** + * @param requestedSegment requested segment as a 1-based index + */ + fun createForDownloadTransferPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + encodedData: String, + requestedSegment: Int + ): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.lastSegment = numSegments == requestedSegment + this.value = BigInteger.valueOf(requestedSegment.toLong()) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = DataTransferResponseType().apply { + this.orderData = OrderData().apply { + val start = segmentSize * (requestedSegment - 1) + this.value = encodedData.substring(start, min(start + segmentSize, encodedData.length)) + } + } + } + } + } + + fun createForDownloadInitializationPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + enc: CryptoUtil.EncryptionResult, + encodedData: String + ): EbicsResponse { + return EbicsResponse().apply { + this.version = "H004" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.lastSegment = (numSegments == 1) + this.value = BigInteger.valueOf(1) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = 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 = OrderData().apply { + this.value = encodedData.substring(0, min(segmentSize, encodedData.length)) + } + } + } + } + } + } +} diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsTypes.kt b/ebics/src/main/kotlin/ebics_h004/EbicsTypes.kt @@ -0,0 +1,402 @@ +/* + * 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.ebics.ebics_h004 + +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.w3c.dom.Element +import java.math.BigInteger +import java.util.* +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import javax.xml.datatype.XMLGregorianCalendar + + +/** + * EBICS type definitions that are shared between other requests / responses / order types. + */ +object EbicsTypes { + /** + * EBICS client product. Identifies the software that accesses the EBICS host. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "Product", propOrder = ["value"]) + class Product { + @get:XmlValue + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "Language", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var language: String + + @get:XmlAttribute(name = "InstituteID") + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + var instituteID: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) + class SegmentNumber { + @XmlValue + lateinit var value: BigInteger + + @XmlAttribute(name = "lastSegment") + var lastSegment: Boolean? = null + } + + + @XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"]) + @XmlAccessorType(XmlAccessType.NONE) + class DataEncryptionInfo { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlElement(name = "EncryptionPubKeyDigest", required = true) + lateinit var encryptionPubKeyDigest: PubKeyDigest + + @get:XmlElement(name = "TransactionKey", required = true) + lateinit var transactionKey: ByteArray + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) + class PubKeyDigest { + /** + * Version of the *digest* of the public key. + */ + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @XmlAttribute(name = "Algorithm", required = true) + @XmlSchemaType(name = "anyURI") + lateinit var algorithm: String + + @get:XmlValue + lateinit var value: ByteArray + } + + @Suppress("UNUSED_PARAMETER") + enum class TransactionPhaseType(value: String) { + @XmlEnumValue("Initialisation") + INITIALISATION("Initialisation"), + + /** + * Auftragsdatentransfer + * + */ + @XmlEnumValue("Transfer") + TRANSFER("Transfer"), + + /** + * Quittungstransfer + * + */ + @XmlEnumValue("Receipt") + RECEIPT("Receipt"); + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "") + class TimestampBankParameter { + @get:XmlValue + lateinit var value: XMLGregorianCalendar + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + + + @XmlType( + name = "PubKeyValueType", propOrder = [ + "rsaKeyValue", + "timeStamp" + ] + ) + @XmlAccessorType(XmlAccessType.NONE) + class PubKeyValueType { + @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) + lateinit var rsaKeyValue: RSAKeyValueType + + @get:XmlElement(name = "TimeStamp", required = false) + @get:XmlSchemaType(name = "dateTime") + var timeStamp: XMLGregorianCalendar? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "AuthenticationPubKeyInfoType", propOrder = [ + "x509Data", + "pubKeyValue", + "authenticationVersion" + ] + ) + class AuthenticationPubKeyInfoType { + @get:XmlAnyElement() + var x509Data: Element? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "AuthenticationVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var authenticationVersion: String + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "EncryptionPubKeyInfoType", propOrder = [ + "x509Data", + "pubKeyValue", + "encryptionVersion" + ] + ) + class EncryptionPubKeyInfoType { + @get:XmlAnyElement() + var x509Data: Element? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "EncryptionVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var encryptionVersion: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class FileFormatType { + @get:XmlAttribute(name = "CountryCode") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var language: String + + @get:XmlValue + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + lateinit var value: String + } + + /** + * Generic key-value pair. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["name", "value"]) + class Parameter { + @get:XmlAttribute(name = "Type", required = true) + lateinit var type: String + + @get:XmlElement(name = "Name", required = true) + lateinit var name: String + + @get:XmlElement(name = "Value", required = true) + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["addressInfo", "bankInfo", "accountInfoList", "orderInfoList"]) + class PartnerInfo { + @get:XmlElement(name = "AddressInfo", required = true) + lateinit var addressInfo: AddressInfo + + @get:XmlElement(name = "BankInfo", required = true) + lateinit var bankInfo: BankInfo + + @get:XmlElement(name = "AccountInfo", type = AccountInfo::class) + var accountInfoList: List<AccountInfo>? = LinkedList<AccountInfo>() + + @get:XmlElement(name = "OrderInfo", type = AuthOrderInfoType::class) + var orderInfoList: List<AuthOrderInfoType> = LinkedList<AuthOrderInfoType>() + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["orderType", "fileFormat", "transferType", "orderFormat", "description", "numSigRequired"] + ) + class AuthOrderInfoType { + @get:XmlElement(name = "OrderType") + lateinit var orderType: String + + @get:XmlElement(name = "FileFormat") + val fileFormat: FileFormatType? = null + + @get:XmlElement(name = "TransferType") + lateinit var transferType: String + + @get:XmlElement(name = "OrderFormat", required = false) + var orderFormat: String? = null + + @get:XmlElement(name = "Description") + lateinit var description: String + + @get:XmlElement(name = "NumSigRequired") + var numSigRequired: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + class UserIDType { + @get:XmlValue + lateinit var value: String; + + @get:XmlAttribute(name = "Status") + var status: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["userID", "name", "permissionList"]) + class UserInfo { + @get:XmlElement(name = "UserID", required = true) + lateinit var userID: UserIDType + + @get:XmlElement(name = "Name") + var name: String? = null + + @get:XmlElement(name = "Permission", type = UserPermission::class) + var permissionList: List<UserPermission>? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderTypes", "fileFormat", "accountID", "maxAmount"]) + class UserPermission { + @get:XmlAttribute(name = "AuthorizationLevel") + var authorizationLevel: String? = null + + @get:XmlElement(name = "OrderTypes") + var orderTypes: String? = null + + @get:XmlElement(name = "FileFormat") + val fileFormat: FileFormatType? = null + + @get:XmlElement(name = "AccountID") + val accountID: String? = null + + @get:XmlElement(name = "MaxAmount") + val maxAmount: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["name", "street", "postCode", "city", "region", "country"]) + class AddressInfo { + @get:XmlElement(name = "Name") + var name: String? = null + + @get:XmlElement(name = "Street") + var street: String? = null + + @get:XmlElement(name = "PostCode") + var postCode: String? = null + + @get:XmlElement(name = "City") + var city: String? = null + + @get:XmlElement(name = "Region") + var region: String? = null + + @get:XmlElement(name = "Country") + var country: String? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + class BankInfo { + @get:XmlElement(name = "HostID") + lateinit var hostID: String + + @get:XmlElement(type = Parameter::class) + var parameters: List<Parameter>? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["accountNumberList", "bankCodeList", "accountHolder"]) + class AccountInfo { + @get:XmlAttribute(name = "Currency") + var currency: String? = null + + @get:XmlAttribute(name = "ID") + lateinit var id: String + + @get:XmlAttribute(name = "Description") + var description: String? = null + + @get:XmlElements( + XmlElement(name = "AccountNumber", type = GeneralAccountNumber::class), + XmlElement(name = "NationalAccountNumber", type = NationalAccountNumber::class) + ) + var accountNumberList: List<AbstractAccountNumber>? = LinkedList<AbstractAccountNumber>() + + @get:XmlElements( + XmlElement(name = "BankCode", type = GeneralBankCode::class), + XmlElement(name = "NationalBankCode", type = NationalBankCode::class) + ) + var bankCodeList: List<AbstractBankCode>? = LinkedList<AbstractBankCode>() + + @get:XmlElement(name = "AccountHolder") + var accountHolder: String? = null + } + + interface AbstractAccountNumber + + @XmlAccessorType(XmlAccessType.NONE) + class GeneralAccountNumber : AbstractAccountNumber { + @get:XmlAttribute(name = "international") + var international: Boolean = true + + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class NationalAccountNumber : AbstractAccountNumber { + @get:XmlAttribute(name = "format") + lateinit var format: String + + @get:XmlValue + lateinit var value: String + } + + interface AbstractBankCode + + @XmlAccessorType(XmlAccessType.NONE) + class GeneralBankCode : AbstractBankCode { + @get:XmlAttribute(name = "prefix") + var prefix: String? = null + + @get:XmlAttribute(name = "international") + var international: Boolean = true + + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class NationalBankCode : AbstractBankCode { + @get:XmlValue + lateinit var value: String + + @get:XmlAttribute(name = "format") + lateinit var format: String + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsUnsecuredRequest.kt b/ebics/src/main/kotlin/ebics_h004/EbicsUnsecuredRequest.kt @@ -0,0 +1,223 @@ +package tech.libeufin.ebics.ebics_h004 + +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import tech.libeufin.ebics.EbicsOrderUtil +import tech.libeufin.ebics.ebics_s001.SignatureTypes +import java.security.interfaces.RSAPrivateCrtKey +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "body"]) +@XmlRootElement(name = "ebicsUnsecuredRequest") +class EbicsUnsecuredRequest { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(name = "header", required = true) + lateinit var header: Header + + @get:XmlElement(required = true) + lateinit var body: Body + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["static", "mutable"]) + class Header { + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "") + class EmptyMutableHeader + + @get:XmlElement(name = "static", required = true) + lateinit var static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: EmptyMutableHeader + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dataTransfer"]) + class Body { + @get:XmlElement(name = "DataTransfer", required = true) + lateinit var dataTransfer: UnsecuredDataTransfer + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderData"]) + class UnsecuredDataTransfer { + @get:XmlElement(name = "OrderData", required = true) + lateinit var orderData: OrderData + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "") + class OrderData { + @get:XmlValue + lateinit var value: ByteArray + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["hostID", "partnerID", "userID", "systemID", "product", "orderDetails", "securityMedium"] + ) + class StaticHeaderType { + @get:XmlElement(name = "HostID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var hostID: String + + @get:XmlElement(name = "PartnerID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var partnerID: String + + @get:XmlElement(name = "UserID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var userID: String + + @get:XmlElement(name = "SystemID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var systemID: String? = null + + @get:XmlElement(name = "Product") + val product: EbicsTypes.Product? = null + + @get:XmlElement(name = "OrderDetails", required = true) + lateinit var orderDetails: OrderDetails + + @get:XmlElement(name = "SecurityMedium", required = true) + lateinit var securityMedium: String + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderType", "orderAttribute"]) + class OrderDetails { + @get:XmlElement(name = "OrderType", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var orderType: String + + @get:XmlElement(name = "OrderAttribute", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var orderAttribute: String + } + + companion object { + + fun createHia( + hostId: String, + userId: String, + partnerId: String, + authKey: RSAPrivateCrtKey, + encKey: RSAPrivateCrtKey + + ): EbicsUnsecuredRequest { + + return EbicsUnsecuredRequest().apply { + + version = "H004" + revision = 1 + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + orderDetails = OrderDetails().apply { + orderAttribute = "DZNNN" + orderType = "HIA" + securityMedium = "0000" + hostID = hostId + userID = userId + partnerID = partnerId + } + } + mutable = Header.EmptyMutableHeader() + } + body = Body().apply { + dataTransfer = UnsecuredDataTransfer().apply { + orderData = OrderData().apply { + value = EbicsOrderUtil.encodeOrderDataXml( + HIARequestOrderData().apply { + authenticationPubKeyInfo = EbicsTypes.AuthenticationPubKeyInfoType() + .apply { + pubKeyValue = EbicsTypes.PubKeyValueType().apply { + rsaKeyValue = RSAKeyValueType().apply { + exponent = authKey.publicExponent.toByteArray() + modulus = authKey.modulus.toByteArray() + } + } + authenticationVersion = "X002" + } + encryptionPubKeyInfo = EbicsTypes.EncryptionPubKeyInfoType() + .apply { + pubKeyValue = EbicsTypes.PubKeyValueType().apply { + rsaKeyValue = RSAKeyValueType().apply { + exponent = encKey.publicExponent.toByteArray() + modulus = encKey.modulus.toByteArray() + } + } + encryptionVersion = "E002" + + } + partnerID = partnerId + userID = userId + } + ) + } + } + } + } + } + + fun createIni( + hostId: String, + userId: String, + partnerId: String, + signKey: RSAPrivateCrtKey + + ): EbicsUnsecuredRequest { + return EbicsUnsecuredRequest().apply { + version = "H004" + revision = 1 + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + orderDetails = OrderDetails().apply { + orderAttribute = "DZNNN" + orderType = "INI" + securityMedium = "0000" + hostID = hostId + userID = userId + partnerID = partnerId + } + } + mutable = Header.EmptyMutableHeader() + } + body = Body().apply { + dataTransfer = UnsecuredDataTransfer().apply { + orderData = OrderData().apply { + value = EbicsOrderUtil.encodeOrderDataXml( + SignatureTypes.SignaturePubKeyOrderData().apply { + signaturePubKeyInfo = SignatureTypes.SignaturePubKeyInfoType().apply { + signatureVersion = "A006" + pubKeyValue = SignatureTypes.PubKeyValueType().apply { + rsaKeyValue = org.apache.xml.security.binding.xmldsig.RSAKeyValueType().apply { + exponent = signKey.publicExponent.toByteArray() + modulus = signKey.modulus.toByteArray() + } + } + } + userID = userId + partnerID = partnerId + } + ) + } + } + } + } + } + } +} diff --git a/ebics/src/main/kotlin/ebics_h004/HIARequestOrderData.kt b/ebics/src/main/kotlin/ebics_h004/HIARequestOrderData.kt @@ -0,0 +1,33 @@ +package tech.libeufin.ebics.ebics_h004 + +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter + + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType( + name = "HIARequestOrderDataType", + propOrder = ["authenticationPubKeyInfo", "encryptionPubKeyInfo", "partnerID", "userID", "any"] +) +@XmlRootElement(name = "HIARequestOrderData") +class HIARequestOrderData { + @get:XmlElement(name = "AuthenticationPubKeyInfo", required = true) + lateinit var authenticationPubKeyInfo: EbicsTypes.AuthenticationPubKeyInfoType + + @get:XmlElement(name = "EncryptionPubKeyInfo", required = true) + lateinit var encryptionPubKeyInfo: EbicsTypes.EncryptionPubKeyInfoType + + @get:XmlElement(name = "PartnerID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var partnerID: String + + @get:XmlElement(name = "UserID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var userID: String + + @get:XmlAnyElement(lax = true) + var any: List<Any>? = null +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h004/HKDResponseOrderData.kt b/ebics/src/main/kotlin/ebics_h004/HKDResponseOrderData.kt @@ -0,0 +1,15 @@ +package tech.libeufin.ebics.ebics_h004 + +import java.security.Permission +import javax.xml.bind.annotation.* + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["partnerInfo", "userInfoList"]) +@XmlRootElement(name = "HTDResponseOrderData") +class HKDResponseOrderData { + @get:XmlElement(name = "PartnerInfo", required = true) + lateinit var partnerInfo: EbicsTypes.PartnerInfo + + @get:XmlElement(name = "UserInfo", type = EbicsTypes.UserInfo::class, required = true) + lateinit var userInfoList: List<EbicsTypes.UserInfo> +} diff --git a/ebics/src/main/kotlin/ebics_h004/HPBResponseOrderData.kt b/ebics/src/main/kotlin/ebics_h004/HPBResponseOrderData.kt @@ -0,0 +1,21 @@ +package tech.libeufin.ebics.ebics_h004 + +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter + + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["authenticationPubKeyInfo", "encryptionPubKeyInfo", "hostID"]) +@XmlRootElement(name = "HPBResponseOrderData") +class HPBResponseOrderData { + @get:XmlElement(name = "AuthenticationPubKeyInfo", required = true) + lateinit var authenticationPubKeyInfo: EbicsTypes.AuthenticationPubKeyInfoType + + @get:XmlElement(name = "EncryptionPubKeyInfo", required = true) + lateinit var encryptionPubKeyInfo: EbicsTypes.EncryptionPubKeyInfoType + + @get:XmlElement(name = "HostID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var hostID: String +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h004/HTDResponseOrderData.kt b/ebics/src/main/kotlin/ebics_h004/HTDResponseOrderData.kt @@ -0,0 +1,14 @@ +package tech.libeufin.ebics.ebics_h004 + +import javax.xml.bind.annotation.* + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["partnerInfo", "userInfo"]) +@XmlRootElement(name = "HTDResponseOrderData") +class HTDResponseOrderData { + @get:XmlElement(name = "PartnerInfo", required = true) + lateinit var partnerInfo: EbicsTypes.PartnerInfo + + @get:XmlElement(name = "UserInfo", required = true) + lateinit var userInfo: EbicsTypes.UserInfo +} diff --git a/ebics/src/main/kotlin/ebics_h004/package-info.java b/ebics/src/main/kotlin/ebics_h004/package-info.java @@ -0,0 +1,12 @@ +/** + * This package-info.java file defines the default namespace for the JAXB bindings + * defined in the package. + */ + +@XmlSchema( + namespace = "urn:org:ebics:H004", + elementFormDefault = XmlNsForm.QUALIFIED +) +package tech.libeufin.ebics.ebics_h004; +import javax.xml.bind.annotation.XmlSchema; +import javax.xml.bind.annotation.XmlNsForm; +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt b/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt @@ -0,0 +1,586 @@ +package tech.libeufin.ebics.ebics_h005 + +import org.apache.xml.security.binding.xmldsig.SignatureType +import tech.libeufin.common.CryptoUtil +import java.math.BigInteger +import java.security.interfaces.RSAPublicKey +import java.util.* +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.XMLGregorianCalendar + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) +@XmlRootElement(name = "ebicsRequest") +class Ebics3Request { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(name = "header", required = true) + lateinit var header: Header + + @get:XmlElement(name = "AuthSignature", required = true) + lateinit var authSignature: SignatureType + + @get:XmlElement(name = "body") + lateinit var body: Body + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["static", "mutable"]) + class Header { + @get:XmlElement(name = "static", required = true) + lateinit var static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: MutableHeader + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = [ + "hostID", "nonce", "timestamp", "partnerID", "userID", "systemID", + "product", "orderDetails", "bankPubKeyDigests", "securityMedium", + "numSegments", "transactionID" + ] + ) + class StaticHeaderType { + @get:XmlElement(name = "HostID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var hostID: String + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "Nonce", type = String::class) + @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) + @get:XmlSchemaType(name = "hexBinary") + var nonce: ByteArray? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "Timestamp") + @get:XmlSchemaType(name = "dateTime") + var timestamp: XMLGregorianCalendar? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "PartnerID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var partnerID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "UserID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var userID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "SystemID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var systemID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "Product") + var product: Ebics3Types.Product? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "OrderDetails") + var orderDetails: OrderDetails? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "BankPubKeyDigests") + var bankPubKeyDigests: BankPubKeyDigests? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "SecurityMedium") + var securityMedium: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElement(name = "NumSegments") + var numSegments: BigInteger? = null + + /** + * Present only in the transaction / finalization phase. + */ + @get:XmlElement(name = "TransactionID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var transactionID: String? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["transactionPhase", "segmentNumber"]) + class MutableHeader { + @get:XmlElement(name = "TransactionPhase", required = true) + @get:XmlSchemaType(name = "token") + lateinit var transactionPhase: Ebics3Types.TransactionPhaseType + + /** + * Number of the currently transmitted segment, if this message + * contains order data. + */ + @get:XmlElement(name = "SegmentNumber") + var segmentNumber: Ebics3Types.SegmentNumber? = null + + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["adminOrderType", "btdOrderParams", "btuOrderParams", "orderID", "orderParams"] + ) + class OrderDetails { + @get:XmlElement(name = "AdminOrderType", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var adminOrderType: String + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["serviceName", "scope", "serviceOption", "container", "messageName"]) + class Service { + @get:XmlElement(name = "ServiceName", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var serviceName: String + + @get:XmlElement(name = "Scope", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var scope: String + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) + class MessageName { + @XmlValue + lateinit var value: String + + @XmlAttribute(name = "version") + var version: String? = null + } + + @get:XmlElement(name = "MsgName", required = true) + lateinit var messageName: MessageName + + @get:XmlElement(name = "ServiceOption", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var serviceOption: String? = null + + @XmlAccessorType(XmlAccessType.NONE) + class Container { + @XmlAttribute(name = "containerType") + lateinit var containerType: String + } + + @get:XmlElement(name = "Container", required = true) + var container: Container? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["service", "signatureFlag", "dateRange"]) + class BTUOrderParams { + @get:XmlElement(name = "Service", required = true) + lateinit var service: Service + + /** + * This element activates the ES signature scheme (disabling + * hence the EDS). It *would* admit a @requestEDS attribute, + * but its omission means false. + */ + @get:XmlElement(name = "SignatureFlag", required = true) + var signatureFlag: Boolean = true + + @get:XmlElement(name = "DateRange", required = true) + var dateRange: DateRange? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["service", "dateRange"]) + class BTDOrderParams { + @get:XmlElement(name = "Service", required = true) + lateinit var service: Service + + @get:XmlElement(name = "DateRange", required = true) + var dateRange: DateRange? = null + } + + @get:XmlElement(name = "BTUOrderParams", required = true) + var btuOrderParams: BTUOrderParams? = null + + @get:XmlElement(name = "BTDOrderParams", required = true) + var btdOrderParams: BTDOrderParams? = null + + /** + * Only present if this ebicsRequest is an upload order + * relating to an already existing order. + */ + @get:XmlElement(name = "OrderID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + var orderID: String? = null + + /** + * Present only in the initialization phase. + */ + @get:XmlElements( + XmlElement( + name = "StandardOrderParams", + type = StandardOrderParams::class // OrderParams inheritor + ), + XmlElement( + name = "GenericOrderParams", + type = GenericOrderParams::class // OrderParams inheritor + ) + ) + // Same as the 2.5 version. + var orderParams: OrderParams? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["preValidation", "dataTransfer", "transferReceipt"]) + class Body { + @get:XmlElement(name = "PreValidation") + var preValidation: PreValidation? = null + + @get:XmlElement(name = "DataTransfer") + var dataTransfer: DataTransfer? = null + + @get:XmlElement(name = "TransferReceipt") + var transferReceipt: TransferReceipt? = null + } + + /** + * FIXME: not implemented yet + */ + @XmlAccessorType(XmlAccessType.NONE) + class PreValidation { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + class SignatureData { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlValue + var value: ByteArray? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "dataDigest", "orderData", "hostId"]) + class DataTransfer { + + @get:XmlElement(name = "DataEncryptionInfo") + var dataEncryptionInfo: Ebics3Types.DataEncryptionInfo? = null + + @get:XmlElement(name = "SignatureData") + var signatureData: SignatureData? = null + + @XmlAccessorType(XmlAccessType.NONE) + class DataDigest { + @get:XmlAttribute(name = "SignatureVersion", required = true) + var signatureVersion: String = "A006" + + @get:XmlValue + var value: ByteArray? = null + } + + @get:XmlElement(name = "DataDigest") + var dataDigest: DataDigest? = null + + @get:XmlElement(name = "OrderData") + var orderData: String? = null + + @get:XmlElement(name = "HostID") + var hostId: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["receiptCode"]) + class TransferReceipt { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlElement(name = "ReceiptCode") + var receiptCode: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + abstract class OrderParams + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dateRange"]) + class StandardOrderParams : OrderParams() { + @get:XmlElement(name = "DateRange") + var dateRange: DateRange? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["parameterList"]) + class GenericOrderParams : OrderParams() { + @get:XmlElement(type = Ebics3Types.Parameter::class) + var parameterList: List<Ebics3Types.Parameter> = LinkedList() + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["start", "end"]) + class DateRange { + @get:XmlElement(name = "Start") + @get:XmlSchemaType(name = "date") + lateinit var start: XMLGregorianCalendar + + @get:XmlElement(name = "End") + @get:XmlSchemaType(name = "date") + lateinit var end: XMLGregorianCalendar + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["authentication", "encryption"]) + class BankPubKeyDigests { + @get:XmlElement(name = "Authentication") + lateinit var authentication: Ebics3Types.PubKeyDigest + + @get:XmlElement(name = "Encryption") + lateinit var encryption: Ebics3Types.PubKeyDigest + } + + companion object { + + fun createForDownloadReceiptPhase( + transactionId: String?, + hostId: String + ): Ebics3Request { + return Ebics3Request().apply { + header = Header().apply { + version = "H005" + revision = 1 + authenticate = true + static = StaticHeaderType().apply { + hostID = hostId + transactionID = transactionId + } + mutable = MutableHeader().apply { + transactionPhase = Ebics3Types.TransactionPhaseType.RECEIPT + } + } + authSignature = SignatureType() + + body = Body().apply { + transferReceipt = TransferReceipt().apply { + authenticate = true + receiptCode = 0 // always true at this point. + } + } + } + } + + fun createForDownloadInitializationPhase( + userId: String, + partnerId: String, + hostId: String, + nonceArg: ByteArray, + date: XMLGregorianCalendar, + bankEncPub: RSAPublicKey, + bankAuthPub: RSAPublicKey, + myOrderParams: OrderDetails.BTDOrderParams + ): Ebics3Request { + return Ebics3Request().apply { + version = "H005" + revision = 1 + authSignature = SignatureType() + body = Body() + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + userID = userId + partnerID = partnerId + hostID = hostId + nonce = nonceArg + timestamp = date + partnerID = partnerId + orderDetails = OrderDetails().apply { + this.adminOrderType = "BTD" + this.btdOrderParams = myOrderParams + } + bankPubKeyDigests = BankPubKeyDigests().apply { + authentication = Ebics3Types.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "X002" + value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) + } + encryption = Ebics3Types.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) + } + securityMedium = "0000" + } + } + mutable = MutableHeader().apply { + transactionPhase = + Ebics3Types.TransactionPhaseType.INITIALISATION + } + } + } + } + + fun createForUploadInitializationPhase( + encryptedTransactionKey: ByteArray, + encryptedSignatureData: ByteArray, + aDataDigest: ByteArray, + hostId: String, + nonceArg: ByteArray, + partnerId: String, + userId: String, + date: XMLGregorianCalendar, + bankAuthPub: RSAPublicKey, + bankEncPub: RSAPublicKey, + segmentsNumber: BigInteger, + aOrderService: OrderDetails.Service, + ): Ebics3Request { + + return Ebics3Request().apply { + header = Header().apply { + version = "H005" + revision = 1 + authenticate = true + static = StaticHeaderType().apply { + hostID = hostId + nonce = nonceArg + timestamp = date + partnerID = partnerId + userID = userId + orderDetails = OrderDetails().apply { + this.adminOrderType = "BTU" + this.btuOrderParams = OrderDetails.BTUOrderParams().apply { + service = aOrderService + } + } + bankPubKeyDigests = BankPubKeyDigests().apply { + authentication = Ebics3Types.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "X002" + value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) + } + encryption = Ebics3Types.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) + } + } + securityMedium = "0000" + numSegments = segmentsNumber + } + mutable = MutableHeader().apply { + transactionPhase = + Ebics3Types.TransactionPhaseType.INITIALISATION + } + } + authSignature = SignatureType() + body = Body().apply { + dataTransfer = DataTransfer().apply { + signatureData = SignatureData().apply { + authenticate = true + value = encryptedSignatureData + } + dataDigest = DataTransfer.DataDigest().apply { + value = aDataDigest + } + dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply { + transactionKey = encryptedTransactionKey + authenticate = true + encryptionPubKeyDigest = Ebics3Types.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) + } + } + } + } + } + } + + fun createForUploadTransferPhase( + hostId: String, + transactionId: String?, + segNumber: BigInteger, + encryptedData: String + ): Ebics3Request { + return Ebics3Request().apply { + header = Header().apply { + version = "H005" + revision = 1 + authenticate = true + static = StaticHeaderType().apply { + hostID = hostId + transactionID = transactionId + } + mutable = MutableHeader().apply { + transactionPhase = Ebics3Types.TransactionPhaseType.TRANSFER + segmentNumber = Ebics3Types.SegmentNumber().apply { + lastSegment = true + value = segNumber + } + } + } + + authSignature = SignatureType() + body = Body().apply { + dataTransfer = DataTransfer().apply { + orderData = encryptedData + } + } + } + } + + fun createForDownloadTransferPhase( + hostID: String, + transactionID: String?, + segmentNumber: Int, + numSegments: Int + ): Ebics3Request { + return Ebics3Request().apply { + version = "H005" + revision = 1 + authSignature = SignatureType() + body = Body() + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + this.hostID = hostID + this.transactionID = transactionID + } + mutable = MutableHeader().apply { + transactionPhase = + Ebics3Types.TransactionPhaseType.TRANSFER + this.segmentNumber = Ebics3Types.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + this.lastSegment = segmentNumber == numSegments + } + } + } + } + } + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h005/Ebics3Response.kt b/ebics/src/main/kotlin/ebics_h005/Ebics3Response.kt @@ -0,0 +1,349 @@ +package tech.libeufin.ebics.ebics_h005 + +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.apache.xml.security.binding.xmldsig.SignedInfoType +import tech.libeufin.common.CryptoUtil +import tech.libeufin.ebics.XMLUtil +import tech.libeufin.ebics.ebics_h004.EbicsTypes +import java.math.BigInteger +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import kotlin.math.min + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) +@XmlRootElement(name = "ebicsResponse") +class Ebics3Response { + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @get:XmlAttribute(name = "Revision") + var revision: Int? = null + + @get:XmlElement(required = true) + lateinit var header: Header + + @get:XmlElement(name = "AuthSignature", required = true) + lateinit var authSignature: SignatureType + + @get:XmlElement(required = true) + lateinit var body: Body + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["_static", "mutable"]) + class Header { + @get:XmlElement(name = "static", required = true) + lateinit var _static: StaticHeaderType + + @get:XmlElement(required = true) + lateinit var mutable: MutableHeaderType + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) + class Body { + @get:XmlElement(name = "DataTransfer") + var dataTransfer: DataTransferResponseType? = null + + @get:XmlElement(name = "ReturnCode", required = true) + lateinit var returnCode: ReturnCode + + @get:XmlElement(name = "TimestampBankParameter") + var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["transactionPhase", "segmentNumber", "orderID", "returnCode", "reportText"] + ) + class MutableHeaderType { + @get:XmlElement(name = "TransactionPhase", required = true) + @get:XmlSchemaType(name = "token") + lateinit var transactionPhase: EbicsTypes.TransactionPhaseType + + @get:XmlElement(name = "SegmentNumber") + var segmentNumber: EbicsTypes.SegmentNumber? = null + + @get:XmlElement(name = "OrderID") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + var orderID: String? = null + + @get:XmlElement(name = "ReturnCode", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var returnCode: String + + @get:XmlElement(name = "ReportText", required = true) + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + @get:XmlSchemaType(name = "normalizedString") + lateinit var reportText: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class OrderData { + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class ReturnCode { + @get:XmlValue + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"]) + class DataTransferResponseType { + @get:XmlElement(name = "DataEncryptionInfo") + var dataEncryptionInfo: Ebics3Types.DataEncryptionInfo? = null + + @get:XmlElement(name = "OrderData", required = true) + lateinit var orderData: OrderData + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) + class StaticHeaderType { + @get:XmlElement(name = "TransactionID") + var transactionID: String? = null + + @get:XmlElement(name = "NumSegments") + @get:XmlSchemaType(name = "positiveInteger") + var numSegments: BigInteger? = null + } + + companion object { + + fun createForUploadWithError( + errorText: String, errorCode: String, phase: EbicsTypes.TransactionPhaseType + ): Ebics3Response { + val resp = Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Ebics3Response.Header().apply { + this.authenticate = true + this.mutable = Ebics3Response.MutableHeaderType().apply { + this.reportText = errorText + this.returnCode = errorCode + this.transactionPhase = phase + } + _static = Ebics3Response.StaticHeaderType() + } + this.authSignature = SignatureType() + this.body = Ebics3Response.Body().apply { + this.returnCode = Ebics3Response.ReturnCode().apply { + this.authenticate = true + this.value = errorCode + } + } + } + return resp + } + + fun createForUploadInitializationPhase(transactionID: String, orderID: String): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = 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 = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + fun createForUploadTransferPhase( + transactionID: String, + segmentNumber: Int, + lastSegment: Boolean, + orderID: String + ): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + if (lastSegment) { + this.lastSegment = true + } + } + this.orderID = orderID + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + } + } + } + + /** + * @param requestedSegment requested segment as a 1-based index + */ + fun createForDownloadTransferPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + encodedData: String, + requestedSegment: Int + ): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.lastSegment = numSegments == requestedSegment + this.value = BigInteger.valueOf(requestedSegment.toLong()) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = DataTransferResponseType().apply { + this.orderData = OrderData().apply { + val start = segmentSize * (requestedSegment - 1) + this.value = encodedData.substring(start, min(start + segmentSize, encodedData.length)) + } + } + } + } + } + fun createForDownloadInitializationPhase( + transactionID: String, + numSegments: Int, + segmentSize: Int, + enc: CryptoUtil.EncryptionResult, + encodedData: String + ): Ebics3Response { + return Ebics3Response().apply { + this.version = "H005" + this.revision = 1 + this.header = Header().apply { + this.authenticate = true + this._static = StaticHeaderType().apply { + this.transactionID = transactionID + this.numSegments = BigInteger.valueOf(numSegments.toLong()) + } + this.mutable = MutableHeaderType().apply { + this.transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.lastSegment = (numSegments == 1) + this.value = BigInteger.valueOf(1) + } + this.reportText = "[EBICS_OK] OK" + this.returnCode = "000000" + } + } + this.authSignature = SignatureType() + this.body = Body().apply { + this.returnCode = ReturnCode().apply { + this.authenticate = true + this.value = "000000" + } + this.dataTransfer = DataTransferResponseType().apply { + this.dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply { + this.authenticate = true + this.encryptionPubKeyDigest = Ebics3Types.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 = OrderData().apply { + this.value = encodedData.substring(0, min(segmentSize, encodedData.length)) + } + } + } + } + } + } +} diff --git a/ebics/src/main/kotlin/ebics_h005/Ebics3Types.kt b/ebics/src/main/kotlin/ebics_h005/Ebics3Types.kt @@ -0,0 +1,401 @@ +/* + * 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.ebics.ebics_h005 + +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.w3c.dom.Element +import java.math.BigInteger +import java.util.* +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import javax.xml.datatype.XMLGregorianCalendar + + +/** + * EBICS type definitions that are shared between other requests / responses / order types. + */ +object Ebics3Types { + /** + * EBICS client product. Identifies the software that accesses the EBICS host. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "Product", propOrder = ["value"]) + class Product { + @get:XmlValue + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "Language", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var language: String + + @get:XmlAttribute(name = "InstituteID") + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + var instituteID: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) + class SegmentNumber { + @XmlValue + lateinit var value: BigInteger + + @XmlAttribute(name = "lastSegment") + var lastSegment: Boolean? = null + } + + @XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"]) + @XmlAccessorType(XmlAccessType.NONE) + class DataEncryptionInfo { + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + + @get:XmlElement(name = "EncryptionPubKeyDigest", required = true) + lateinit var encryptionPubKeyDigest: PubKeyDigest + + @get:XmlElement(name = "TransactionKey", required = true) + lateinit var transactionKey: ByteArray + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["value"]) + class PubKeyDigest { + /** + * Version of the *digest* of the public key. + */ + @get:XmlAttribute(name = "Version", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var version: String + + @XmlAttribute(name = "Algorithm", required = true) + @XmlSchemaType(name = "anyURI") + lateinit var algorithm: String + + @get:XmlValue + lateinit var value: ByteArray + } + + @Suppress("UNUSED_PARAMETER") + enum class TransactionPhaseType(value: String) { + @XmlEnumValue("Initialisation") + INITIALISATION("Initialisation"), + + /** + * Auftragsdatentransfer + * + */ + @XmlEnumValue("Transfer") + TRANSFER("Transfer"), + + /** + * Quittungstransfer + * + */ + @XmlEnumValue("Receipt") + RECEIPT("Receipt"); + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "") + class TimestampBankParameter { + @get:XmlValue + lateinit var value: XMLGregorianCalendar + + @get:XmlAttribute(name = "authenticate", required = true) + var authenticate: Boolean = false + } + + + + @XmlType( + name = "PubKeyValueType", propOrder = [ + "rsaKeyValue", + "timeStamp" + ] + ) + @XmlAccessorType(XmlAccessType.NONE) + class PubKeyValueType { + @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) + lateinit var rsaKeyValue: RSAKeyValueType + + @get:XmlElement(name = "TimeStamp", required = false) + @get:XmlSchemaType(name = "dateTime") + var timeStamp: XMLGregorianCalendar? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "AuthenticationPubKeyInfoType", propOrder = [ + "x509Data", + "pubKeyValue", + "authenticationVersion" + ] + ) + class AuthenticationPubKeyInfoType { + @get:XmlAnyElement() + var x509Data: Element? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "AuthenticationVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var authenticationVersion: String + } + + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "EncryptionPubKeyInfoType", propOrder = [ + "x509Data", + "pubKeyValue", + "encryptionVersion" + ] + ) + class EncryptionPubKeyInfoType { + @get:XmlAnyElement() + var x509Data: Element? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "EncryptionVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var encryptionVersion: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class FileFormatType { + @get:XmlAttribute(name = "CountryCode") + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var language: String + + @get:XmlValue + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + lateinit var value: String + } + + /** + * Generic key-value pair. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["name", "value"]) + class Parameter { + @get:XmlAttribute(name = "Type", required = true) + lateinit var type: String + + @get:XmlElement(name = "Name", required = true) + lateinit var name: String + + @get:XmlElement(name = "Value", required = true) + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["addressInfo", "bankInfo", "accountInfoList", "orderInfoList"]) + class PartnerInfo { + @get:XmlElement(name = "AddressInfo", required = true) + lateinit var addressInfo: AddressInfo + + @get:XmlElement(name = "BankInfo", required = true) + lateinit var bankInfo: BankInfo + + @get:XmlElement(name = "AccountInfo", type = AccountInfo::class) + var accountInfoList: List<AccountInfo>? = LinkedList<AccountInfo>() + + @get:XmlElement(name = "OrderInfo", type = AuthOrderInfoType::class) + var orderInfoList: List<AuthOrderInfoType> = LinkedList<AuthOrderInfoType>() + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["orderType", "fileFormat", "transferType", "orderFormat", "description", "numSigRequired"] + ) + class AuthOrderInfoType { + @get:XmlElement(name = "OrderType") + lateinit var orderType: String + + @get:XmlElement(name = "FileFormat") + val fileFormat: FileFormatType? = null + + @get:XmlElement(name = "TransferType") + lateinit var transferType: String + + @get:XmlElement(name = "OrderFormat", required = false) + var orderFormat: String? = null + + @get:XmlElement(name = "Description") + lateinit var description: String + + @get:XmlElement(name = "NumSigRequired") + var numSigRequired: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + class UserIDType { + @get:XmlValue + lateinit var value: String; + + @get:XmlAttribute(name = "Status") + var status: Int? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["userID", "name", "permissionList"]) + class UserInfo { + @get:XmlElement(name = "UserID", required = true) + lateinit var userID: UserIDType + + @get:XmlElement(name = "Name") + var name: String? = null + + @get:XmlElement(name = "Permission", type = UserPermission::class) + var permissionList: List<UserPermission>? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["orderTypes", "fileFormat", "accountID", "maxAmount"]) + class UserPermission { + @get:XmlAttribute(name = "AuthorizationLevel") + var authorizationLevel: String? = null + + @get:XmlElement(name = "OrderTypes") + var orderTypes: String? = null + + @get:XmlElement(name = "FileFormat") + val fileFormat: FileFormatType? = null + + @get:XmlElement(name = "AccountID") + val accountID: String? = null + + @get:XmlElement(name = "MaxAmount") + val maxAmount: String? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["name", "street", "postCode", "city", "region", "country"]) + class AddressInfo { + @get:XmlElement(name = "Name") + var name: String? = null + + @get:XmlElement(name = "Street") + var street: String? = null + + @get:XmlElement(name = "PostCode") + var postCode: String? = null + + @get:XmlElement(name = "City") + var city: String? = null + + @get:XmlElement(name = "Region") + var region: String? = null + + @get:XmlElement(name = "Country") + var country: String? = null + } + + + @XmlAccessorType(XmlAccessType.NONE) + class BankInfo { + @get:XmlElement(name = "HostID") + lateinit var hostID: String + + @get:XmlElement(type = Parameter::class) + var parameters: List<Parameter>? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["accountNumberList", "bankCodeList", "accountHolder"]) + class AccountInfo { + @get:XmlAttribute(name = "Currency") + var currency: String? = null + + @get:XmlAttribute(name = "ID") + lateinit var id: String + + @get:XmlAttribute(name = "Description") + var description: String? = null + + @get:XmlElements( + XmlElement(name = "AccountNumber", type = GeneralAccountNumber::class), + XmlElement(name = "NationalAccountNumber", type = NationalAccountNumber::class) + ) + var accountNumberList: List<AbstractAccountNumber>? = LinkedList<AbstractAccountNumber>() + + @get:XmlElements( + XmlElement(name = "BankCode", type = GeneralBankCode::class), + XmlElement(name = "NationalBankCode", type = NationalBankCode::class) + ) + var bankCodeList: List<AbstractBankCode>? = LinkedList<AbstractBankCode>() + + @get:XmlElement(name = "AccountHolder") + var accountHolder: String? = null + } + + interface AbstractAccountNumber + + @XmlAccessorType(XmlAccessType.NONE) + class GeneralAccountNumber : AbstractAccountNumber { + @get:XmlAttribute(name = "international") + var international: Boolean = true + + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class NationalAccountNumber : AbstractAccountNumber { + @get:XmlAttribute(name = "format") + lateinit var format: String + + @get:XmlValue + lateinit var value: String + } + + interface AbstractBankCode + + @XmlAccessorType(XmlAccessType.NONE) + class GeneralBankCode : AbstractBankCode { + @get:XmlAttribute(name = "prefix") + var prefix: String? = null + + @get:XmlAttribute(name = "international") + var international: Boolean = true + + @get:XmlValue + lateinit var value: String + } + + @XmlAccessorType(XmlAccessType.NONE) + class NationalBankCode : AbstractBankCode { + @get:XmlValue + lateinit var value: String + + @get:XmlAttribute(name = "format") + lateinit var format: String + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_h005/package-info.java b/ebics/src/main/kotlin/ebics_h005/package-info.java @@ -0,0 +1,13 @@ +/** + * This package-info.java file defines the default namespace for the JAXB bindings + * defined in the package. + */ + +@XmlSchema( + namespace = "urn:org:ebics:H005", + elementFormDefault = XmlNsForm.QUALIFIED +) +package tech.libeufin.ebics.ebics_h005; +import javax.xml.bind.annotation.XmlNs; +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_hev/EbicsMessages.kt b/ebics/src/main/kotlin/ebics_hev/EbicsMessages.kt @@ -0,0 +1,92 @@ +/* + * 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.ebics.ebics_hev + +import java.util.* +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.NormalizedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType( + name = "HEVRequestDataType" +) +@XmlRootElement(name = "ebicsHEVRequest") +class HEVRequest{ + @get:XmlElement(name = "HostID", required = true) + lateinit var hostId: String +} + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType( + name = "HEVResponseDataType", + propOrder = ["systemReturnCode", "versionNumber", "any"] +) +@XmlRootElement(name = "ebicsHEVResponse") +class HEVResponse { + @get:XmlElement(name = "SystemReturnCode", required = true) + lateinit var systemReturnCode: SystemReturnCodeType + + @get:XmlElement(name = "VersionNumber", namespace = "http://www.ebics.org/H000") + var versionNumber: List<VersionNumber> = LinkedList() + + @get:XmlAnyElement(lax = true) + var any: List<Any>? = null + + @XmlAccessorType(XmlAccessType.NONE) + class VersionNumber { + @get:XmlValue + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var value: String + + @get:XmlAttribute(name = "ProtocolVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var protocolVersion: String + + companion object { + fun create(protocolVersion: String, versionNumber: String): VersionNumber { + return VersionNumber().apply { + this.protocolVersion = protocolVersion + this.value = versionNumber + } + } + } + } +} + + +@XmlAccessorType(XmlAccessType.NONE) +@XmlType( + name = "SystemReturnCodeType", + propOrder = [ + "returnCode", + "reportText" + ] +) +class SystemReturnCodeType { + @get:XmlElement(name = "ReturnCode", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var returnCode: String + + @get:XmlElement(name = "ReportText", required = true) + @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) + lateinit var reportText: String +} diff --git a/ebics/src/main/kotlin/ebics_hev/package-info.java b/ebics/src/main/kotlin/ebics_hev/package-info.java @@ -0,0 +1,13 @@ +/** + * This package-info.java file defines the default namespace for the JAXB bindings + * defined in the package. + */ + +@XmlSchema( + namespace = "http://www.ebics.org/H000", + elementFormDefault = XmlNsForm.QUALIFIED +) +package tech.libeufin.ebics.ebics_hev; + +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; diff --git a/ebics/src/main/kotlin/ebics_s001/SignatureTypes.kt b/ebics/src/main/kotlin/ebics_s001/SignatureTypes.kt @@ -0,0 +1,92 @@ +/* + * 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.ebics.ebics_s001 + +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.apache.xml.security.binding.xmldsig.X509DataType +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import javax.xml.datatype.XMLGregorianCalendar + + +object SignatureTypes { + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "PubKeyValueType", namespace = "http://www.ebics.org/S001", propOrder = [ + "rsaKeyValue", + "timeStamp" + ] + ) + class PubKeyValueType { + @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) + lateinit var rsaKeyValue: RSAKeyValueType + + @get:XmlElement(name = "TimeStamp") + @get:XmlSchemaType(name = "dateTime") + var timeStamp: XMLGregorianCalendar? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = [ + "x509Data", + "pubKeyValue", + "signatureVersion" + ] + ) + class SignaturePubKeyInfoType { + @get:XmlElement(name = "X509Data") + var x509Data: X509DataType? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "SignatureVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var signatureVersion: String + } + + /** + * EBICS INI payload. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["signaturePubKeyInfo", "partnerID", "userID"] + ) + @XmlRootElement(name = "SignaturePubKeyOrderData") + class SignaturePubKeyOrderData { + @get:XmlElement(name = "SignaturePubKeyInfo", required = true) + lateinit var signaturePubKeyInfo: SignaturePubKeyInfoType + + @get:XmlElement(name = "PartnerID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var partnerID: String + + @get:XmlElement(name = "UserID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var userID: String + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_s001/UserSignatureData.kt b/ebics/src/main/kotlin/ebics_s001/UserSignatureData.kt @@ -0,0 +1,27 @@ +package tech.libeufin.ebics.ebics_s001 + +import javax.xml.bind.annotation.* + +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "UserSignatureData") +@XmlType(name = "", propOrder = ["orderSignatureList"]) +class UserSignatureData { + @XmlElement(name = "OrderSignatureData", type = OrderSignatureData::class) + var orderSignatureList: List<OrderSignatureData>? = null + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["signatureVersion", "signatureValue", "partnerID", "userID"]) + class OrderSignatureData { + @XmlElement(name = "SignatureVersion") + lateinit var signatureVersion: String + + @XmlElement(name = "SignatureValue") + lateinit var signatureValue: ByteArray + + @XmlElement(name = "PartnerID") + lateinit var partnerID: String + + @XmlElement(name = "UserID") + lateinit var userID: String + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_s001/package-info.java b/ebics/src/main/kotlin/ebics_s001/package-info.java @@ -0,0 +1,13 @@ +/** + * This package-info.java file defines the default namespace for the JAXB bindings + * defined in the package. + */ + +@XmlSchema( + namespace = "http://www.ebics.org/S001", + elementFormDefault = XmlNsForm.QUALIFIED +) +package tech.libeufin.ebics.ebics_s001; + +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_s002/SignatureTypes.kt b/ebics/src/main/kotlin/ebics_s002/SignatureTypes.kt @@ -0,0 +1,91 @@ +/* + * 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.ebics.ebics_s002 + +import org.apache.xml.security.binding.xmldsig.RSAKeyValueType +import org.apache.xml.security.binding.xmldsig.X509DataType +import javax.xml.bind.annotation.* +import javax.xml.bind.annotation.adapters.CollapsedStringAdapter +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter +import javax.xml.datatype.XMLGregorianCalendar + + +object SignatureTypes { + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "PubKeyValueType", namespace = "http://www.ebics.org/S002", propOrder = [ + "rsaKeyValue", + "timeStamp" + ] + ) + class PubKeyValueType { + @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) + lateinit var rsaKeyValue: RSAKeyValueType + + @get:XmlElement(name = "TimeStamp") + @get:XmlSchemaType(name = "dateTime") + var timeStamp: XMLGregorianCalendar? = null + } + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = [ + "x509Data", + "pubKeyValue", + "signatureVersion" + ] + ) + class SignaturePubKeyInfoType { + @get:XmlElement(name = "X509Data") + var x509Data: X509DataType? = null + + @get:XmlElement(name = "PubKeyValue", required = true) + lateinit var pubKeyValue: PubKeyValueType + + @get:XmlElement(name = "SignatureVersion", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + lateinit var signatureVersion: String + } + + /** + * EBICS INI payload. + */ + @XmlAccessorType(XmlAccessType.NONE) + @XmlType( + name = "", + propOrder = ["signaturePubKeyInfo", "partnerID", "userID"] + ) + @XmlRootElement(name = "SignaturePubKeyOrderData") + class SignaturePubKeyOrderData { + @get:XmlElement(name = "SignaturePubKeyInfo", required = true) + lateinit var signaturePubKeyInfo: SignaturePubKeyInfoType + + @get:XmlElement(name = "PartnerID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var partnerID: String + + @get:XmlElement(name = "UserID", required = true) + @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) + @get:XmlSchemaType(name = "token") + lateinit var userID: String + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt b/ebics/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt @@ -0,0 +1,27 @@ +package tech.libeufin.ebics.ebics_s002 + +import javax.xml.bind.annotation.* + +@XmlAccessorType(XmlAccessType.NONE) +@XmlRootElement(name = "UserSignatureData") +@XmlType(name = "", propOrder = ["orderSignatureList"]) +class UserSignatureDataEbics3 { + @XmlElement(name = "OrderSignatureData", type = OrderSignatureData::class) + var orderSignatureList: List<OrderSignatureData>? = null + + @XmlAccessorType(XmlAccessType.NONE) + @XmlType(name = "", propOrder = ["signatureVersion", "signatureValue", "partnerID", "userID"]) + class OrderSignatureData { + @XmlElement(name = "SignatureVersion") + lateinit var signatureVersion: String + + @XmlElement(name = "SignatureValue") + lateinit var signatureValue: ByteArray + + @XmlElement(name = "PartnerID") + lateinit var partnerID: String + + @XmlElement(name = "UserID") + lateinit var userID: String + } +} +\ No newline at end of file diff --git a/ebics/src/main/kotlin/ebics_s002/package-info.java b/ebics/src/main/kotlin/ebics_s002/package-info.java @@ -0,0 +1,13 @@ +/** + * This package-info.java file defines the default namespace for the JAXB bindings + * defined in the package. + */ + +@XmlSchema( + namespace = "http://www.ebics.org/S002", + elementFormDefault = XmlNsForm.QUALIFIED +) +package tech.libeufin.ebics.ebics_s002; + +import javax.xml.bind.annotation.XmlNsForm; +import javax.xml.bind.annotation.XmlSchema; +\ No newline at end of file diff --git a/ebics/src/main/resources/version.txt b/ebics/src/main/resources/version.txt @@ -0,0 +1 @@ +v0.9.4-git-8aeffb3f +\ No newline at end of file diff --git a/util/src/main/resources/xsd/camt.052.001.02.xsd b/ebics/src/main/resources/xsd/camt.052.001.02.xsd diff --git a/util/src/main/resources/xsd/camt.053.001.02.xsd b/ebics/src/main/resources/xsd/camt.053.001.02.xsd diff --git a/util/src/main/resources/xsd/camt.054.001.02.xsd b/ebics/src/main/resources/xsd/camt.054.001.02.xsd diff --git a/util/src/main/resources/xsd/ebics_H004.xsd b/ebics/src/main/resources/xsd/ebics_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_H005.xsd b/ebics/src/main/resources/xsd/ebics_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_hev.xsd b/ebics/src/main/resources/xsd/ebics_hev.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd b/ebics/src/main/resources/xsd/ebics_keymgmt_request_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd b/ebics/src/main/resources/xsd/ebics_keymgmt_request_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd b/ebics/src/main/resources/xsd/ebics_keymgmt_response_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd b/ebics/src/main/resources/xsd/ebics_keymgmt_response_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_orders_H004.xsd b/ebics/src/main/resources/xsd/ebics_orders_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_orders_H005.xsd b/ebics/src/main/resources/xsd/ebics_orders_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_request_H004.xsd b/ebics/src/main/resources/xsd/ebics_request_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_request_H005.xsd b/ebics/src/main/resources/xsd/ebics_request_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_response_H004.xsd b/ebics/src/main/resources/xsd/ebics_response_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_response_H005.xsd b/ebics/src/main/resources/xsd/ebics_response_H005.xsd diff --git a/util/src/main/resources/xsd/ebics_signature_S002.xsd b/ebics/src/main/resources/xsd/ebics_signature_S002.xsd diff --git a/util/src/main/resources/xsd/ebics_signatures.xsd b/ebics/src/main/resources/xsd/ebics_signatures.xsd diff --git a/util/src/main/resources/xsd/ebics_types_H004.xsd b/ebics/src/main/resources/xsd/ebics_types_H004.xsd diff --git a/util/src/main/resources/xsd/ebics_types_H005.xsd b/ebics/src/main/resources/xsd/ebics_types_H005.xsd diff --git a/util/src/main/resources/xsd/pain.001.001.03.ch.02.xsd b/ebics/src/main/resources/xsd/pain.001.001.03.ch.02.xsd diff --git a/util/src/main/resources/xsd/pain.001.001.03.xsd b/ebics/src/main/resources/xsd/pain.001.001.03.xsd diff --git a/util/src/main/resources/xsd/pain.001.001.09.ch.03.xsd b/ebics/src/main/resources/xsd/pain.001.001.09.ch.03.xsd diff --git a/util/src/main/resources/xsd/pain.002.001.13.xsd b/ebics/src/main/resources/xsd/pain.002.001.13.xsd diff --git a/util/src/main/resources/xsd/xmldsig-core-schema.xsd b/ebics/src/main/resources/xsd/xmldsig-core-schema.xsd diff --git a/ebics/src/test/kotlin/EbicsMessagesTest.kt b/ebics/src/test/kotlin/EbicsMessagesTest.kt @@ -0,0 +1,371 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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 junit.framework.TestCase.assertEquals +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.junit.Test +import org.w3c.dom.Element +import tech.libeufin.ebics.ebics_h004.* +import tech.libeufin.ebics.ebics_hev.HEVResponse +import tech.libeufin.ebics.ebics_hev.SystemReturnCodeType +import tech.libeufin.ebics.ebics_s001.SignatureTypes +import tech.libeufin.common.CryptoUtil +import tech.libeufin.ebics.XMLUtil +import javax.xml.datatype.DatatypeFactory +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class EbicsMessagesTest { + /** + * Tests the JAXB instantiation of non-XmlRootElement documents, + * as notably are the inner XML strings carrying keys in INI/HIA + * messages. + */ + @Test + fun testImportNonRoot() { + val classLoader = ClassLoader.getSystemClassLoader() + val ini = classLoader.getResource("ebics_ini_inner_key.xml") + val jaxb = XMLUtil.convertStringToJaxb<SignatureTypes.SignaturePubKeyOrderData>(ini.readText()) + assertEquals("A006", jaxb.value.signaturePubKeyInfo.signatureVersion) + } + + /** + * Test string -> JAXB + */ + @Test + fun testStringToJaxb() { + val classLoader = ClassLoader.getSystemClassLoader() + val ini = classLoader.getResource("ebics_ini_request_sample.xml") + val jaxb = XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(ini.readText()) + println("jaxb loaded") + assertEquals( + "INI", + jaxb.value.header.static.orderDetails.orderType + ) + } + + /** + * Test JAXB -> string + */ + @Test + fun testJaxbToString() { + val hevResponseJaxb = HEVResponse().apply { + this.systemReturnCode = SystemReturnCodeType().apply { + this.reportText = "[EBICS_OK]" + this.returnCode = "000000" + } + this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50")) + } + XMLUtil.convertJaxbToString(hevResponseJaxb) + } + + /** + * Test DOM -> JAXB + */ + @Test + fun testDomToJaxb() { + val classLoader = ClassLoader.getSystemClassLoader() + val ini = classLoader.getResource("ebics_ini_request_sample.xml")!! + val iniDom = XMLUtil.parseStringIntoDom(ini.readText()) + XMLUtil.convertDomToJaxb<EbicsUnsecuredRequest>( + EbicsUnsecuredRequest::class.java, + iniDom + ) + } + + @Test + fun testKeyMgmgResponse() { + val responseXml = EbicsKeyManagementResponse().apply { + header = EbicsKeyManagementResponse.Header().apply { + mutable = EbicsKeyManagementResponse.MutableHeaderType().apply { + reportText = "foo" + returnCode = "bar" + } + _static = EbicsKeyManagementResponse.EmptyStaticHeader() + } + version = "H004" + body = EbicsKeyManagementResponse.Body().apply { + returnCode = EbicsKeyManagementResponse.ReturnCode().apply { + authenticate = true + value = "000000" + } + } + } + val text = XMLUtil.convertJaxbToString(responseXml) + assertTrue(text.isNotEmpty()) + } + + @Test + fun testParseHiaRequestOrderData() { + val classLoader = ClassLoader.getSystemClassLoader() + val hia = classLoader.getResource("hia_request_order_data.xml")!!.readText() + XMLUtil.convertStringToJaxb<HIARequestOrderData>(hia) + } + + @Test + fun testHiaLoad() { + val classLoader = ClassLoader.getSystemClassLoader() + val hia = classLoader.getResource("hia_request.xml")!! + val hiaDom = XMLUtil.parseStringIntoDom(hia.readText()) + val x: Element = hiaDom.getElementsByTagNameNS( + "urn:org:ebics:H004", + "OrderDetails" + )?.item(0) as Element + + x.setAttributeNS( + "http://www.w3.org/2001/XMLSchema-instance", + "type", + "UnsecuredReqOrderDetailsType" + ) + + XMLUtil.convertDomToJaxb<EbicsUnsecuredRequest>( + EbicsUnsecuredRequest::class.java, + hiaDom + ) + } + + @Test + fun testLoadInnerKey() { + val jaxbKey = run { + val classLoader = ClassLoader.getSystemClassLoader() + val file = classLoader.getResource( + "ebics_ini_inner_key.xml" + ) + assertNotNull(file) + XMLUtil.convertStringToJaxb<SignatureTypes.SignaturePubKeyOrderData>(file.readText()) + } + + val modulus = jaxbKey.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.modulus + val exponent = jaxbKey.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.exponent + CryptoUtil.loadRsaPublicKeyFromComponents(modulus, exponent) + } + + @Test + fun testLoadIniMessage() { + val classLoader = ClassLoader.getSystemClassLoader() + val text = classLoader.getResource("ebics_ini_request_sample.xml")!!.readText() + XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(text) + } + + @Test + fun testLoadResponse() { + val response = EbicsResponse().apply { + version = "H004" + header = EbicsResponse.Header().apply { + _static = EbicsResponse.StaticHeaderType() + mutable = EbicsResponse.MutableHeaderType().apply { + this.reportText = "foo" + this.returnCode = "bar" + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + } + } + authSignature = SignatureType() + body = EbicsResponse.Body().apply { + returnCode = EbicsResponse.ReturnCode().apply { + authenticate = true + value = "asdf" + } + } + } + print(XMLUtil.convertJaxbToString(response)) + } + + @Test + fun testLoadHpb() { + val classLoader = ClassLoader.getSystemClassLoader() + val text = classLoader.getResource("hpb_request.xml")!!.readText() + XMLUtil.convertStringToJaxb<EbicsNpkdRequest>(text) + } + + @Test + fun testHtd() { + val htd = HTDResponseOrderData().apply { + this.partnerInfo = EbicsTypes.PartnerInfo().apply { + this.accountInfoList = listOf( + EbicsTypes.AccountInfo().apply { + this.id = "acctid1" + this.accountHolder = "Mina Musterfrau" + this.accountNumberList = listOf( + EbicsTypes.GeneralAccountNumber().apply { + this.international = true + this.value = "AT411100000237571500" + } + ) + this.currency = "EUR" + this.description = "some account" + this.bankCodeList = listOf( + EbicsTypes.GeneralBankCode().apply { + this.international = true + this.value = "ABAGATWWXXX" + } + ) + } + ) + this.addressInfo = EbicsTypes.AddressInfo().apply { + this.name = "Foo" + } + this.bankInfo = EbicsTypes.BankInfo().apply { + this.hostID = "MYHOST" + } + this.orderInfoList = listOf( + EbicsTypes.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "CCC" + this.orderFormat = "foo" + this.transferType = "Upload" + } + ) + } + this.userInfo = EbicsTypes.UserInfo().apply { + this.name = "Some User" + this.userID = EbicsTypes.UserIDType().apply { + this.status = 2 + this.value = "myuserid" + } + this.permissionList = listOf( + EbicsTypes.UserPermission().apply { + this.orderTypes = "CCC ABC" + } + ) + } + } + + val str = XMLUtil.convertJaxbToString(htd) + println(str) + assert(XMLUtil.validateFromString(str)) + } + + + @Test + fun testHkd() { + val hkd = HKDResponseOrderData().apply { + this.partnerInfo = EbicsTypes.PartnerInfo().apply { + this.accountInfoList = listOf( + EbicsTypes.AccountInfo().apply { + this.id = "acctid1" + this.accountHolder = "Mina Musterfrau" + this.accountNumberList = listOf( + EbicsTypes.GeneralAccountNumber().apply { + this.international = true + this.value = "AT411100000237571500" + } + ) + this.currency = "EUR" + this.description = "some account" + this.bankCodeList = listOf( + EbicsTypes.GeneralBankCode().apply { + this.international = true + this.value = "ABAGATWWXXX" + } + ) + } + ) + this.addressInfo = EbicsTypes.AddressInfo().apply { + this.name = "Foo" + } + this.bankInfo = EbicsTypes.BankInfo().apply { + this.hostID = "MYHOST" + } + this.orderInfoList = listOf( + EbicsTypes.AuthOrderInfoType().apply { + this.description = "foo" + this.orderType = "CCC" + this.orderFormat = "foo" + this.transferType = "Upload" + } + ) + } + this.userInfoList = listOf( + EbicsTypes.UserInfo().apply { + this.name = "Some User" + this.userID = EbicsTypes.UserIDType().apply { + this.status = 2 + this.value = "myuserid" + } + this.permissionList = listOf( + EbicsTypes.UserPermission().apply { + this.orderTypes = "CCC ABC" + } + ) + }) + } + + val str = XMLUtil.convertJaxbToString(hkd) + println(str) + assert(XMLUtil.validateFromString(str)) + } + + @Test + fun testEbicsRequestInitializationPhase() { + val ebicsRequestObj = EbicsRequest().apply { + this.version = "H004" + this.revision = 1 + this.authSignature = SignatureType() + this.header = EbicsRequest.Header().apply { + this.authenticate = true + this.mutable = EbicsRequest.MutableHeader().apply { + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + } + this.static = EbicsRequest.StaticHeaderType().apply { + this.hostID = "myhost" + this.nonce = ByteArray(16) + this.timestamp = + DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(2019, 5, 5, 5, 5, 5, 0, 0) + this.partnerID = "mypid01" + this.userID = "myusr01" + this.product = EbicsTypes.Product().apply { + this.instituteID = "test" + this.language = "en" + this.value = "test" + } + this.orderDetails = EbicsRequest.OrderDetails().apply { + this.orderAttribute = "DZHNN" + this.orderID = "OR01" + this.orderType = "BLA" + this.orderParams = EbicsRequest.StandardOrderParams() + } + this.bankPubKeyDigests = EbicsRequest.BankPubKeyDigests().apply { + this.authentication = EbicsTypes.PubKeyDigest().apply { + this.algorithm = "foo" + this.value = ByteArray(32) + this.version = "X002" + } + this.encryption = EbicsTypes.PubKeyDigest().apply { + this.algorithm = "foo" + this.value = ByteArray(32) + this.version = "E002" + } + } + this.securityMedium = "0000" + } + } + this.body = EbicsRequest.Body().apply { + } + } + + val str = XMLUtil.convertJaxbToString(ebicsRequestObj) + val doc = XMLUtil.parseStringIntoDom(str) + val pair = CryptoUtil.generateRsaKeyPair(1024) + XMLUtil.signEbicsDocument(doc, pair.private) + val finalStr = XMLUtil.convertDomToString(doc) + assert(XMLUtil.validateFromString(finalStr)) + } +} +\ No newline at end of file diff --git a/ebics/src/test/kotlin/EbicsOrderUtilTest.kt b/ebics/src/test/kotlin/EbicsOrderUtilTest.kt @@ -0,0 +1,308 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.junit.Test +import tech.libeufin.ebics.EbicsOrderUtil +import tech.libeufin.ebics.XMLUtil +import tech.libeufin.ebics.ebics_h004.HTDResponseOrderData +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)) + } + + @Test + fun testDecodeOrderData() { + val orderDataXml = """ + <?xml version="1.0" encoding="UTF-8"?> + <HTDResponseOrderData xmlns="urn:org:ebics:H004" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:org:ebics:H004 ebics_orders_H004.xsd"> + <PartnerInfo> + <AddressInfo> + <Name>Mr Anybody</Name> + <Street>CENSORED</Street> + <PostCode>12345</PostCode> + <City>Footown</City> + </AddressInfo> + <BankInfo> + <HostID>BLABLUBLA</HostID> + </BankInfo> + <AccountInfo ID="accid000000001" Currency="EUR"> + <AccountNumber international="false">12345667</AccountNumber> + <AccountNumber international="true">DE54430609999999999999</AccountNumber> + <BankCode international="false">43060967</BankCode> + <BankCode international="true">GENODEM1GLS</BankCode> + <AccountHolder>Mr Anybody</AccountHolder> + </AccountInfo> + <OrderInfo> + <OrderType>C52</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>CAMT052</OrderFormat> + <Description>Abholen Vormerkposten</Description> + </OrderInfo> + <OrderInfo> + <OrderType>C53</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>CAMT053</OrderFormat> + <Description>Abholen Kontoauszuege</Description> + </OrderInfo> + <OrderInfo> + <OrderType>C54</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>CAMT054</OrderFormat> + <Description>Abholen Nachricht Sammelbuchungsdatei, Soll-, Haben-Avis</Description> + </OrderInfo> + <OrderInfo> + <OrderType>CDZ</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>XMLBIN</OrderFormat> + <Description>Abholen Payment Status Report for Direct Debit</Description> + </OrderInfo> + <OrderInfo> + <OrderType>CRZ</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>XMLBIN</OrderFormat> + <Description>Abholen Payment Status Report for Credit Transfer</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HAA</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Abrufbare Auftragsarten abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HAC</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>HAC</OrderFormat> + <Description>Kundenprotokoll (XML-Format) abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HKD</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Kunden- und Teilnehmerdaten abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HPB</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Public Keys der Bank abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HPD</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Bankparameter abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HTD</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Kunden- und Teilnehmerdaten abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HVD</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>VEU-Status abrufen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HVT</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>VEU-Transaktionsdetails abrufen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HVU</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>VEU-Uebersicht abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>HVZ</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>VEU-Uebersicht mit Zusatzinformationen abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>PTK</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>PTK</OrderFormat> + <Description>Protokolldatei abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>STA</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MT940</OrderFormat> + <Description>Swift-Tagesauszuege abholen</Description> + </OrderInfo> + <OrderInfo> + <OrderType>VMK</OrderType> + <TransferType>Download</TransferType> + <OrderFormat>MT942</OrderFormat> + <Description>Abholen kurzfristige Vormerkposten</Description> + </OrderInfo> + <OrderInfo> + <OrderType>AZV</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>DTAZVJS</OrderFormat> + <Description>AZV im Diskettenformat senden</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>C1C</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>P8CCOR1</OrderFormat> + <Description>Einreichen von Lastschriften D-1-Option in einem Container</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>C2C</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>PN8CONCS</OrderFormat> + <Description>Einreichen von Firmenlastschriften in einem Container</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>CCC</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>PN1CONCS</OrderFormat> + <Description>Ueberweisungen im SEPA-Container</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>CCT</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>PN1GOCS</OrderFormat> + <Description>Überweisungen im ZKA-Format</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>CCU</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>P1URGCS</OrderFormat> + <Description>Einreichen von Eilueberweisungen</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>CDB</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>PAIN8CS</OrderFormat> + <Description>Einreichen von Firmenlastschriften</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>CDC</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>PN8CONCS</OrderFormat> + <Description>Einreichen von Lastschriften in einem Container</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>CDD</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>PN8GOCS</OrderFormat> + <Description>Einreichen von Lastschriften</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>HCA</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Public Key senden</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>HCS</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Teilnehmerschluessel EU und EBICS aendern</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>HIA</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Initiales Senden Public Keys</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>HVE</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>VEU-Unterschrift hinzufuegen</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>HVS</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>VEU-Storno</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>INI</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Passwort-Initialisierung</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>PUB</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Public-Key senden</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + <OrderInfo> + <OrderType>SPR</OrderType> + <TransferType>Upload</TransferType> + <OrderFormat>MISC</OrderFormat> + <Description>Sperrung der Zugangsberechtigung</Description> + <NumSigRequired>0</NumSigRequired> + </OrderInfo> + </PartnerInfo> + <UserInfo> + <UserID Status="1">ANYBOMR</UserID> + <Name>Mr Anybody</Name> + <Permission> + <OrderTypes>C52 C53 C54 CDZ CRZ HAA HAC HKD HPB HPD HTD HVD HVT HVU HVZ PTK</OrderTypes> + </Permission> + <Permission> + <OrderTypes></OrderTypes> + <AccountID>accid000000001</AccountID> + </Permission> + <Permission AuthorisationLevel="E"> + <OrderTypes>AZV CCC CCT CCU</OrderTypes> + </Permission> + <Permission AuthorisationLevel="T"> + <OrderTypes>HCA HCS HIA HVE HVS INI PUB SPR</OrderTypes> + </Permission> + </UserInfo> + </HTDResponseOrderData> + """.trimIndent() + XMLUtil.convertStringToJaxb<HTDResponseOrderData>(orderDataXml); + } +} +\ No newline at end of file diff --git a/ebics/src/test/kotlin/SignatureDataTest.kt b/ebics/src/test/kotlin/SignatureDataTest.kt @@ -0,0 +1,96 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.junit.Test +import tech.libeufin.common.CryptoUtil +import tech.libeufin.ebics.XMLUtil +import tech.libeufin.ebics.ebics_h004.EbicsRequest +import tech.libeufin.ebics.ebics_h004.EbicsTypes +import java.math.BigInteger +import java.util.* +import javax.xml.datatype.DatatypeFactory + +class SignatureDataTest { + + @Test + fun makeSignatureData() { + + val pair = CryptoUtil.generateRsaKeyPair(1024) + + val tmp = EbicsRequest().apply { + header = EbicsRequest.Header().apply { + version = "H004" + revision = 1 + authenticate = true + static = EbicsRequest.StaticHeaderType().apply { + hostID = "some host ID" + nonce = "nonce".toByteArray() + timestamp = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()) + partnerID = "some partner ID" + userID = "some user ID" + orderDetails = EbicsRequest.OrderDetails().apply { + orderType = "TST" + orderAttribute = "OZHNN" + } + bankPubKeyDigests = EbicsRequest.BankPubKeyDigests().apply { + authentication = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "X002" + value = CryptoUtil.getEbicsPublicKeyHash(pair.public) + } + encryption = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = CryptoUtil.getEbicsPublicKeyHash(pair.public) + } + } + securityMedium = "0000" + numSegments = BigInteger.ONE + + authSignature = SignatureType() + } + mutable = EbicsRequest.MutableHeader().apply { + transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + } + body = EbicsRequest.Body().apply { + dataTransfer = EbicsRequest.DataTransfer().apply { + signatureData = EbicsRequest.SignatureData().apply { + authenticate = true + value = "to byte array".toByteArray() + } + dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { + transactionKey = "mock".toByteArray() + authenticate = true + encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { + algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" + version = "E002" + value = + CryptoUtil.getEbicsPublicKeyHash(pair.public) + } + } + hostId = "a host ID" + } + } + } + } + + println(XMLUtil.convertJaxbToString(tmp)) + } +} +\ No newline at end of file diff --git a/ebics/src/test/kotlin/XmlCombinatorsTest.kt b/ebics/src/test/kotlin/XmlCombinatorsTest.kt @@ -0,0 +1,75 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.junit.Test +import tech.libeufin.ebics.XmlElementBuilder +import tech.libeufin.ebics.constructXml + +class XmlCombinatorsTest { + + @Test + fun testWithModularity() { + fun module(base: XmlElementBuilder) { + base.element("module") + } + val s = constructXml { + root("root") { + module(this) + } + } + println(s) + } + + @Test + fun testWithIterable() { + val s = constructXml(indent = true) { + namespace("iter", "able") + root("iterable") { + element("endOfDocument") { + for (i in 1..10) + element("$i") { + element("$i$i") { + text("$i$i$i") + } + } + } + } + } + println(s) + } + + @Test + fun testBasicXmlBuilding() { + val s = constructXml(indent = true) { + namespace("ebics", "urn:org:ebics:H004") + root("ebics:ebicsRequest") { + attribute("version", "H004") + element("a/b/c") { + attribute("attribute-of", "c") + element("//d/e/f//") { + attribute("nested", "true") + element("g/h/") + } + } + element("one more") + } + } + println(s) + } +} diff --git a/ebics/src/test/kotlin/XmlUtilTest.kt b/ebics/src/test/kotlin/XmlUtilTest.kt @@ -0,0 +1,195 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * 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/> + */ + +import org.apache.xml.security.binding.xmldsig.SignatureType +import org.junit.Test +import org.junit.Assert.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import tech.libeufin.ebics.ebics_h004.EbicsKeyManagementResponse +import tech.libeufin.ebics.ebics_h004.EbicsResponse +import tech.libeufin.ebics.ebics_h004.EbicsTypes +import tech.libeufin.ebics.ebics_h004.HTDResponseOrderData +import tech.libeufin.common.CryptoUtil +import tech.libeufin.ebics.XMLUtil +import java.security.KeyPairGenerator +import java.util.* +import javax.xml.transform.stream.StreamSource +import tech.libeufin.ebics.XMLUtil.Companion.signEbicsResponse + +class XmlUtilTest { + + @Test + fun deserializeConsecutiveLists() { + + val tmp = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(""" + <?xml version="1.0" encoding="UTF-8" standalone="yes"?> + <HTDResponseOrderData xmlns="urn:org:ebics:H004"> + <PartnerInfo> + <AddressInfo> + <Name>Foo</Name> + </AddressInfo> + <BankInfo> + <HostID>host01</HostID> + </BankInfo> + <AccountInfo Currency="EUR" Description="ACCT" ID="acctid1"> + <AccountNumber international="true">DE21500105174751659277</AccountNumber> + <BankCode international="true">INGDDEFFXXX</BankCode> + <AccountHolder>Mina Musterfrau</AccountHolder> + </AccountInfo> + <AccountInfo Currency="EUR" Description="glsdemoacct" ID="glsdemo"> + <AccountNumber international="true">DE91430609670123123123</AccountNumber> + <BankCode international="true">GENODEM1GLS</BankCode> + <AccountHolder>Mina Musterfrau</AccountHolder> + </AccountInfo> + <OrderInfo> + <OrderType>C53</OrderType> + <TransferType>Download</TransferType> + <Description>foo</Description> + </OrderInfo> + <OrderInfo> + <OrderType>C52</OrderType> + <TransferType>Download</TransferType> + <Description>foo</Description> + </OrderInfo> + <OrderInfo> + <OrderType>CCC</OrderType> + <TransferType>Upload</TransferType> + <Description>foo</Description> + </OrderInfo> + </PartnerInfo> + <UserInfo> + <UserID Status="5">USER1</UserID> + <Name>Some User</Name> + <Permission> + <OrderTypes>C54 C53 C52 CCC</OrderTypes> + </Permission> + </UserInfo> + </HTDResponseOrderData>""".trimIndent() + ) + + println(tmp.value.partnerInfo.orderInfoList[0].description) + } + + @Test + fun exceptionOnConversion() { + try { + XMLUtil.convertStringToJaxb<EbicsKeyManagementResponse>("<malformed xml>") + } catch (e: javax.xml.bind.UnmarshalException) { + // just ensuring this is the exception + println("caught") + return + } + assertTrue(false) + } + + @Test + fun hevValidation(){ + val classLoader = ClassLoader.getSystemClassLoader() + val hev = classLoader.getResourceAsStream("ebics_hev.xml") + assertTrue(XMLUtil.validate(StreamSource(hev))) + } + + @Test + fun iniValidation(){ + val classLoader = ClassLoader.getSystemClassLoader() + val ini = classLoader.getResourceAsStream("ebics_ini_request_sample.xml") + assertTrue(XMLUtil.validate(StreamSource(ini))) + } + + @Test + fun basicSigningTest() { + val doc = XMLUtil.parseStringIntoDom(""" + <myMessage xmlns:ebics="urn:org:ebics:H004"> + <ebics:AuthSignature /> + <foo authenticate="true">Hello World</foo> + </myMessage> + """.trimIndent()) + val kpg = KeyPairGenerator.getInstance("RSA") + kpg.initialize(2048) + val pair = kpg.genKeyPair() + val otherPair = kpg.genKeyPair() + XMLUtil.signEbicsDocument(doc, pair.private) + kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) + kotlin.test.assertFalse(XMLUtil.verifyEbicsDocument(doc, otherPair.public)) + } + + @Test + fun verifySigningWithConversion() { + + val pair = CryptoUtil.generateRsaKeyPair(2048) + + val response = EbicsResponse().apply { + version = "H004" + header = EbicsResponse.Header().apply { + _static = EbicsResponse.StaticHeaderType() + mutable = EbicsResponse.MutableHeaderType().apply { + this.reportText = "foo" + this.returnCode = "bar" + this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION + } + } + authSignature = SignatureType() + body = EbicsResponse.Body().apply { + returnCode = EbicsResponse.ReturnCode().apply { + authenticate = true + value = "asdf" + } + } + } + + val signature = signEbicsResponse(response, pair.private) + val signatureJaxb = XMLUtil.convertStringToJaxb<EbicsResponse>(signature) + + assertTrue( + + XMLUtil.verifyEbicsDocument( + XMLUtil.convertJaxbToDocument(signatureJaxb.value), + pair.public + ) + ) + } + + @Test + fun multiAuthSigningTest() { + val doc = XMLUtil.parseStringIntoDom(""" + <myMessage xmlns:ebics="urn:org:ebics:H004"> + <ebics:AuthSignature /> + <foo authenticate="true">Hello World</foo> + <bar authenticate="true">Another one!</bar> + </myMessage> + """.trimIndent()) + val kpg = KeyPairGenerator.getInstance("RSA") + kpg.initialize(2048) + val pair = kpg.genKeyPair() + XMLUtil.signEbicsDocument(doc, pair.private) + kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) + } + + @Test + fun testRefSignature() { + val classLoader = ClassLoader.getSystemClassLoader() + val docText = classLoader.getResourceAsStream("signature1/doc.xml")!!.readAllBytes().toString(Charsets.UTF_8) + val doc = XMLUtil.parseStringIntoDom(docText) + val keyText = classLoader.getResourceAsStream("signature1/public_key.txt")!!.readAllBytes() + val keyBytes = Base64.getDecoder().decode(keyText) + val key = CryptoUtil.loadRsaPublicKey(keyBytes) + assertTrue(XMLUtil.verifyEbicsDocument(doc, key)) + } +} +\ No newline at end of file diff --git a/util/src/test/resources/ebics_hev.xml b/ebics/src/test/resources/ebics_hev.xml diff --git a/util/src/test/resources/ebics_ini_inner_key.xml b/ebics/src/test/resources/ebics_ini_inner_key.xml diff --git a/util/src/test/resources/ebics_ini_request_sample.xml b/ebics/src/test/resources/ebics_ini_request_sample.xml diff --git a/util/src/test/resources/hia_request.xml b/ebics/src/test/resources/hia_request.xml diff --git a/util/src/test/resources/hia_request_order_data.xml b/ebics/src/test/resources/hia_request_order_data.xml diff --git a/util/src/test/resources/hpb_request.xml b/ebics/src/test/resources/hpb_request.xml diff --git a/util/src/test/resources/signature1/doc.xml b/ebics/src/test/resources/signature1/doc.xml diff --git a/util/src/test/resources/signature1/public_key.txt b/ebics/src/test/resources/signature1/public_key.txt diff --git a/integration/build.gradle b/integration/build.gradle @@ -1,39 +0,0 @@ -plugins { - id("kotlin") - id("application") -} - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -compileKotlin.kotlinOptions.jvmTarget = "17" -compileTestKotlin.kotlinOptions.jvmTarget = "17" - -sourceSets.main.java.srcDirs = ["src/main/kotlin"] - -dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") - - implementation(project(":util")) - implementation(project(":bank")) - implementation(project(":nexus")) - - implementation("com.github.ajalt.clikt:clikt:$clikt_version") - - implementation("org.postgresql:postgresql:$postgres_version") - - implementation("io.ktor:ktor-server-test-host:$ktor_version") - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") - implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") -} - -application { - mainClass = "tech.libeufin.integration.MainKt" - applicationName = "libeufin-integration-test" -} - -run { - standardInput = System.in -} -\ No newline at end of file diff --git a/integration/src/main/kotlin/Main.kt b/integration/src/main/kotlin/Main.kt @@ -1,230 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - * - * 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.integration - -import tech.libeufin.nexus.Database as NexusDb -import tech.libeufin.nexus.* -import tech.libeufin.bank.* -import tech.libeufin.util.* -import com.github.ajalt.clikt.core.* -import com.github.ajalt.clikt.parameters.arguments.* -import com.github.ajalt.clikt.parameters.types.* -import com.github.ajalt.clikt.testing.* -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import kotlin.test.* -import java.io.File -import java.nio.file.* -import java.time.Instant -import kotlinx.coroutines.runBlocking -import io.ktor.client.request.* -import net.taler.wallet.crypto.Base32Crockford -import kotlin.io.path.* - -fun randBytes(lenght: Int): ByteArray { - val bytes = ByteArray(lenght) - kotlin.random.Random.nextBytes(bytes) - return bytes -} - -val nexusCmd = LibeufinNexusCommand() -val client = HttpClient(CIO) - -fun step(name: String) { - println("\u001b[35m$name\u001b[0m") -} - -fun ask(question: String): String? { - print("\u001b[;1m$question\u001b[0m") - System.out.flush() - return readlnOrNull() -} - -fun CliktCommandTestResult.assertOk(msg: String? = null) { - println("$output") - assertEquals(0, statusCode, msg) -} - -fun CliktCommandTestResult.assertErr(msg: String? = null) { - println("$output") - assertEquals(1, statusCode, msg) -} - -enum class Kind { - postfinance, - netzbon -} - -class Cli : CliktCommand("Run integration tests on banks provider") { - val kind: Kind by argument().enum<Kind>() - override fun run() { - val name = kind.name - step("Test init $name") - - runBlocking { - Path("test/$name").createDirectories() - val conf = "conf/$name.conf" - val log = "DEBUG" - val flags = " -c $conf -L $log" - val ebicsFlags = "$flags --transient --debug-ebics test/$name" - val cfg = loadConfig(conf) - - val clientKeysPath = Path(cfg.requireString("nexus-ebics", "client_private_keys_file")) - val bankKeysPath = Path(cfg.requireString("nexus-ebics", "bank_public_keys_file")) - - var hasClientKeys = clientKeysPath.exists() - var hasBankKeys = bankKeysPath.exists() - - if (ask("Reset DB ? y/n>") == "y") nexusCmd.test("dbinit -r $flags").assertOk() - else nexusCmd.test("dbinit $flags").assertOk() - val nexusDb = NexusDb("postgresql:///libeufincheck") - - when (kind) { - Kind.postfinance -> { - if (hasClientKeys || hasBankKeys) { - if (ask("Reset keys ? y/n>") == "y") { - if (hasClientKeys) clientKeysPath.deleteIfExists() - if (hasBankKeys) bankKeysPath.deleteIfExists() - hasClientKeys = false - hasBankKeys = false - } - } - - if (!hasClientKeys) { - step("Test INI order") - ask("Got to https://isotest.postfinance.ch/corporates/user/settings/ebics and click on 'Reset EBICS user'.\nPress Enter when done>") - nexusCmd.test("ebics-setup $flags") - .assertErr("ebics-setup should failed the first time") - } - - if (!hasBankKeys) { - step("Test HIA order") - ask("Got to https://isotest.postfinance.ch/corporates/user/settings/ebics and click on 'Activate EBICS user'.\nPress Enter when done>") - nexusCmd.test("ebics-setup --auto-accept-keys $flags") - .assertOk("ebics-setup should succeed the second time") - } - - val payto = "payto://iban/CH2989144971918294289?receiver-name=Test" - - step("Test fetch transactions") - nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01").assertOk() - - while (true) { - when (ask("Run 'fetch', 'submit', 'tx', 'txs', 'logs', 'ack' or 'exit'>")) { - "fetch" -> { - step("Fetch new transactions") - nexusCmd.test("ebics-fetch $ebicsFlags").assertOk() - } - "tx" -> { - step("Test submit one transaction") - nexusDb.initiatedPaymentCreate(InitiatedPayment( - amount = TalerAmount("CFH:42"), - creditPaytoUri = payto, - wireTransferSubject = "single transaction test", - initiationTime = Instant.now(), - requestUid = Base32Crockford.encode(randBytes(16)) - )) - nexusCmd.test("ebics-submit $ebicsFlags").assertOk() - } - "txs" -> { - step("Test submit many transaction") - repeat(4) { - nexusDb.initiatedPaymentCreate(InitiatedPayment( - amount = TalerAmount("CFH:${100L+it}"), - creditPaytoUri = payto, - wireTransferSubject = "multi transaction test $it", - initiationTime = Instant.now(), - requestUid = Base32Crockford.encode(randBytes(16)) - )) - } - nexusCmd.test("ebics-submit $ebicsFlags").assertOk() - } - "submit" -> { - step("Submit pending transactions") - nexusCmd.test("ebics-submit $ebicsFlags").assertOk() - } - "logs" -> { - step("Fetch logs") - nexusCmd.test("ebics-fetch $ebicsFlags --only-logs").assertOk() - } - "ack" -> { - step("Fetch ack") - nexusCmd.test("ebics-fetch $ebicsFlags --only-ack").assertOk() - } - "exit" -> break - } - } - } - Kind.netzbon -> { - if (!hasClientKeys) - throw Exception("Clients keys are required to run netzbon tests") - - if (!hasBankKeys) { - step("Test HIA order") - nexusCmd.test("ebics-setup --auto-accept-keys $flags").assertOk("ebics-setup should succeed the second time") - } - - step("Test fetch transactions") - nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01").assertOk() - - while (true) { - when (ask("Run 'fetch', 'submit', 'logs', 'ack' or 'exit'>")) { - "fetch" -> { - step("Fetch new transactions") - nexusCmd.test("ebics-fetch $ebicsFlags").assertOk() - } - "submit" -> { - step("Submit pending transactions") - nexusCmd.test("ebics-submit $ebicsFlags").assertOk() - } - "tx" -> { - step("Submit new transaction") - // TODO interactive payment editor - nexusDb.initiatedPaymentCreate(InitiatedPayment( - amount = TalerAmount("CFH:1.1"), - creditPaytoUri = "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans", - wireTransferSubject = "single transaction test", - initiationTime = Instant.now(), - requestUid = Base32Crockford.encode(randBytes(16)) - )) - nexusCmd.test("ebics-submit $ebicsFlags").assertOk() - } - "logs" -> { - step("Fetch logs") - nexusCmd.test("ebics-fetch $ebicsFlags --only-logs").assertOk() - } - "ack" -> { - step("Fetch ack") - nexusCmd.test("ebics-fetch $ebicsFlags --only-ack").assertOk() - } - "exit" -> break - } - } - } - } - } - - step("Test succeed") - } -} - -fun main(args: Array<String>) { - Cli().main(args) -} diff --git a/integration/src/test/kotlin/IntegrationTest.kt b/integration/src/test/kotlin/IntegrationTest.kt @@ -1,326 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - * - * 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/> - */ - -import org.junit.Test -import net.taler.wallet.crypto.Base32Crockford -import tech.libeufin.bank.* -import tech.libeufin.nexus.* -import tech.libeufin.nexus.Database as NexusDb -import tech.libeufin.bank.db.AccountDAO.* -import tech.libeufin.util.* -import java.io.File -import java.time.Instant -import java.util.Arrays -import java.sql.SQLException -import kotlinx.coroutines.runBlocking -import com.github.ajalt.clikt.testing.test -import com.github.ajalt.clikt.core.CliktCommand -import org.postgresql.jdbc.PgConnection -import kotlin.test.* -import io.ktor.client.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.HttpStatusCode - -fun CliktCommand.run(cmd: String) { - val result = test(cmd) - if (result.statusCode != 0) - throw Exception(result.output) - println(result.output) -} - -fun HttpResponse.assertNoContent() { - assertEquals(HttpStatusCode.NoContent, this.status) -} - -fun randBytes(lenght: Int): ByteArray { - val bytes = ByteArray(lenght) - kotlin.random.Random.nextBytes(bytes) - return bytes -} - -fun server(lambda: () -> Unit) { - // Start the HTTP server in another thread - kotlin.concurrent.thread(isDaemon = true) { - lambda() - } - // Wait for the HTTP server to be up - runBlocking { - HttpClient(CIO) { - install(HttpRequestRetry) { - maxRetries = 10 - constantDelay(200, 100) - } - }.get("http://0.0.0.0:8080/config") - } - -} - -fun setup(lambda: suspend (NexusDb) -> Unit) { - try { - runBlocking { - NexusDb("postgresql:///libeufincheck").use { - lambda(it) - } - } - } finally { - engine?.stop(0, 0) // Stop http server if started - } -} - -inline fun assertException(msg: String, lambda: () -> Unit) { - try { - lambda() - throw Exception("Expected failure: $msg") - } catch (e: Exception) { - assert(e.message!!.startsWith(msg)) { "${e.message}" } - } -} - -class IntegrationTest { - val nexusCmd = LibeufinNexusCommand() - val bankCmd = LibeufinBankCommand(); - val client = HttpClient(CIO) - - @Test - fun mini() { - bankCmd.run("dbinit -c conf/mini.conf -r") - bankCmd.run("passwd admin password -c conf/mini.conf") - bankCmd.run("dbinit -c conf/mini.conf") // Indempotent - - server { - bankCmd.run("serve -c conf/mini.conf") - } - - setup { _ -> - // Check bank is running - client.get("http://0.0.0.0:8080/public-accounts").assertNoContent() - } - } - - @Test - fun errors() { - nexusCmd.run("dbinit -c conf/integration.conf -r") - bankCmd.run("dbinit -c conf/integration.conf -r") - bankCmd.run("passwd admin password -c conf/integration.conf") - - suspend fun checkCount(db: NexusDb, nbIncoming: Int, nbBounce: Int, nbTalerable: Int) { - db.conn { conn -> - conn.prepareStatement("SELECT count(*) FROM incoming_transactions").oneOrNull { - assertEquals(nbIncoming, it.getInt(1)) - } - conn.prepareStatement("SELECT count(*) FROM bounced_transactions").oneOrNull { - assertEquals(nbBounce, it.getInt(1)) - } - conn.prepareStatement("SELECT count(*) FROM talerable_incoming_transactions").oneOrNull { - assertEquals(nbTalerable, it.getInt(1)) - } - } - } - - setup { db -> - val userPayTo = IbanPayTo(genIbanPaytoUri()) - val fiatPayTo = IbanPayTo(genIbanPaytoUri()) - - // Load conversion setup manually as the server would refuse to start without an exchange account - val sqlProcedures = File("../database-versioning/libeufin-conversion-setup.sql") - db.conn { - it.execSQLUpdate(sqlProcedures.readText()) - it.execSQLUpdate("SET search_path TO libeufin_nexus;") - } - - val reservePub = randBytes(32) - val payment = IncomingPayment( - amount = TalerAmount("EUR:10"), - debitPaytoUri = userPayTo.canonical, - wireTransferSubject = "Error test ${Base32Crockford.encode(reservePub)}", - executionTime = Instant.now(), - bankId = "error" - ) - - assertException("ERROR: cashin failed: missing exchange account") { - ingestIncomingPayment(db, payment) - } - - // Create exchange account - bankCmd.run("create-account -c conf/integration.conf -u exchange -p password --name 'Mr Money' --exchange") - - assertException("ERROR: cashin currency conversion failed: missing conversion rates") { - ingestIncomingPayment(db, payment) - } - - // Start server - server { - bankCmd.run("serve -c conf/integration.conf") - } - - // Set conversion rates - client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { - basicAuth("admin", "password") - json { - "cashin_ratio" to "0.8" - "cashin_fee" to "KUDOS:0.02" - "cashin_tiny_amount" to "KUDOS:0.01" - "cashin_rounding_mode" to "nearest" - "cashin_min_amount" to "EUR:0" - "cashout_ratio" to "1.25" - "cashout_fee" to "EUR:0.003" - "cashout_tiny_amount" to "EUR:0.00000001" - "cashout_rounding_mode" to "zero" - "cashout_min_amount" to "KUDOS:0.1" - } - }.assertNoContent() - - assertException("ERROR: cashin failed: admin balance insufficient") { - db.registerTalerableIncoming(payment, reservePub) - } - - // Allow admin debt - bankCmd.run("edit-account admin --debit_threshold KUDOS:100 -c conf/integration.conf") - - // Too small amount - checkCount(db, 0, 0, 0) - ingestIncomingPayment(db, payment.copy( - amount = TalerAmount("EUR:0.01"), - )) - checkCount(db, 1, 1, 0) - client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { - basicAuth("exchange", "password") - }.assertNoContent() - - // Check success - ingestIncomingPayment(db, IncomingPayment( - amount = TalerAmount("EUR:10"), - debitPaytoUri = userPayTo.canonical, - wireTransferSubject = "Success ${Base32Crockford.encode(randBytes(32))}", - executionTime = Instant.now(), - bankId = "success" - )) - checkCount(db, 2, 1, 1) - client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { - basicAuth("exchange", "password") - }.assertOkJson<BankAccountTransactionsResponse>() - - // TODO check double insert cashin with different subject - } - } - - @Test - fun conversion() { - nexusCmd.run("dbinit -c conf/integration.conf -r") - bankCmd.run("dbinit -c conf/integration.conf -r") - bankCmd.run("passwd admin password -c conf/integration.conf") - bankCmd.run("edit-account admin --debit_threshold KUDOS:1000 -c conf/integration.conf") - bankCmd.run("create-account -c conf/integration.conf -u exchange -p password --name 'Mr Money' --exchange") - nexusCmd.run("dbinit -c conf/integration.conf") // Idempotent - bankCmd.run("dbinit -c conf/integration.conf") // Idempotent - - server { - bankCmd.run("serve -c conf/integration.conf") - } - - setup { db -> - val userPayTo = IbanPayTo(genIbanPaytoUri()) - val fiatPayTo = IbanPayTo(genIbanPaytoUri()) - - // Create user - client.post("http://0.0.0.0:8080/accounts") { - basicAuth("admin", "password") - json { - "username" to "customer" - "password" to "password" - "name" to "JohnSmith" - "internal_payto_uri" to userPayTo - "cashout_payto_uri" to fiatPayTo - "debit_threshold" to "KUDOS:100" - "contact_data" to obj { - "phone" to "+99" - } - } - }.assertOkJson<RegisterAccountResponse>() - - // Set conversion rates - client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { - basicAuth("admin", "password") - json { - "cashin_ratio" to "0.8" - "cashin_fee" to "KUDOS:0.02" - "cashin_tiny_amount" to "KUDOS:0.01" - "cashin_rounding_mode" to "nearest" - "cashin_min_amount" to "EUR:0" - "cashout_ratio" to "1.25" - "cashout_fee" to "EUR:0.003" - "cashout_tiny_amount" to "EUR:0.00000001" - "cashout_rounding_mode" to "zero" - "cashout_min_amount" to "KUDOS:0.1" - } - }.assertNoContent() - - // Cashin - repeat(3) { i -> - val reservePub = randBytes(32); - val amount = TalerAmount("EUR:${20+i}") - val subject = "cashin test $i: ${Base32Crockford.encode(reservePub)}" - ingestIncomingPayment(db, - IncomingPayment( - amount = amount, - debitPaytoUri = userPayTo.canonical, - wireTransferSubject = subject, - executionTime = Instant.now(), - bankId = Base32Crockford.encode(reservePub) - ) - ) - val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}") - .assertOkJson<ConversionResponse>().amount_credit - client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { - basicAuth("exchange", "password") - }.assertOkJson<BankAccountTransactionsResponse> { - val tx = it.transactions.first() - assertEquals(subject, tx.subject) - assertEquals(converted, tx.amount) - } - client.get("http://0.0.0.0:8080/accounts/exchange/taler-wire-gateway/history/incoming") { - basicAuth("exchange", "password") - }.assertOkJson<IncomingHistory> { - val tx = it.incoming_transactions.first() - assertEquals(converted, tx.amount) - assert(Arrays.equals(reservePub, tx.reserve_pub.raw)) - } - } - - // Cashout - repeat(3) { i -> - val requestUid = randBytes(32); - val amount = TalerAmount("KUDOS:${10+i}") - val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount") - .assertOkJson<ConversionResponse>().amount_credit; - client.post("http://0.0.0.0:8080/accounts/customer/cashouts") { - basicAuth("customer", "password") - json { - "request_uid" to ShortHashCode(requestUid) - "amount_debit" to amount - "amount_credit" to convert - } - }.assertOkJson<CashoutResponse>() - } - } - } -} diff --git a/nexus/build.gradle b/nexus/build.gradle @@ -21,8 +21,8 @@ dependencies { // Core language libraries implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") - // LibEuFin util library - implementation(project(":util")) + implementation(project(":common")) + implementation(project(":ebics")) // XML parsing/binding and encryption implementation("javax.xml.bind:jaxb-api:2.3.0") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.postgresql.jdbc.PgConnection import org.postgresql.util.PSQLState -import tech.libeufin.util.* +import tech.libeufin.common.* import java.sql.PreparedStatement import java.sql.SQLException import java.time.Instant diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DbInit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DbInit.kt @@ -21,7 +21,7 @@ package tech.libeufin.nexus import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.groups.* -import tech.libeufin.util.* +import tech.libeufin.common.* /** * This subcommand tries to load the SQL files that define diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt @@ -23,11 +23,9 @@ import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.groups.* import io.ktor.client.* import kotlinx.coroutines.* -import net.taler.wallet.crypto.Base32Crockford -import net.taler.wallet.crypto.EncodingException import tech.libeufin.nexus.ebics.* -import tech.libeufin.util.* -import tech.libeufin.util.ebics_h005.Ebics3Request +import tech.libeufin.common.* +import tech.libeufin.ebics.ebics_h005.Ebics3Request import java.io.File import java.io.IOException import java.nio.file.Path diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt @@ -24,13 +24,13 @@ import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.groups.* import io.ktor.client.* import kotlinx.coroutines.runBlocking -import tech.libeufin.util.ebics_h004.EbicsTypes +import tech.libeufin.ebics.ebics_h004.EbicsTypes import java.io.File -import TalerConfigError import kotlinx.serialization.encodeToString import tech.libeufin.nexus.ebics.* -import tech.libeufin.util.* -import tech.libeufin.util.ebics_h004.HTDResponseOrderData +import tech.libeufin.common.* +import tech.libeufin.ebics.* +import tech.libeufin.ebics.ebics_h004.HTDResponseOrderData import java.time.Instant import kotlin.reflect.typeOf import java.nio.file.Files diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt @@ -28,7 +28,7 @@ import tech.libeufin.nexus.ebics.EbicsSideError import tech.libeufin.nexus.ebics.EbicsSideException import tech.libeufin.nexus.ebics.EbicsUploadException import tech.libeufin.nexus.ebics.submitPain001 -import tech.libeufin.util.* +import tech.libeufin.common.* import java.io.File import java.nio.file.Path import java.time.Instant diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt @@ -18,7 +18,8 @@ */ package tech.libeufin.nexus -import tech.libeufin.util.* +import tech.libeufin.common.* +import tech.libeufin.ebics.* import java.net.URLEncoder import java.time.Instant import java.time.ZoneId diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Log.kt @@ -20,7 +20,7 @@ package tech.libeufin.nexus import tech.libeufin.nexus.ebics.unzipForEach -import tech.libeufin.util.* +import tech.libeufin.common.* import java.io.* import java.nio.file.* import kotlin.io.path.* diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -23,8 +23,7 @@ * kept in their respective files. */ package tech.libeufin.nexus -import ConfigSource -import TalerConfig + import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.parameters.options.versionOption @@ -43,9 +42,8 @@ import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule -import net.taler.wallet.crypto.Base32Crockford import tech.libeufin.nexus.ebics.* -import tech.libeufin.util.* +import tech.libeufin.common.* import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey import java.io.FileNotFoundException diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics2.kt @@ -30,9 +30,9 @@ import org.slf4j.LoggerFactory import tech.libeufin.nexus.BankPublicKeysFile import tech.libeufin.nexus.ClientPrivateKeysFile import tech.libeufin.nexus.EbicsSetupConfig -import tech.libeufin.util.* -import tech.libeufin.util.ebics_h004.* -import tech.libeufin.util.ebics_h005.Ebics3Request +import tech.libeufin.ebics.* +import tech.libeufin.ebics.ebics_h004.* +import tech.libeufin.ebics.ebics_h005.Ebics3Request import java.security.interfaces.RSAPrivateCrtKey import java.time.Instant import java.time.ZoneId diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/Ebics3.kt @@ -23,11 +23,11 @@ import tech.libeufin.nexus.BankPublicKeysFile import tech.libeufin.nexus.ClientPrivateKeysFile import tech.libeufin.nexus.EbicsSetupConfig import tech.libeufin.nexus.logger -import tech.libeufin.util.PreparedUploadData -import tech.libeufin.util.XMLUtil -import tech.libeufin.util.ebics_h005.Ebics3Request -import tech.libeufin.util.getNonce -import tech.libeufin.util.getXmlDate +import tech.libeufin.ebics.PreparedUploadData +import tech.libeufin.ebics.XMLUtil +import tech.libeufin.ebics.ebics_h005.Ebics3Request +import tech.libeufin.ebics.getNonce +import tech.libeufin.ebics.getXmlDate import java.math.BigInteger import java.time.Instant import java.util.* diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -47,8 +47,9 @@ import org.apache.commons.compress.utils.SeekableInMemoryByteChannel import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.nexus.* -import tech.libeufin.util.* -import tech.libeufin.util.ebics_h005.Ebics3Request +import tech.libeufin.common.* +import tech.libeufin.ebics.* +import tech.libeufin.ebics.ebics_h005.Ebics3Request import java.io.ByteArrayOutputStream import java.security.interfaces.RSAPrivateCrtKey import java.time.LocalDateTime diff --git a/nexus/src/test/kotlin/CliTest.kt b/nexus/src/test/kotlin/CliTest.kt @@ -24,7 +24,7 @@ import kotlin.test.* import java.io.* import java.nio.file.* import kotlin.io.path.* -import tech.libeufin.util.* +import tech.libeufin.common.* val nexusCmd = LibeufinNexusCommand() diff --git a/nexus/src/test/kotlin/Common.kt b/nexus/src/test/kotlin/Common.kt @@ -23,7 +23,7 @@ import io.ktor.client.request.* import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import tech.libeufin.nexus.* -import tech.libeufin.util.* +import tech.libeufin.common.* import java.security.interfaces.RSAPrivateCrtKey import java.time.Instant diff --git a/nexus/src/test/kotlin/ConfigLoading.kt b/nexus/src/test/kotlin/ConfigLoading.kt @@ -24,6 +24,7 @@ import tech.libeufin.nexus.NEXUS_CONFIG_SOURCE import tech.libeufin.nexus.getFrequencyInSeconds import kotlin.test.assertEquals import kotlin.test.assertNull +import tech.libeufin.common.* class ConfigLoading { /** diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt @@ -20,7 +20,7 @@ import kotlinx.coroutines.runBlocking import org.junit.Test import tech.libeufin.nexus.* -import tech.libeufin.util.* +import tech.libeufin.common.* import java.time.Instant import kotlin.random.Random import kotlin.test.* diff --git a/nexus/src/test/kotlin/Ebics.kt b/nexus/src/test/kotlin/Ebics.kt @@ -25,8 +25,8 @@ import org.junit.Ignore import org.junit.Test import tech.libeufin.nexus.* import tech.libeufin.nexus.ebics.* -import tech.libeufin.util.XMLUtil -import tech.libeufin.util.ebics_h004.EbicsUnsecuredRequest +import tech.libeufin.ebics.XMLUtil +import tech.libeufin.ebics.ebics_h004.EbicsUnsecuredRequest import java.io.File import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/nexus/src/test/kotlin/Keys.kt b/nexus/src/test/kotlin/Keys.kt @@ -19,7 +19,7 @@ import org.junit.Test import tech.libeufin.nexus.* -import tech.libeufin.util.CryptoUtil +import tech.libeufin.common.CryptoUtil import java.io.File import kotlin.test.* diff --git a/nexus/src/test/kotlin/MySerializers.kt b/nexus/src/test/kotlin/MySerializers.kt @@ -20,13 +20,13 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule -import net.taler.wallet.crypto.Base32Crockford import org.junit.Test import tech.libeufin.nexus.ClientPrivateKeysFile import tech.libeufin.nexus.RSAPrivateCrtKeySerializer -import tech.libeufin.util.CryptoUtil +import tech.libeufin.common.CryptoUtil import java.security.interfaces.RSAPrivateCrtKey import kotlin.test.assertEquals +import tech.libeufin.common.* class MySerializers { // Testing deserialization of RSA private keys. diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt @@ -20,9 +20,9 @@ import org.junit.Test import org.junit.jupiter.api.assertThrows import tech.libeufin.nexus.* -import tech.libeufin.util.* -import tech.libeufin.util.parseBookDate -import tech.libeufin.util.parseCamtTime +import tech.libeufin.common.* +import tech.libeufin.common.parseBookDate +import tech.libeufin.common.parseCamtTime import java.lang.StringBuilder import kotlin.test.assertEquals import kotlin.test.assertNotNull diff --git a/settings.gradle b/settings.gradle @@ -1,5 +1,6 @@ rootProject.name = 'libeufin' include("bank") include("nexus") -include("util") -include("integration") +include("common") +include("testbench") +include("ebics") +\ No newline at end of file diff --git a/testbench/build.gradle b/testbench/build.gradle @@ -0,0 +1,39 @@ +plugins { + id("kotlin") + id("application") +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +compileKotlin.kotlinOptions.jvmTarget = "17" +compileTestKotlin.kotlinOptions.jvmTarget = "17" + +sourceSets.main.java.srcDirs = ["src/main/kotlin"] + +dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") + + implementation(project(":common")) + implementation(project(":bank")) + implementation(project(":nexus")) + + implementation("com.github.ajalt.clikt:clikt:$clikt_version") + + implementation("org.postgresql:postgresql:$postgres_version") + + implementation("io.ktor:ktor-server-test-host:$ktor_version") + implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") + implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") +} + +application { + mainClass = "tech.libeufin.testbench.MainKt" + applicationName = "libeufin-testbench-test" +} + +run { + standardInput = System.in +} +\ No newline at end of file diff --git a/integration/conf/integration.conf b/testbench/conf/integration.conf diff --git a/integration/conf/mini.conf b/testbench/conf/mini.conf diff --git a/integration/conf/netzbon.conf b/testbench/conf/netzbon.conf diff --git a/integration/conf/postfinance.conf b/testbench/conf/postfinance.conf diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -0,0 +1,229 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + * + * 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.testbench + +import tech.libeufin.nexus.Database as NexusDb +import tech.libeufin.nexus.* +import tech.libeufin.bank.* +import tech.libeufin.common.* +import com.github.ajalt.clikt.core.* +import com.github.ajalt.clikt.parameters.arguments.* +import com.github.ajalt.clikt.parameters.types.* +import com.github.ajalt.clikt.testing.* +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import kotlin.test.* +import java.io.File +import java.nio.file.* +import java.time.Instant +import kotlinx.coroutines.runBlocking +import io.ktor.client.request.* +import kotlin.io.path.* + +fun randBytes(lenght: Int): ByteArray { + val bytes = ByteArray(lenght) + kotlin.random.Random.nextBytes(bytes) + return bytes +} + +val nexusCmd = LibeufinNexusCommand() +val client = HttpClient(CIO) + +fun step(name: String) { + println("\u001b[35m$name\u001b[0m") +} + +fun ask(question: String): String? { + print("\u001b[;1m$question\u001b[0m") + System.out.flush() + return readlnOrNull() +} + +fun CliktCommandTestResult.assertOk(msg: String? = null) { + println("$output") + assertEquals(0, statusCode, msg) +} + +fun CliktCommandTestResult.assertErr(msg: String? = null) { + println("$output") + assertEquals(1, statusCode, msg) +} + +enum class Kind { + postfinance, + netzbon +} + +class Cli : CliktCommand("Run integration tests on banks provider") { + val kind: Kind by argument().enum<Kind>() + override fun run() { + val name = kind.name + step("Test init $name") + + runBlocking { + Path("test/$name").createDirectories() + val conf = "conf/$name.conf" + val log = "DEBUG" + val flags = " -c $conf -L $log" + val ebicsFlags = "$flags --transient --debug-ebics test/$name" + val cfg = loadConfig(conf) + + val clientKeysPath = Path(cfg.requireString("nexus-ebics", "client_private_keys_file")) + val bankKeysPath = Path(cfg.requireString("nexus-ebics", "bank_public_keys_file")) + + var hasClientKeys = clientKeysPath.exists() + var hasBankKeys = bankKeysPath.exists() + + if (ask("Reset DB ? y/n>") == "y") nexusCmd.test("dbinit -r $flags").assertOk() + else nexusCmd.test("dbinit $flags").assertOk() + val nexusDb = NexusDb("postgresql:///libeufincheck") + + when (kind) { + Kind.postfinance -> { + if (hasClientKeys || hasBankKeys) { + if (ask("Reset keys ? y/n>") == "y") { + if (hasClientKeys) clientKeysPath.deleteIfExists() + if (hasBankKeys) bankKeysPath.deleteIfExists() + hasClientKeys = false + hasBankKeys = false + } + } + + if (!hasClientKeys) { + step("Test INI order") + ask("Got to https://isotest.postfinance.ch/corporates/user/settings/ebics and click on 'Reset EBICS user'.\nPress Enter when done>") + nexusCmd.test("ebics-setup $flags") + .assertErr("ebics-setup should failed the first time") + } + + if (!hasBankKeys) { + step("Test HIA order") + ask("Got to https://isotest.postfinance.ch/corporates/user/settings/ebics and click on 'Activate EBICS user'.\nPress Enter when done>") + nexusCmd.test("ebics-setup --auto-accept-keys $flags") + .assertOk("ebics-setup should succeed the second time") + } + + val payto = "payto://iban/CH2989144971918294289?receiver-name=Test" + + step("Test fetch transactions") + nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01").assertOk() + + while (true) { + when (ask("Run 'fetch', 'submit', 'tx', 'txs', 'logs', 'ack' or 'exit'>")) { + "fetch" -> { + step("Fetch new transactions") + nexusCmd.test("ebics-fetch $ebicsFlags").assertOk() + } + "tx" -> { + step("Test submit one transaction") + nexusDb.initiatedPaymentCreate(InitiatedPayment( + amount = TalerAmount("CFH:42"), + creditPaytoUri = payto, + wireTransferSubject = "single transaction test", + initiationTime = Instant.now(), + requestUid = Base32Crockford.encode(randBytes(16)) + )) + nexusCmd.test("ebics-submit $ebicsFlags").assertOk() + } + "txs" -> { + step("Test submit many transaction") + repeat(4) { + nexusDb.initiatedPaymentCreate(InitiatedPayment( + amount = TalerAmount("CFH:${100L+it}"), + creditPaytoUri = payto, + wireTransferSubject = "multi transaction test $it", + initiationTime = Instant.now(), + requestUid = Base32Crockford.encode(randBytes(16)) + )) + } + nexusCmd.test("ebics-submit $ebicsFlags").assertOk() + } + "submit" -> { + step("Submit pending transactions") + nexusCmd.test("ebics-submit $ebicsFlags").assertOk() + } + "logs" -> { + step("Fetch logs") + nexusCmd.test("ebics-fetch $ebicsFlags --only-logs").assertOk() + } + "ack" -> { + step("Fetch ack") + nexusCmd.test("ebics-fetch $ebicsFlags --only-ack").assertOk() + } + "exit" -> break + } + } + } + Kind.netzbon -> { + if (!hasClientKeys) + throw Exception("Clients keys are required to run netzbon tests") + + if (!hasBankKeys) { + step("Test HIA order") + nexusCmd.test("ebics-setup --auto-accept-keys $flags").assertOk("ebics-setup should succeed the second time") + } + + step("Test fetch transactions") + nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01").assertOk() + + while (true) { + when (ask("Run 'fetch', 'submit', 'logs', 'ack' or 'exit'>")) { + "fetch" -> { + step("Fetch new transactions") + nexusCmd.test("ebics-fetch $ebicsFlags").assertOk() + } + "submit" -> { + step("Submit pending transactions") + nexusCmd.test("ebics-submit $ebicsFlags").assertOk() + } + "tx" -> { + step("Submit new transaction") + // TODO interactive payment editor + nexusDb.initiatedPaymentCreate(InitiatedPayment( + amount = TalerAmount("CFH:1.1"), + creditPaytoUri = "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans", + wireTransferSubject = "single transaction test", + initiationTime = Instant.now(), + requestUid = Base32Crockford.encode(randBytes(16)) + )) + nexusCmd.test("ebics-submit $ebicsFlags").assertOk() + } + "logs" -> { + step("Fetch logs") + nexusCmd.test("ebics-fetch $ebicsFlags --only-logs").assertOk() + } + "ack" -> { + step("Fetch ack") + nexusCmd.test("ebics-fetch $ebicsFlags --only-ack").assertOk() + } + "exit" -> break + } + } + } + } + } + + step("Test succeed") + } +} + +fun main(args: Array<String>) { + Cli().main(args) +} diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt @@ -0,0 +1,325 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + * + * 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/> + */ + +import org.junit.Test +import tech.libeufin.bank.* +import tech.libeufin.nexus.* +import tech.libeufin.nexus.Database as NexusDb +import tech.libeufin.bank.db.AccountDAO.* +import tech.libeufin.common.* +import java.io.File +import java.time.Instant +import java.util.Arrays +import java.sql.SQLException +import kotlinx.coroutines.runBlocking +import com.github.ajalt.clikt.testing.test +import com.github.ajalt.clikt.core.CliktCommand +import org.postgresql.jdbc.PgConnection +import kotlin.test.* +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.HttpStatusCode + +fun CliktCommand.run(cmd: String) { + val result = test(cmd) + if (result.statusCode != 0) + throw Exception(result.output) + println(result.output) +} + +fun HttpResponse.assertNoContent() { + assertEquals(HttpStatusCode.NoContent, this.status) +} + +fun randBytes(lenght: Int): ByteArray { + val bytes = ByteArray(lenght) + kotlin.random.Random.nextBytes(bytes) + return bytes +} + +fun server(lambda: () -> Unit) { + // Start the HTTP server in another thread + kotlin.concurrent.thread(isDaemon = true) { + lambda() + } + // Wait for the HTTP server to be up + runBlocking { + HttpClient(CIO) { + install(HttpRequestRetry) { + maxRetries = 10 + constantDelay(200, 100) + } + }.get("http://0.0.0.0:8080/config") + } + +} + +fun setup(lambda: suspend (NexusDb) -> Unit) { + try { + runBlocking { + NexusDb("postgresql:///libeufincheck").use { + lambda(it) + } + } + } finally { + engine?.stop(0, 0) // Stop http server if started + } +} + +inline fun assertException(msg: String, lambda: () -> Unit) { + try { + lambda() + throw Exception("Expected failure: $msg") + } catch (e: Exception) { + assert(e.message!!.startsWith(msg)) { "${e.message}" } + } +} + +class IntegrationTest { + val nexusCmd = LibeufinNexusCommand() + val bankCmd = LibeufinBankCommand(); + val client = HttpClient(CIO) + + @Test + fun mini() { + bankCmd.run("dbinit -c conf/mini.conf -r") + bankCmd.run("passwd admin password -c conf/mini.conf") + bankCmd.run("dbinit -c conf/mini.conf") // Indempotent + + server { + bankCmd.run("serve -c conf/mini.conf") + } + + setup { _ -> + // Check bank is running + client.get("http://0.0.0.0:8080/public-accounts").assertNoContent() + } + } + + @Test + fun errors() { + nexusCmd.run("dbinit -c conf/integration.conf -r") + bankCmd.run("dbinit -c conf/integration.conf -r") + bankCmd.run("passwd admin password -c conf/integration.conf") + + suspend fun checkCount(db: NexusDb, nbIncoming: Int, nbBounce: Int, nbTalerable: Int) { + db.conn { conn -> + conn.prepareStatement("SELECT count(*) FROM incoming_transactions").oneOrNull { + assertEquals(nbIncoming, it.getInt(1)) + } + conn.prepareStatement("SELECT count(*) FROM bounced_transactions").oneOrNull { + assertEquals(nbBounce, it.getInt(1)) + } + conn.prepareStatement("SELECT count(*) FROM talerable_incoming_transactions").oneOrNull { + assertEquals(nbTalerable, it.getInt(1)) + } + } + } + + setup { db -> + val userPayTo = IbanPayTo(genIbanPaytoUri()) + val fiatPayTo = IbanPayTo(genIbanPaytoUri()) + + // Load conversion setup manually as the server would refuse to start without an exchange account + val sqlProcedures = File("../database-versioning/libeufin-conversion-setup.sql") + db.conn { + it.execSQLUpdate(sqlProcedures.readText()) + it.execSQLUpdate("SET search_path TO libeufin_nexus;") + } + + val reservePub = randBytes(32) + val payment = IncomingPayment( + amount = TalerAmount("EUR:10"), + debitPaytoUri = userPayTo.canonical, + wireTransferSubject = "Error test ${Base32Crockford.encode(reservePub)}", + executionTime = Instant.now(), + bankId = "error" + ) + + assertException("ERROR: cashin failed: missing exchange account") { + ingestIncomingPayment(db, payment) + } + + // Create exchange account + bankCmd.run("create-account -c conf/integration.conf -u exchange -p password --name 'Mr Money' --exchange") + + assertException("ERROR: cashin currency conversion failed: missing conversion rates") { + ingestIncomingPayment(db, payment) + } + + // Start server + server { + bankCmd.run("serve -c conf/integration.conf") + } + + // Set conversion rates + client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { + basicAuth("admin", "password") + json { + "cashin_ratio" to "0.8" + "cashin_fee" to "KUDOS:0.02" + "cashin_tiny_amount" to "KUDOS:0.01" + "cashin_rounding_mode" to "nearest" + "cashin_min_amount" to "EUR:0" + "cashout_ratio" to "1.25" + "cashout_fee" to "EUR:0.003" + "cashout_tiny_amount" to "EUR:0.00000001" + "cashout_rounding_mode" to "zero" + "cashout_min_amount" to "KUDOS:0.1" + } + }.assertNoContent() + + assertException("ERROR: cashin failed: admin balance insufficient") { + db.registerTalerableIncoming(payment, reservePub) + } + + // Allow admin debt + bankCmd.run("edit-account admin --debit_threshold KUDOS:100 -c conf/integration.conf") + + // Too small amount + checkCount(db, 0, 0, 0) + ingestIncomingPayment(db, payment.copy( + amount = TalerAmount("EUR:0.01"), + )) + checkCount(db, 1, 1, 0) + client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { + basicAuth("exchange", "password") + }.assertNoContent() + + // Check success + ingestIncomingPayment(db, IncomingPayment( + amount = TalerAmount("EUR:10"), + debitPaytoUri = userPayTo.canonical, + wireTransferSubject = "Success ${Base32Crockford.encode(randBytes(32))}", + executionTime = Instant.now(), + bankId = "success" + )) + checkCount(db, 2, 1, 1) + client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { + basicAuth("exchange", "password") + }.assertOkJson<BankAccountTransactionsResponse>() + + // TODO check double insert cashin with different subject + } + } + + @Test + fun conversion() { + nexusCmd.run("dbinit -c conf/integration.conf -r") + bankCmd.run("dbinit -c conf/integration.conf -r") + bankCmd.run("passwd admin password -c conf/integration.conf") + bankCmd.run("edit-account admin --debit_threshold KUDOS:1000 -c conf/integration.conf") + bankCmd.run("create-account -c conf/integration.conf -u exchange -p password --name 'Mr Money' --exchange") + nexusCmd.run("dbinit -c conf/integration.conf") // Idempotent + bankCmd.run("dbinit -c conf/integration.conf") // Idempotent + + server { + bankCmd.run("serve -c conf/integration.conf") + } + + setup { db -> + val userPayTo = IbanPayTo(genIbanPaytoUri()) + val fiatPayTo = IbanPayTo(genIbanPaytoUri()) + + // Create user + client.post("http://0.0.0.0:8080/accounts") { + basicAuth("admin", "password") + json { + "username" to "customer" + "password" to "password" + "name" to "JohnSmith" + "internal_payto_uri" to userPayTo + "cashout_payto_uri" to fiatPayTo + "debit_threshold" to "KUDOS:100" + "contact_data" to obj { + "phone" to "+99" + } + } + }.assertOkJson<RegisterAccountResponse>() + + // Set conversion rates + client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { + basicAuth("admin", "password") + json { + "cashin_ratio" to "0.8" + "cashin_fee" to "KUDOS:0.02" + "cashin_tiny_amount" to "KUDOS:0.01" + "cashin_rounding_mode" to "nearest" + "cashin_min_amount" to "EUR:0" + "cashout_ratio" to "1.25" + "cashout_fee" to "EUR:0.003" + "cashout_tiny_amount" to "EUR:0.00000001" + "cashout_rounding_mode" to "zero" + "cashout_min_amount" to "KUDOS:0.1" + } + }.assertNoContent() + + // Cashin + repeat(3) { i -> + val reservePub = randBytes(32); + val amount = TalerAmount("EUR:${20+i}") + val subject = "cashin test $i: ${Base32Crockford.encode(reservePub)}" + ingestIncomingPayment(db, + IncomingPayment( + amount = amount, + debitPaytoUri = userPayTo.canonical, + wireTransferSubject = subject, + executionTime = Instant.now(), + bankId = Base32Crockford.encode(reservePub) + ) + ) + val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}") + .assertOkJson<ConversionResponse>().amount_credit + client.get("http://0.0.0.0:8080/accounts/exchange/transactions") { + basicAuth("exchange", "password") + }.assertOkJson<BankAccountTransactionsResponse> { + val tx = it.transactions.first() + assertEquals(subject, tx.subject) + assertEquals(converted, tx.amount) + } + client.get("http://0.0.0.0:8080/accounts/exchange/taler-wire-gateway/history/incoming") { + basicAuth("exchange", "password") + }.assertOkJson<IncomingHistory> { + val tx = it.incoming_transactions.first() + assertEquals(converted, tx.amount) + assert(Arrays.equals(reservePub, tx.reserve_pub.raw)) + } + } + + // Cashout + repeat(3) { i -> + val requestUid = randBytes(32); + val amount = TalerAmount("KUDOS:${10+i}") + val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount") + .assertOkJson<ConversionResponse>().amount_credit; + client.post("http://0.0.0.0:8080/accounts/customer/cashouts") { + basicAuth("customer", "password") + json { + "request_uid" to ShortHashCode(requestUid) + "amount_debit" to amount + "amount_credit" to convert + } + }.assertOkJson<CashoutResponse>() + } + } + } +} diff --git a/util/build.gradle b/util/build.gradle @@ -1,37 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -plugins { - id("java") - id("kotlin") - id("org.jetbrains.kotlin.plugin.serialization") version "$kotlin_version" -} - -version = rootProject.version - -java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 -} - -compileKotlin.kotlinOptions.jvmTarget = "17" -compileTestKotlin.kotlinOptions.jvmTarget = "17" - -sourceSets.main.java.srcDirs = ["src/main/kotlin"] - -dependencies { - implementation("ch.qos.logback:logback-classic:1.4.5") - // XML Stuff - implementation("javax.xml.bind:jaxb-api:2.3.1") - implementation("org.glassfish.jaxb:jaxb-runtime:2.3.1") - implementation("org.apache.santuario:xmlsec:2.2.2") - // Crypto - implementation("org.bouncycastle:bcprov-jdk15on:1.69") - // Database helper - implementation("org.postgresql:postgresql:$postgres_version") - implementation("com.zaxxer:HikariCP:5.0.1") - - implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") - implementation("io.ktor:ktor-server-test-host:$ktor_version") - implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version") - implementation("com.github.ajalt.clikt:clikt:$clikt_version") -} -\ No newline at end of file diff --git a/util/import.py b/util/import.py @@ -1,66 +0,0 @@ -# Update EBICS constants file using latest external code sets files - -import requests -from zipfile import ZipFile -from io import BytesIO -import polars as pl - -# Get XLSX zip file from server -r = requests.get( - "https://www.iso20022.org/sites/default/files/media/file/ExternalCodeSets_XLSX.zip" -) -assert r.status_code == 200 - -# Unzip the XLSX file -zip = ZipFile(BytesIO(r.content)) -files = zip.namelist() -assert len(files) == 1 -file = zip.open(files[0]) - -# Parse excel -df = pl.read_excel(file, sheet_name="AllCodeSets") - -def extractCodeSet(setName: str, className: str) -> str: - out = f"enum class {className}(val isoCode: String, val description: String) {{" - - for row in df.filter(pl.col("Code Set") == setName).sort("Code Value").rows(named=True): - (value, isoCode, description) = ( - row["Code Value"], - row["Code Name"], - row["Code Definition"].split("\n", 1)[0].strip(), - ) - out += f'\n\t{value}("{isoCode}", "{description}"),' - - out += "\n}" - return out - -# Write kotlin file -kt = f"""/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -// THIS FILE IS GENERATED, DO NOT EDIT - -package tech.libeufin.util - -{extractCodeSet("ExternalStatusReason1Code", "ExternalStatusReasonCode")} - -{extractCodeSet("ExternalPaymentGroupStatus1Code", "ExternalPaymentGroupStatusCode")} -""" -with open("src/main/kotlin/EbicsCodeSets.kt", "w") as file1: - file1.write(kt) diff --git a/util/src/main/kotlin/Backoff.kt b/util/src/main/kotlin/Backoff.kt @@ -1,41 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - - * 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.util - -import kotlin.random.Random - -/** Infinite exponential backoff with decorrelated jitter */ -class ExpoBackoffDecorr( - private val base: Long = 100, // 0.1 second - private val max: Long = 5000, // 5 second - private val factor: Double = 2.0, -) { - private var sleep: Long = base - - public fun next() : Long { - sleep = Random.nextDouble(base.toDouble(), sleep.toDouble() * factor) - .toLong().coerceAtMost(max) - return sleep - } - - public fun reset() { - sleep = base - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/Cli.kt b/util/src/main/kotlin/Cli.kt @@ -1,134 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - - * 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.util - -import ConfigSource -import TalerConfig -import TalerConfigError -import com.github.ajalt.clikt.core.* -import com.github.ajalt.clikt.parameters.types.* -import com.github.ajalt.clikt.parameters.arguments.* -import com.github.ajalt.clikt.parameters.options.* -import com.github.ajalt.clikt.parameters.groups.* -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.slf4j.event.Level - -private val logger: Logger = LoggerFactory.getLogger("libeufin-config") - -fun cliCmd(logger: Logger, level: Level, lambda: () -> Unit) { - // Set root log level - val root = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger - root.setLevel(ch.qos.logback.classic.Level.convertAnSLF4JLevel(level)); - // Run cli command catching all errors - try { - lambda() - } catch (e: Throwable) { - var msg = StringBuilder(e.message) - var cause = e.cause; - while (cause != null) { - msg.append(": ") - msg.append(cause.message) - cause = cause.cause - } - logger.error(msg.toString()) - logger.debug("$e", e) - throw ProgramResult(1) - } -} - -private fun talerConfig(configSource: ConfigSource, configPath: String?): TalerConfig { - val config = TalerConfig(configSource) - config.load(configPath) - return config -} - -class CommonOption: OptionGroup() { - val config by option( - "--config", "-c", - help = "Specifies the configuration file" - ).path( - mustExist = true, - canBeDir = false, - mustBeReadable = true, - ).convert { it.toString() } // TODO take path to load config - val log by option( - "--log", "-L", - help = "Configure logging to use LOGLEVEL" - ).enum<Level>().default(Level.INFO) -} - -class CliConfigCmd(configSource: ConfigSource) : CliktCommand("Inspect or change the configuration", name = "config") { - init { - subcommands(CliConfigDump(configSource), CliConfigPathsub(configSource), CliConfigGet(configSource)) - } - - override fun run() = Unit -} - -private class CliConfigGet(private val configSource: ConfigSource) : CliktCommand("Lookup config value", name = "get") { - private val common by CommonOption() - private val isPath by option( - "--filename", "-f", - help = "Interpret value as path with dollar-expansion" - ).flag() - private val sectionName by argument() - private val optionName by argument() - - - override fun run() = cliCmd(logger, common.log) { - val config = talerConfig(configSource, common.config) - if (isPath) { - val res = config.lookupPath(sectionName, optionName) - if (res == null) { - throw Exception("value not found in config") - } - println(res) - } else { - val res = config.lookupString(sectionName, optionName) - if (res == null) { - throw Exception("value not found in config") - } - println(res) - } - } -} - - - -private class CliConfigPathsub(private val configSource: ConfigSource) : CliktCommand("Substitute variables in a path", name = "pathsub") { - private val common by CommonOption() - private val pathExpr by argument() - - override fun run() = cliCmd(logger, common.log) { - val config = talerConfig(configSource, common.config) - println(config.pathsub(pathExpr)) - } -} - -private class CliConfigDump(private val configSource: ConfigSource) : CliktCommand("Dump the configuration", name = "dump") { - private val common by CommonOption() - - override fun run() = cliCmd(logger, common.log) { - val config = talerConfig(configSource, common.config) - println("# install path: ${config.getInstallPath()}") - println(config.stringify()) - } -} diff --git a/util/src/main/kotlin/Client.kt b/util/src/main/kotlin/Client.kt @@ -1,81 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - - * 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.util - -import io.ktor.http.* -import kotlinx.serialization.json.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import java.io.ByteArrayOutputStream -import java.util.zip.DeflaterOutputStream -import kotlin.test.assertEquals -import net.taler.common.errorcodes.TalerErrorCode - -/* ----- Json DSL ----- */ - -inline fun obj(from: JsonObject = JsonObject(emptyMap()), builderAction: JsonBuilder.() -> Unit): JsonObject { - val builder = JsonBuilder(from) - builder.apply(builderAction) - return JsonObject(builder.content) -} - -class JsonBuilder(from: JsonObject) { - val content: MutableMap<String, JsonElement> = from.toMutableMap() - - infix inline fun <reified T> String.to(v: T) { - val json = Json.encodeToJsonElement(kotlinx.serialization.serializer<T>(), v); - content.put(this, json) - } -} - -/* ----- Json body helper ----- */ - -inline fun <reified B> HttpRequestBuilder.json(b: B, deflate: Boolean = false) { - val json = Json.encodeToString(kotlinx.serialization.serializer<B>(), b); - contentType(ContentType.Application.Json) - if (deflate) { - headers.set("Content-Encoding", "deflate") - val bos = ByteArrayOutputStream() - val ios = DeflaterOutputStream(bos) - ios.write(json.toByteArray()) - ios.finish() - setBody(bos.toByteArray()) - } else { - setBody(json) - } -} - -inline fun HttpRequestBuilder.json( - from: JsonObject = JsonObject(emptyMap()), - deflate: Boolean = false, - builderAction: JsonBuilder.() -> Unit -) { - json(obj(from, builderAction), deflate) -} - -inline suspend fun <reified B> HttpResponse.json(): B = - Json.decodeFromString(kotlinx.serialization.serializer<B>(), bodyAsText()) - -inline suspend fun <reified B> HttpResponse.assertOkJson(lambda: (B) -> Unit = {}): B { - assertEquals(HttpStatusCode.OK, status) - val body = json<B>() - lambda(body) - return body -} -\ No newline at end of file diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt @@ -1,36 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import ch.qos.logback.core.util.Loader - -/** - * Putting those values into the 'attributes' container because they - * are needed by the util routines that do NOT have Sandbox and Nexus - * as dependencies, and therefore cannot access their global variables. - * - * Note: putting Sandbox and Nexus as Utils dependencies would result - * into circular dependency. - */ -fun getVersion(): String { - return Loader.getResource( - "version.txt", ClassLoader.getSystemClassLoader() - ).readText() -} -\ No newline at end of file diff --git a/util/src/main/kotlin/Constants.kt b/util/src/main/kotlin/Constants.kt @@ -1,23 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -// DB -const val MIN_VERSION: Int = 14 -const val SERIALIZATION_RETRY: Int = 10; -\ No newline at end of file diff --git a/util/src/main/kotlin/CryptoUtil.kt b/util/src/main/kotlin/CryptoUtil.kt @@ -1,330 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import net.taler.wallet.crypto.Base32Crockford -import org.bouncycastle.jce.provider.BouncyCastleProvider -import java.io.ByteArrayOutputStream -import java.math.BigInteger -import java.security.* -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.security.spec.* -import javax.crypto.* -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.PBEKeySpec -import javax.crypto.spec.PBEParameterSpec -import javax.crypto.spec.SecretKeySpec - -/** - * Helpers for dealing with cryptographic operations in EBICS / LibEuFin. - */ -object CryptoUtil { - - /** - * RSA key pair. - */ - data class RsaCrtKeyPair(val private: RSAPrivateCrtKey, val public: RSAPublicKey) - - // FIXME(dold): This abstraction needs to be improved. - class EncryptionResult( - val encryptedTransactionKey: ByteArray, - val pubKeyDigest: ByteArray, - val encryptedData: ByteArray, - /** - * This key needs to be reused between different upload phases. - */ - val plainTransactionKey: SecretKey? = null - ) - - 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.toUnsignedHexString().lowercase().trimStart('0').toByteArray()) - keyBytes.write(' '.code) - keyBytes.writeBytes(publicKey.modulus.toUnsignedHexString().lowercase().trimStart('0').toByteArray()) - // println("buffer before hashing: '${keyBytes.toString(Charsets.UTF_8)}'") - val digest = MessageDigest.getInstance("SHA-256") - return digest.digest(keyBytes.toByteArray()) - } - - fun encryptEbicsE002(data: ByteArray, encryptionPublicKey: RSAPublicKey): EncryptionResult { - val keygen = KeyGenerator.getInstance("AES", bouncyCastleProvider) - keygen.init(128) - val transactionKey = keygen.generateKey() - return encryptEbicsE002withTransactionKey( - data, - encryptionPublicKey, - transactionKey - ) - } - /** - * Encrypt data according to the EBICS E002 encryption process. - */ - fun encryptEbicsE002withTransactionKey( - data: ByteArray, - encryptionPublicKey: RSAPublicKey, - transactionKey: SecretKey - ): EncryptionResult { - 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, - transactionKey - ) - } - - fun decryptEbicsE002(enc: EncryptionResult, privateKey: RSAPrivateCrtKey): ByteArray { - return decryptEbicsE002( - enc.encryptedTransactionKey, - enc.encryptedData, - privateKey - ) - } - - fun decryptEbicsE002( - encryptedTransactionKey: ByteArray, - encryptedData: ByteArray, - privateKey: RSAPrivateCrtKey - ): ByteArray { - val asymmetricCipher = Cipher.getInstance( - "RSA/None/PKCS1Padding", - bouncyCastleProvider - ) - asymmetricCipher.init(Cipher.DECRYPT_MODE, privateKey) - val transactionKeyBytes = asymmetricCipher.doFinal(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(encryptedData) - return data - } - - /** - * Signing algorithm corresponding to the EBICS A006 signing process. - * - * Note that while [data] can be arbitrary-length data, in EBICS, the order - * data is *always* hashed *before* passing it to the signing algorithm, which again - * uses a hash internally. - */ - fun signEbicsA006(data: ByteArray, privateKey: RSAPrivateCrtKey): ByteArray { - val signature = Signature.getInstance("SHA256withRSA/PSS", bouncyCastleProvider) - signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) - signature.initSign(privateKey) - signature.update(data) - return signature.sign() - } - - fun verifyEbicsA006(sig: ByteArray, data: ByteArray, publicKey: RSAPublicKey): Boolean { - val signature = Signature.getInstance("SHA256withRSA/PSS", bouncyCastleProvider) - signature.setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1)) - signature.initVerify(publicKey) - signature.update(data) - return signature.verify(sig) - } - - fun digestEbicsOrderA006(orderData: ByteArray): ByteArray { - val digest = MessageDigest.getInstance("SHA-256") - for (b in orderData) { - when (b) { - '\r'.code.toByte(), '\n'.code.toByte(), (26).toByte() -> Unit - else -> digest.update(b) - } - } - return digest.digest() - } - - fun decryptKey(data: EncryptedPrivateKeyInfo, passphrase: String): RSAPrivateCrtKey { - /* make key out of passphrase */ - val pbeKeySpec = PBEKeySpec(passphrase.toCharArray()) - val keyFactory = SecretKeyFactory.getInstance(data.algName) - val secretKey = keyFactory.generateSecret(pbeKeySpec) - /* Make a cipher */ - val cipher = Cipher.getInstance(data.algName) - cipher.init( - Cipher.DECRYPT_MODE, - secretKey, - data.algParameters // has hash count and salt - ) - /* Ready to decrypt */ - val decryptedKeySpec: PKCS8EncodedKeySpec = data.getKeySpec(cipher) - val priv = KeyFactory.getInstance("RSA").generatePrivate(decryptedKeySpec) - if (priv !is RSAPrivateCrtKey) - throw Exception("wrong encoding") - return priv - } - - fun encryptKey(data: ByteArray, passphrase: String): ByteArray { - /* Cipher parameters: salt and hash count */ - val hashIterations = 30 - val salt = ByteArray(8) - SecureRandom().nextBytes(salt) - val pbeParameterSpec = PBEParameterSpec(salt, hashIterations) - /* *Other* cipher parameters: symmetric key (from password) */ - val pbeAlgorithm = "PBEWithSHA1AndDESede" - val pbeKeySpec = PBEKeySpec(passphrase.toCharArray()) - val keyFactory = SecretKeyFactory.getInstance(pbeAlgorithm) - val secretKey = keyFactory.generateSecret(pbeKeySpec) - /* Make a cipher */ - val cipher = Cipher.getInstance(pbeAlgorithm) - cipher.init(Cipher.ENCRYPT_MODE, secretKey, pbeParameterSpec) - /* ready to encrypt now */ - val cipherText = cipher.doFinal(data) - /* Must now bundle a PKCS#8-compatible object, that contains - * algorithm, salt and hash count information */ - val bundleAlgorithmParams = AlgorithmParameters.getInstance(pbeAlgorithm) - bundleAlgorithmParams.init(pbeParameterSpec) - val bundle = EncryptedPrivateKeyInfo(bundleAlgorithmParams, cipherText) - return bundle.encoded - } - - fun checkValidEddsaPublicKey(enc: String): Boolean { - val data = try { - Base32Crockford.decode(enc) - } catch (e: Exception) { - return false - } - if (data.size != 32) { - return false - } - return true - } - - fun hashStringSHA256(input: String): ByteArray { - return MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8)) - } - - fun hashpw(pw: String): String { - val saltBytes = ByteArray(8) - SecureRandom().nextBytes(saltBytes) - val salt = bytesToBase64(saltBytes) - val pwh = bytesToBase64(CryptoUtil.hashStringSHA256("$salt|$pw")) - return "sha256-salted\$$salt\$$pwh" - } - - fun checkpw(pw: String, storedPwHash: String): Boolean { - val components = storedPwHash.split('$') - when (val algo = components[0]) { - "sha256" -> { // Support legacy unsalted passwords - if (components.size != 2) throw Exception("bad password hash") - val hash = components[1] - val pwh = bytesToBase64(CryptoUtil.hashStringSHA256(pw)) - return pwh == hash - } - "sha256-salted" -> { - if (components.size != 3) throw Exception("bad password hash") - val salt = components[1] - val hash = components[2] - val pwh = bytesToBase64(CryptoUtil.hashStringSHA256("$salt|$pw")) - return pwh == hash - } - else -> throw Exception("unsupported hash algo: '$algo'") - } - } -} diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt @@ -1,339 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - * - * 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.util - -import org.postgresql.ds.PGSimpleDataSource -import org.postgresql.jdbc.PgConnection -import org.postgresql.util.PSQLState -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.File -import java.net.URI -import java.sql.PreparedStatement -import java.sql.ResultSet -import java.sql.SQLException -import kotlinx.coroutines.* -import com.zaxxer.hikari.* - -fun getCurrentUser(): String = System.getProperty("user.name") - -private val logger: Logger = LoggerFactory.getLogger("libeufin-db") - -// Check GANA (https://docs.gnunet.org/gana/index.html) for numbers allowance. - -/** - * This function converts postgresql:// URIs to JDBC URIs. - * - * URIs that are already jdbc: URIs are passed through. - * - * This avoids the user having to create complex JDBC URIs for postgres connections. - * They are especially complex when using unix domain sockets, as they're not really - * supported natively by JDBC. - */ -fun getJdbcConnectionFromPg(pgConn: String): String { - // Pass through jdbc URIs. - if (pgConn.startsWith("jdbc:")) { - return pgConn - } - if (!pgConn.startsWith("postgresql://") && !pgConn.startsWith("postgres://")) { - logger.info("Not a Postgres connection string: $pgConn") - throw Exception("Not a Postgres connection string: $pgConn") - } - var maybeUnixSocket = false - val parsed = URI(pgConn) - val hostAsParam: String? = if (parsed.query != null) { - getQueryParam(parsed.query, "host") - } else { - null - } - /** - * In some cases, it is possible to leave the hostname empty - * and specify it via a query param, therefore a "postgresql:///"-starting - * connection string does NOT always mean Unix domain socket. - * https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING - */ - if (parsed.host == null && - (hostAsParam == null || hostAsParam.startsWith('/')) - ) { - maybeUnixSocket = true - } - if (maybeUnixSocket) { - // Check whether the database user should differ from the process user. - var pgUser = getCurrentUser() - if (parsed.query != null) { - val maybeUserParam = getQueryParam(parsed.query, "user") - if (maybeUserParam != null) pgUser = maybeUserParam - } - // Check whether the Unix domain socket location was given non-standard. - val socketLocation = hostAsParam ?: "/var/run/postgresql/.s.PGSQL.5432" - if (!socketLocation.startsWith('/')) { - throw Exception("PG connection wants Unix domain socket, but non-null host doesn't start with slash") - } - return "jdbc:postgresql://localhost${parsed.path}?user=$pgUser&socketFactory=org.newsclub.net.unix." + - "AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=$socketLocation" - } - if (pgConn.startsWith("postgres://")) { - // The JDBC driver doesn't like postgres://, only postgresql://. - // For consistency with other components, we normalize the postgres:// URI - // into one that the JDBC driver likes. - return "jdbc:postgresql://" + pgConn.removePrefix("postgres://") - } - return "jdbc:$pgConn" -} - -data class DatabaseConfig( - val dbConnStr: String, - val sqlDir: String -) - -fun pgDataSource(dbConfig: String): PGSimpleDataSource { - val jdbcConnStr = getJdbcConnectionFromPg(dbConfig) - logger.info("connecting to database via JDBC string '$jdbcConnStr'") - val pgSource = PGSimpleDataSource() - pgSource.setUrl(jdbcConnStr) - pgSource.prepareThreshold = 1 - return pgSource -} - -fun PGSimpleDataSource.pgConnection(): PgConnection { - val conn = connection.unwrap(PgConnection::class.java) - // FIXME: bring the DB schema to a function argument. - conn.execSQLUpdate("SET search_path TO libeufin_bank;") - return conn -} - -fun <R> PgConnection.transaction(lambda: (PgConnection) -> R): R { - try { - setAutoCommit(false); - val result = lambda(this) - commit(); - setAutoCommit(true); - return result - } catch(e: Exception){ - rollback(); - setAutoCommit(true); - throw e; - } -} - -fun <T> PreparedStatement.oneOrNull(lambda: (ResultSet) -> T): T? { - executeQuery().use { - if (!it.next()) return null - return lambda(it) - } -} - -fun <T> PreparedStatement.all(lambda: (ResultSet) -> T): List<T> { - executeQuery().use { - val ret = mutableListOf<T>() - while (it.next()) { - ret.add(lambda(it)) - } - return ret - } -} - -fun PreparedStatement.executeQueryCheck(): Boolean { - executeQuery().use { - return it.next() - } -} - -fun PreparedStatement.executeUpdateCheck(): Boolean { - executeUpdate() - return updateCount > 0 -} - -/** - * Helper that returns false if the row to be inserted - * hits a unique key constraint violation, true when it - * succeeds. Any other error (re)throws exception. - */ -fun PreparedStatement.executeUpdateViolation(): Boolean { - return try { - executeUpdateCheck() - } catch (e: SQLException) { - logger.debug(e.message) - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false - throw e // rethrowing, not to hide other types of errors. - } -} - -fun PreparedStatement.executeProcedureViolation(): Boolean { - val savepoint = connection.setSavepoint(); - return try { - executeUpdate() - connection.releaseSavepoint(savepoint) - true - } catch (e: SQLException) { - connection.rollback(savepoint); - if (e.sqlState == PSQLState.UNIQUE_VIOLATION.state) return false - throw e // rethrowing, not to hide other types of errors. - } -} - -// TODO comment -fun PgConnection.dynamicUpdate( - table: String, - fields: Sequence<String>, - filter: String, - bind: Sequence<Any?>, -) { - val sql = fields.joinToString() - if (sql.isEmpty()) return - prepareStatement("UPDATE $table SET $sql $filter").run { - for ((idx, value) in bind.withIndex()) { - setObject(idx+1, value) - } - executeUpdate() - } -} - -/** - * Only runs versioning.sql if the _v schema is not found. - * - * @param conn database connection - * @param cfg database configuration - */ -fun maybeApplyV(conn: PgConnection, cfg: DatabaseConfig) { - conn.transaction { - val checkVSchema = conn.prepareStatement( - "SELECT schema_name FROM information_schema.schemata WHERE schema_name = '_v'" - ) - if (!checkVSchema.executeQueryCheck()) { - logger.debug("_v schema not found, applying versioning.sql") - val sqlVersioning = File("${cfg.sqlDir}/versioning.sql").readText() - conn.execSQLUpdate(sqlVersioning) - } - } -} - -// sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash). -fun initializeDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { - logger.info("doing DB initialization, sqldir ${cfg.sqlDir}") - maybeApplyV(conn, cfg) - conn.transaction { - val checkStmt = conn.prepareStatement("SELECT count(*) as n FROM _v.patches where patch_name = ?") - - for (n in 1..9999) { - val numStr = n.toString().padStart(4, '0') - val patchName = "$sqlFilePrefix-$numStr" - - checkStmt.setString(1, patchName) - val patchCount = checkStmt.oneOrNull { it.getInt(1) } ?: throw Exception("unable to query patches"); - if (patchCount >= 1) { - logger.info("patch $patchName already applied") - continue - } - - val path = File("${cfg.sqlDir}/$sqlFilePrefix-$numStr.sql") - if (!path.exists()) { - logger.info("path $path doesn't exist anymore, stopping") - break - } - logger.info("applying patch $path") - val sqlPatchText = path.readText() - conn.execSQLUpdate(sqlPatchText) - } - val sqlProcedures = File("${cfg.sqlDir}/$sqlFilePrefix-procedures.sql") - if (!sqlProcedures.exists()) { - logger.info("no procedures.sql for the SQL collection: $sqlFilePrefix") - return@transaction - } - logger.info("run procedure.sql") - conn.execSQLUpdate(sqlProcedures.readText()) - } -} - -// sqlFilePrefix is, for example, "libeufin-bank" or "libeufin-nexus" (no trailing dash). -fun resetDatabaseTables(conn: PgConnection, cfg: DatabaseConfig, sqlFilePrefix: String) { - logger.info("reset DB, sqldir ${cfg.sqlDir}") - val isInitialized = conn.prepareStatement(""" - SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name='_v') AND - EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name='${sqlFilePrefix.replace("-", "_")}') - """).oneOrNull { - it.getBoolean(1) - }!! - if (!isInitialized) { - logger.info("versioning schema not present, not running drop sql") - return - } - - val sqlDrop = File("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText() - conn.execSQLUpdate(sqlDrop) -} - -abstract class DbPool(cfg: String, schema: String): java.io.Closeable { - val pgSource = pgDataSource(cfg) - private val pool: HikariDataSource - - init { - val config = HikariConfig(); - config.dataSource = pgSource - config.schema = schema - config.transactionIsolation = "TRANSACTION_SERIALIZABLE" - pool = HikariDataSource(config) - pool.getConnection().use { con -> - val meta = con.getMetaData(); - val majorVersion = meta.getDatabaseMajorVersion() - val minorVersion = meta.getDatabaseMinorVersion() - if (majorVersion < MIN_VERSION) { - throw Exception("postgres version must be at least $MIN_VERSION.0 got $majorVersion.$minorVersion") - } - } - } - - suspend fun <R> conn(lambda: suspend (PgConnection) -> R): R { - // Use a coroutine dispatcher that we can block as JDBC API is blocking - return withContext(Dispatchers.IO) { - val conn = pool.getConnection() - conn.use{ it -> lambda(it.unwrap(PgConnection::class.java)) } - } - } - - suspend fun <R> serializable(lambda: suspend (PgConnection) -> R): R = conn { conn -> - repeat(SERIALIZATION_RETRY) { - try { - return@conn lambda(conn); - } catch (e: SQLException) { - if (e.sqlState != PSQLState.SERIALIZATION_FAILURE.state) - throw e - } - } - try { - return@conn lambda(conn) - } catch(e: SQLException) { - logger.warn("Serialization failure after $SERIALIZATION_RETRY retry") - throw e - } - } - - override fun close() { - pool.close() - } -} - -fun ResultSet.getAmount(name: String, currency: String): TalerAmount{ - return TalerAmount( - getLong("${name}_val"), - getInt("${name}_frac"), - currency - ) -} -\ No newline at end of file diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -1,430 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -/** - * This is the main "EBICS library interface". Functions here are stateless helpers - * used to implement both an EBICS server and EBICS client. - */ - -package tech.libeufin.util - -import io.ktor.http.HttpStatusCode -import tech.libeufin.util.ebics_h004.* -import tech.libeufin.util.ebics_h005.Ebics3Response -import tech.libeufin.util.ebics_s001.UserSignatureData -import java.security.SecureRandom -import java.security.interfaces.RSAPrivateCrtKey -import java.security.interfaces.RSAPublicKey -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.util.* -import javax.xml.bind.JAXBElement -import javax.xml.datatype.DatatypeFactory -import javax.xml.datatype.XMLGregorianCalendar - -data class EbicsProtocolError( - val httpStatusCode: HttpStatusCode, - val reason: String, - /** - * This class is also used when Nexus finds itself - * in an inconsistent state, without interacting with the - * bank. In this case, the EBICS code below can be left - * null. - */ - val ebicsTechnicalCode: EbicsReturnCode? = null -) : Exception(reason) - -data class EbicsDateRange( - val start: Instant, - val end: Instant -) - -sealed class EbicsOrderParams -data class EbicsStandardOrderParams( - val dateRange: EbicsDateRange? = null -) : EbicsOrderParams() - -data class EbicsGenericOrderParams( - val params: Map<String, String> = mapOf() -) : EbicsOrderParams() - -enum class EbicsInitState { - SENT, NOT_SENT, UNKNOWN -} - -/** - * This class is a mere container that keeps data found - * in the database and that is further needed to sign / verify - * / make messages. And not all the values are needed all - * the time. - */ -data class EbicsClientSubscriberDetails( - val partnerId: String, - val userId: String, - var bankAuthPub: RSAPublicKey?, - var bankEncPub: RSAPublicKey?, - val ebicsUrl: String, - val hostId: String, - val customerEncPriv: RSAPrivateCrtKey, - val customerAuthPriv: RSAPrivateCrtKey, - val customerSignPriv: RSAPrivateCrtKey, - val ebicsIniState: EbicsInitState, - val ebicsHiaState: EbicsInitState, - var dialect: String? = null -) - -/** - * @param size in bits - */ -fun getNonce(size: Int): ByteArray { - val sr = SecureRandom() - val ret = ByteArray(size / 8) - sr.nextBytes(ret) - return ret -} - -fun getXmlDate(i: Instant): XMLGregorianCalendar { - val zonedTimestamp = ZonedDateTime.ofInstant(i, ZoneId.of("UTC")) - return getXmlDate(zonedTimestamp) -} -fun getXmlDate(d: ZonedDateTime): XMLGregorianCalendar { - return DatatypeFactory.newInstance() - .newXMLGregorianCalendar( - d.year, - d.monthValue, - d.dayOfMonth, - 0, - 0, - 0, - 0, - d.offset.totalSeconds / 60 - ) -} - -fun makeOrderParams(orderParams: EbicsOrderParams): EbicsRequest.OrderParams { - return when (orderParams) { - is EbicsStandardOrderParams -> { - EbicsRequest.StandardOrderParams().apply { - val r = orderParams.dateRange - if (r != null) { - this.dateRange = EbicsRequest.DateRange().apply { - this.start = getXmlDate(r.start) - this.end = getXmlDate(r.end) - } - } - } - } - is EbicsGenericOrderParams -> { - EbicsRequest.GenericOrderParams().apply { - this.parameterList = orderParams.params.map { entry -> - EbicsTypes.Parameter().apply { - this.name = entry.key - this.value = entry.value - this.type = "string" - } - } - } - } - } -} - -fun signOrder( - orderBlob: ByteArray, - signKey: RSAPrivateCrtKey, - partnerId: String, - userId: String -): UserSignatureData { - val ES_signature = CryptoUtil.signEbicsA006( - CryptoUtil.digestEbicsOrderA006(orderBlob), - signKey - ) - val userSignatureData = UserSignatureData().apply { - orderSignatureList = listOf( - UserSignatureData.OrderSignatureData().apply { - signatureVersion = "A006" - signatureValue = ES_signature - partnerID = partnerId - userID = userId - } - ) - } - return userSignatureData -} - -fun signOrderEbics3( - orderBlob: ByteArray, - signKey: RSAPrivateCrtKey, - partnerId: String, - userId: String -): tech.libeufin.util.ebics_s002.UserSignatureDataEbics3 { - val ES_signature = CryptoUtil.signEbicsA006( - CryptoUtil.digestEbicsOrderA006(orderBlob), - signKey - ) - val userSignatureData = tech.libeufin.util.ebics_s002.UserSignatureDataEbics3().apply { - orderSignatureList = listOf( - tech.libeufin.util.ebics_s002.UserSignatureDataEbics3.OrderSignatureData().apply { - signatureVersion = "A006" - signatureValue = ES_signature - partnerID = partnerId - userID = userId - } - ) - } - return userSignatureData -} - -data class PreparedUploadData( - val transactionKey: ByteArray, - val userSignatureDataEncrypted: ByteArray, - val dataDigest: ByteArray, - val encryptedPayloadChunks: List<String> -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as PreparedUploadData - - if (!transactionKey.contentEquals(other.transactionKey)) return false - if (!userSignatureDataEncrypted.contentEquals(other.userSignatureDataEncrypted)) return false - if (encryptedPayloadChunks != other.encryptedPayloadChunks) return false - - return true - } - - override fun hashCode(): Int { - var result = transactionKey.contentHashCode() - result = 31 * result + userSignatureDataEncrypted.contentHashCode() - result = 31 * result + encryptedPayloadChunks.hashCode() - return result - } -} - -data class DataEncryptionInfo( - val transactionKey: ByteArray, - val bankPubDigest: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as DataEncryptionInfo - - if (!transactionKey.contentEquals(other.transactionKey)) return false - if (!bankPubDigest.contentEquals(other.bankPubDigest)) return false - - return true - } - - override fun hashCode(): Int { - var result = transactionKey.contentHashCode() - result = 31 * result + bankPubDigest.contentHashCode() - return result - } -} - - -// TODO import missing using a script -@Suppress("SpellCheckingInspection") -enum class EbicsReturnCode(val errorCode: String) { - EBICS_OK("000000"), - EBICS_DOWNLOAD_POSTPROCESS_DONE("011000"), - EBICS_DOWNLOAD_POSTPROCESS_SKIPPED("011001"), - EBICS_TX_SEGMENT_NUMBER_UNDERRUN("011101"), - EBICS_AUTHENTICATION_FAILED("061001"), - EBICS_INVALID_REQUEST("061002"), - EBICS_INTERNAL_ERROR("061099"), - EBICS_TX_RECOVERY_SYNC("061101"), - EBICS_AUTHORISATION_ORDER_IDENTIFIER_FAILED("090003"), - EBICS_NO_DOWNLOAD_DATA_AVAILABLE("090005"), - EBICS_INVALID_USER_OR_USER_STATE("091002"), - EBICS_USER_UNKNOWN("091003"), - EBICS_EBICS_INVALID_USER_STATE("091004"), - EBICS_INVALID_ORDER_IDENTIFIER("091005"), - EBICS_UNSUPPORTED_ORDER_TYPE("091006"), - EBICS_INVALID_XML("091010"), - EBICS_TX_MESSAGE_REPLAY("091103"), - EBICS_PROCESSING_ERROR("091116"), - EBICS_ACCOUNT_AUTHORISATION_FAILED("091302"), - EBICS_AMOUNT_CHECK_FAILED("091303"); - - companion object { - fun lookup(errorCode: String): EbicsReturnCode { - for (x in values()) { - if (x.errorCode == errorCode) { - return x - } - } - throw Exception( - "Unknown EBICS status code: $errorCode" - ) - } - } -} - -data class EbicsResponseContent( - val transactionID: String?, - val dataEncryptionInfo: DataEncryptionInfo?, - val orderDataEncChunk: String?, - val technicalReturnCode: EbicsReturnCode, - val bankReturnCode: EbicsReturnCode, - val reportText: String, - val segmentNumber: Int?, - // Only present in init phase - val numSegments: Int? -) - -data class EbicsKeyManagementResponseContent( - val technicalReturnCode: EbicsReturnCode, - val bankReturnCode: EbicsReturnCode?, - val orderData: ByteArray? -) - - -class HpbResponseData( - val hostID: String, - val encryptionPubKey: RSAPublicKey, - val encryptionVersion: String, - val authenticationPubKey: RSAPublicKey, - val authenticationVersion: String -) - -fun parseEbicsHpbOrder(orderDataRaw: ByteArray): HpbResponseData { - val resp = try { - XMLUtil.convertStringToJaxb<HPBResponseOrderData>(orderDataRaw.toString(Charsets.UTF_8)) - } catch (e: Exception) { - throw EbicsProtocolError(HttpStatusCode.InternalServerError, "Invalid XML (as HPB response) received from bank") - } - val encPubKey = CryptoUtil.loadRsaPublicKeyFromComponents( - resp.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - resp.value.encryptionPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ) - val authPubKey = CryptoUtil.loadRsaPublicKeyFromComponents( - resp.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.modulus, - resp.value.authenticationPubKeyInfo.pubKeyValue.rsaKeyValue.exponent - ) - return HpbResponseData( - hostID = resp.value.hostID, - encryptionPubKey = encPubKey, - encryptionVersion = resp.value.encryptionPubKeyInfo.encryptionVersion, - authenticationPubKey = authPubKey, - authenticationVersion = resp.value.authenticationPubKeyInfo.authenticationVersion - ) -} - -fun ebics3toInternalRepr(response: String): EbicsResponseContent { - // logger.debug("Converting bank resp to internal repr.: $response") - val resp: JAXBElement<Ebics3Response> = try { - XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw EbicsProtocolError( - HttpStatusCode.InternalServerError, - "Could not transform string-response from bank into JAXB" - ) - } - val bankReturnCodeStr = resp.value.body.returnCode.value - val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) - - val techReturnCodeStr = resp.value.header.mutable.returnCode - val techReturnCode = EbicsReturnCode.lookup(techReturnCodeStr) - - val reportText = resp.value.header.mutable.reportText - - val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo - val dataEncryptionInfo = if (daeXml == null) { - null - } else { - DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) - } - - return EbicsResponseContent( - transactionID = resp.value.header._static.transactionID, - bankReturnCode = bankReturnCode, - technicalReturnCode = techReturnCode, - reportText = reportText, - orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, - dataEncryptionInfo = dataEncryptionInfo, - numSegments = resp.value.header._static.numSegments?.toInt(), - segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() - ) -} - -fun ebics25toInternalRepr(response: String): EbicsResponseContent { - val resp: JAXBElement<EbicsResponse> = try { - XMLUtil.convertStringToJaxb(response) - } catch (e: Exception) { - throw EbicsProtocolError( - HttpStatusCode.InternalServerError, - "Could not transform string-response from bank into JAXB" - ) - } - val bankReturnCodeStr = resp.value.body.returnCode.value - val bankReturnCode = EbicsReturnCode.lookup(bankReturnCodeStr) - - val techReturnCodeStr = resp.value.header.mutable.returnCode - val techReturnCode = EbicsReturnCode.lookup(techReturnCodeStr) - - val reportText = resp.value.header.mutable.reportText - - val daeXml = resp.value.body.dataTransfer?.dataEncryptionInfo - val dataEncryptionInfo = if (daeXml == null) { - null - } else { - DataEncryptionInfo(daeXml.transactionKey, daeXml.encryptionPubKeyDigest.value) - } - - return EbicsResponseContent( - transactionID = resp.value.header._static.transactionID, - bankReturnCode = bankReturnCode, - technicalReturnCode = techReturnCode, - reportText = reportText, - orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, - dataEncryptionInfo = dataEncryptionInfo, - numSegments = resp.value.header._static.numSegments?.toInt(), - segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() - ) -} - -/** - * Get the private key that matches the given public key digest. - */ -fun getDecryptionKey(subscriberDetails: EbicsClientSubscriberDetails, pubDigest: ByteArray): RSAPrivateCrtKey { - val authPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerAuthPriv) - val encPub = CryptoUtil.getRsaPublicFromPrivate(subscriberDetails.customerEncPriv) - val authPubDigest = CryptoUtil.getEbicsPublicKeyHash(authPub) - val encPubDigest = CryptoUtil.getEbicsPublicKeyHash(encPub) - if (pubDigest.contentEquals(authPubDigest)) { - return subscriberDetails.customerAuthPriv - } - if (pubDigest.contentEquals(encPubDigest)) { - return subscriberDetails.customerEncPriv - } - throw EbicsProtocolError(HttpStatusCode.NotFound, "Could not find customer's public key") -} - -data class EbicsVersionSpec( - val protocol: String, - val version: String -) - -data class EbicsHevDetails( - val versions: List<EbicsVersionSpec> -) -\ No newline at end of file diff --git a/util/src/main/kotlin/EbicsCodeSets.kt b/util/src/main/kotlin/EbicsCodeSets.kt @@ -1,309 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -// THIS FILE IS GENERATED, DO NOT EDIT - -package tech.libeufin.util - -enum class ExternalStatusReasonCode(val isoCode: String, val description: String) { - AB01("AbortedClearingTimeout", "Clearing process aborted due to timeout."), - AB02("AbortedClearingFatalError", "Clearing process aborted due to a fatal error."), - AB03("AbortedSettlementTimeout", "Settlement aborted due to timeout."), - AB04("AbortedSettlementFatalError", "Settlement process aborted due to a fatal error."), - AB05("TimeoutCreditorAgent", "Transaction stopped due to timeout at the Creditor Agent."), - AB06("TimeoutInstructedAgent", "Transaction stopped due to timeout at the Instructed Agent."), - AB07("OfflineAgent", "Agent of message is not online."), - AB08("OfflineCreditorAgent", "Creditor Agent is not online."), - AB09("ErrorCreditorAgent", "Transaction stopped due to error at the Creditor Agent."), - AB10("ErrorInstructedAgent", "Transaction stopped due to error at the Instructed Agent."), - AB11("TimeoutDebtorAgent", "Transaction stopped due to timeout at the Debtor Agent."), - AC01("IncorrectAccountNumber", "Account number is invalid or missing."), - AC02("InvalidDebtorAccountNumber", "Debtor account number invalid or missing"), - AC03("InvalidCreditorAccountNumber", "Creditor account number invalid or missing"), - AC04("ClosedAccountNumber", "Account number specified has been closed on the bank of account's books."), - AC05("ClosedDebtorAccountNumber", "Debtor account number closed"), - AC06("BlockedAccount", "Account specified is blocked, prohibiting posting of transactions against it."), - AC07("ClosedCreditorAccountNumber", "Creditor account number closed"), - AC08("InvalidBranchCode", "Branch code is invalid or missing"), - AC09("InvalidAccountCurrency", "Account currency is invalid or missing"), - AC10("InvalidDebtorAccountCurrency", "Debtor account currency is invalid or missing"), - AC11("InvalidCreditorAccountCurrency", "Creditor account currency is invalid or missing"), - AC12("InvalidAccountType", "Account type missing or invalid."), - AC13("InvalidDebtorAccountType", "Debtor account type missing or invalid"), - AC14("InvalidCreditorAccountType", "Creditor account type missing or invalid"), - AC15("AccountDetailsChanged", "The account details for the counterparty have changed."), - AC16("CardNumberInvalid", "Credit or debit card number is invalid."), - AEXR("AlreadyExpiredRTP", "Request-to-pay Expiry Date and Time has already passed."), - AG01("TransactionForbidden", "Transaction forbidden on this type of account (formerly NoAgreement)"), - AG02("InvalidBankOperationCode", "Bank Operation code specified in the message is not valid for receiver"), - AG03("TransactionNotSupported", "Transaction type not supported/authorized on this account"), - AG04("InvalidAgentCountry", "Agent country code is missing or invalid."), - AG05("InvalidDebtorAgentCountry", "Debtor agent country code is missing or invalid"), - AG06("InvalidCreditorAgentCountry", "Creditor agent country code is missing or invalid"), - AG07("UnsuccesfulDirectDebit", "Debtor account cannot be debited for a generic reason."), - AG08("InvalidAccessRights", "Transaction failed due to invalid or missing user or access right"), - AG09("PaymentNotReceived", "Original payment never received."), - AG10("AgentSuspended", "Agent of message is suspended from the Real Time Payment system."), - AG11("CreditorAgentSuspended", "Creditor Agent of message is suspended from the Real Time Payment system."), - AG12("NotAllowedBookTransfer", "Payment orders made by transferring funds from one account to another at the same financial institution (bank or payment institution) are not allowed."), - AG13("ForbiddenReturnPayment", "Returned payments derived from previously returned transactions are not allowed."), - AGNT("IncorrectAgent", "Agent in the payment workflow is incorrect"), - ALAC("AlreadyAcceptedRTP", "Request-to-pay has already been accepted by the Debtor."), - AM01("ZeroAmount", "Specified message amount is equal to zero"), - AM02("NotAllowedAmount", "Specific transaction/message amount is greater than allowed maximum"), - AM03("NotAllowedCurrency", "Specified message amount is an non processable currency outside of existing agreement"), - AM04("InsufficientFunds", "Amount of funds available to cover specified message amount is insufficient."), - AM05("Duplication", "Duplication"), - AM06("TooLowAmount", "Specified transaction amount is less than agreed minimum."), - AM07("BlockedAmount", "Amount specified in message has been blocked by regulatory authorities."), - AM09("WrongAmount", "Amount received is not the amount agreed or expected"), - AM10("InvalidControlSum", "Sum of instructed amounts does not equal the control sum."), - AM11("InvalidTransactionCurrency", "Transaction currency is invalid or missing"), - AM12("InvalidAmount", "Amount is invalid or missing"), - AM13("AmountExceedsClearingSystemLimit", "Transaction amount exceeds limits set by clearing system"), - AM14("AmountExceedsAgreedLimit", "Transaction amount exceeds limits agreed between bank and client"), - AM15("AmountBelowClearingSystemMinimum", "Transaction amount below minimum set by clearing system"), - AM16("InvalidGroupControlSum", "Control Sum at the Group level is invalid"), - AM17("InvalidPaymentInfoControlSum", "Control Sum at the Payment Information level is invalid"), - AM18("InvalidNumberOfTransactions", "Number of transactions is invalid or missing."), - AM19("InvalidGroupNumberOfTransactions", "Number of transactions at the Group level is invalid or missing"), - AM20("InvalidPaymentInfoNumberOfTransactions", "Number of transactions at the Payment Information level is invalid"), - AM21("LimitExceeded", "Transaction amount exceeds limits agreed between bank and client."), - AM22("ZeroAmountNotApplied", "Unable to apply zero amount to designated account. For example, where the rules of a service allow the use of zero amount payments, however the back-office system is unable to apply the funds to the account. If the rules of a service prohibit the use of zero amount payments, then code AM01 is used to report the error condition."), - AM23("AmountExceedsSettlementLimit", "Transaction amount exceeds settlement limit."), - APAR("AlreadyPaidRTP", "Request To Pay has already been paid by the Debtor."), - ARFR("AlreadyRefusedRTP", "Request-to-pay has already been refused by the Debtor."), - ARJR("AlreadyRejectedRTP", "Request-to-pay has already been rejected."), - ATNS("AttachementsNotSupported", "Attachments to the request-to-pay are not supported."), - BE01("InconsistenWithEndCustomer", "Identification of end customer is not consistent with associated account number. (formerly CreditorConsistency)."), - BE04("MissingCreditorAddress", "Specification of creditor's address, which is required for payment, is missing/not correct (formerly IncorrectCreditorAddress)."), - BE05("UnrecognisedInitiatingParty", "Party who initiated the message is not recognised by the end customer"), - BE06("UnknownEndCustomer", "End customer specified is not known at associated Sort/National Bank Code or does no longer exist in the books"), - BE07("MissingDebtorAddress", "Specification of debtor's address, which is required for payment, is missing/not correct."), - BE08("MissingDebtorName", "Debtor name is missing"), - BE09("InvalidCountry", "Country code is missing or Invalid."), - BE10("InvalidDebtorCountry", "Debtor country code is missing or invalid"), - BE11("InvalidCreditorCountry", "Creditor country code is missing or invalid"), - BE12("InvalidCountryOfResidence", "Country code of residence is missing or Invalid."), - BE13("InvalidDebtorCountryOfResidence", "Country code of debtor's residence is missing or Invalid"), - BE14("InvalidCreditorCountryOfResidence", "Country code of creditor's residence is missing or Invalid"), - BE15("InvalidIdentificationCode", "Identification code missing or invalid."), - BE16("InvalidDebtorIdentificationCode", "Debtor or Ultimate Debtor identification code missing or invalid"), - BE17("InvalidCreditorIdentificationCode", "Creditor or Ultimate Creditor identification code missing or invalid"), - BE18("InvalidContactDetails", "Contact details missing or invalid"), - BE19("InvalidChargeBearerCode", "Charge bearer code for transaction type is invalid"), - BE20("InvalidNameLength", "Name length exceeds local rules for payment type."), - BE21("MissingName", "Name missing or invalid. Generic usage if cannot specifically identify debtor or creditor."), - BE22("MissingCreditorName", "Creditor name is missing"), - BE23("AccountProxyInvalid", "Phone number or email address, or any other proxy, used as the account proxy is unknown or invalid."), - CERI("CheckERI", "Credit transfer is not tagged as an Extended Remittance Information (ERI) transaction but contains ERI."), - CH03("RequestedExecutionDateOrRequestedCollectionDateTooFarInFuture", "Value in Requested Execution Date or Requested Collection Date is too far in the future"), - CH04("RequestedExecutionDateOrRequestedCollectionDateTooFarInPast", "Value in Requested Execution Date or Requested Collection Date is too far in the past"), - CH07("ElementIsNotToBeUsedAtB-andC-Level", "Element is not to be used at B- and C-Level"), - CH09("MandateChangesNotAllowed", "Mandate changes are not allowed"), - CH10("InformationOnMandateChangesMissing", "Information on mandate changes are missing"), - CH11("CreditorIdentifierIncorrect", "Value in Creditor Identifier is incorrect"), - CH12("CreditorIdentifierNotUnambiguouslyAtTransaction-Level", "Creditor Identifier is ambiguous at Transaction Level"), - CH13("OriginalDebtorAccountIsNotToBeUsed", "Original Debtor Account is not to be used"), - CH14("OriginalDebtorAgentIsNotToBeUsed", "Original Debtor Agent is not to be used"), - CH15("ElementContentIncludesMoreThan140Characters", "Content Remittance Information/Structured includes more than 140 characters"), - CH16("ElementContentFormallyIncorrect", "Content is incorrect"), - CH17("ElementNotAdmitted", "Element is not allowed"), - CH19("ValuesWillBeSetToNextTARGETday", "Values in Interbank Settlement Date or Requested Collection Date will be set to the next TARGET day"), - CH20("DecimalPointsNotCompatibleWithCurrency", "Number of decimal points not compatible with the currency"), - CH21("RequiredCompulsoryElementMissing", "Mandatory element is missing"), - CH22("COREandB2BwithinOnemessage", "SDD CORE and B2B not permitted within one message"), - CHQC("ChequeSettledOnCreditorAccount", "Cheque has been presented in cheque clearing and settled on the creditor’s account."), - CN01("AuthorisationCancelled", "Authorisation is cancelled."), - CNOR("CreditorBankIsNotRegistered", "Creditor bank is not registered under this BIC in the CSM"), - CURR("IncorrectCurrency", "Currency of the payment is incorrect"), - CUST("RequestedByCustomer", "Cancellation requested by the Debtor"), - DC02("SettlementNotReceived", "Rejection of a payment due to covering FI settlement not being received."), - DNOR("DebtorBankIsNotRegistered", "Debtor bank is not registered under this BIC in the CSM"), - DS01("ElectronicSignaturesCorrect", "The electronic signature(s) is/are correct"), - DS02("OrderCancelled", "An authorized user has cancelled the order"), - DS03("OrderNotCancelled", "The user’s attempt to cancel the order was not successful"), - DS04("OrderRejected", "The order was rejected by the bank side (for reasons concerning content)"), - DS05("OrderForwardedForPostprocessing", "The order was correct and could be forwarded for postprocessing"), - DS06("TransferOrder", "The order was transferred to VEU"), - DS07("ProcessingOK", "All actions concerning the order could be done by the EBICS bank server"), - DS08("DecompressionError", "The decompression of the file was not successful"), - DS09("DecryptionError", "The decryption of the file was not successful"), - DS0A("DataSignRequested", "Data signature is required."), - DS0B("UnknownDataSignFormat", "Data signature for the format is not available or invalid."), - DS0C("SignerCertificateRevoked", "The signer certificate is revoked."), - DS0D("SignerCertificateNotValid", "The signer certificate is not valid (revoked or not active)."), - DS0E("IncorrectSignerCertificate", "The signer certificate is not present."), - DS0F("SignerCertificationAuthoritySignerNotValid", "The authority of the signer certification sending the certificate is unknown."), - DS0G("NotAllowedPayment", "Signer is not allowed to sign this operation type."), - DS0H("NotAllowedAccount", "Signer is not allowed to sign for this account."), - DS0K("NotAllowedNumberOfTransaction", "The number of transaction is over the number allowed for this signer."), - DS10("Signer1CertificateRevoked", "The certificate is revoked for the first signer."), - DS11("Signer1CertificateNotValid", "The certificate is not valid (revoked or not active) for the first signer."), - DS12("IncorrectSigner1Certificate", "The certificate is not present for the first signer."), - DS13("SignerCertificationAuthoritySigner1NotValid", "The authority of signer certification sending the certificate is unknown for the first signer."), - DS14("UserDoesNotExist", "The user is unknown on the server"), - DS15("IdenticalSignatureFound", "The same signature has already been sent to the bank"), - DS16("PublicKeyVersionIncorrect", "The public key version is not correct. This code is returned when a customer sends signature files to the financial institution after conversion from an older program version (old ES format) to a new program version (new ES format) without having carried out re-initialisation with regard to a public key change."), - DS17("DifferentOrderDataInSignatures", "Order data and signatures don’t match"), - DS18("RepeatOrder", "File cannot be tested, the complete order has to be repeated. This code is returned in the event of a malfunction during the signature check, e.g. not enough storage space."), - DS19("ElectronicSignatureRightsInsufficient", "The user’s rights (concerning his signature) are insufficient to execute the order"), - DS20("Signer2CertificateRevoked", "The certificate is revoked for the second signer."), - DS21("Signer2CertificateNotValid", "The certificate is not valid (revoked or not active) for the second signer."), - DS22("IncorrectSigner2Certificate", "The certificate is not present for the second signer."), - DS23("SignerCertificationAuthoritySigner2NotValid", "The authority of signer certification sending the certificate is unknown for the second signer."), - DS24("WaitingTimeExpired", "Waiting time expired due to incomplete order"), - DS25("OrderFileDeleted", "The order file was deleted by the bank server"), - DS26("UserSignedMultipleTimes", "The same user has signed multiple times"), - DS27("UserNotYetActivated", "The user is not yet activated (technically)"), - DT01("InvalidDate", "Invalid date (eg, wrong or missing settlement date)"), - DT02("InvalidCreationDate", "Invalid creation date and time in Group Header (eg, historic date)"), - DT03("InvalidNonProcessingDate", "Invalid non bank processing date (eg, weekend or local public holiday)"), - DT04("FutureDateNotSupported", "Future date not supported"), - DT05("InvalidCutOffDate", "Associated message, payment information block or transaction was received after agreed processing cut-off date, i.e., date in the past."), - DT06("ExecutionDateChanged", "Execution Date has been modified in order for transaction to be processed"), - DU01("DuplicateMessageID", "Message Identification is not unique."), - DU02("DuplicatePaymentInformationID", "Payment Information Block is not unique."), - DU03("DuplicateTransaction", "Transaction is not unique."), - DU04("DuplicateEndToEndID", "End To End ID is not unique."), - DU05("DuplicateInstructionID", "Instruction ID is not unique."), - DUPL("DuplicatePayment", "Payment is a duplicate of another payment"), - ED01("CorrespondentBankNotPossible", "Correspondent bank not possible."), - ED03("BalanceInfoRequest", "Balance of payments complementary info is requested"), - ED05("SettlementFailed", "Settlement of the transaction has failed."), - ED06("SettlementSystemNotAvailable", "Interbank settlement system not available."), - EDTL("ExpiryDateTooLong", "Expiry date time of the request-to-pay is too far in the future."), - EDTR("ExpiryDateTimeReached", "Expiry date time of the request-to-pay is already reached."), - ERIN("ERIOptionNotSupported", "Extended Remittance Information (ERI) option is not supported."), - FF01("InvalidFileFormat", "File Format incomplete or invalid"), - FF02("SyntaxError", "Syntax error reason is provided as narrative information in the additional reason information."), - FF03("InvalidPaymentTypeInformation", "Payment Type Information is missing or invalid."), - FF04("InvalidServiceLevelCode", "Service Level code is missing or invalid"), - FF05("InvalidLocalInstrumentCode", "Local Instrument code is missing or invalid"), - FF06("InvalidCategoryPurposeCode", "Category Purpose code is missing or invalid"), - FF07("InvalidPurpose", "Purpose is missing or invalid"), - FF08("InvalidEndToEndId", "End to End Id missing or invalid"), - FF09("InvalidChequeNumber", "Cheque number missing or invalid"), - FF10("BankSystemProcessingError", "File or transaction cannot be processed due to technical issues at the bank side"), - FF11("ClearingRequestAborted", "Clearing request rejected due it being subject to an abort operation."), - FF12("OriginalTransactionNotEligibleForRequestedReturn", "Original payment is not eligible to be returned given its current status."), - FF13("RequestForCancellationNotFound", "No record of request for cancellation found."), - FOCR("FollowingCancellationRequest", "Return following a cancellation request."), - FR01("Fraud", "Returned as a result of fraud."), - FRAD("FraudulentOrigin", "Cancellation requested following a transaction that was originated fraudulently. The use of the FraudulentOrigin code should be governed by jurisdictions."), - G000("PaymentTransferredAndTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is tracked. No further updates will follow from the Status Originator."), - G001("PaymentTransferredAndNotTracked", "In an FI To FI Customer Credit Transfer: The Status Originator transferred the payment to the next Agent or to a Market Infrastructure. The payment transfer is not tracked. No further updates will follow from the Status Originator."), - G002("CreditDebitNotConfirmed", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account may not be confirmed same day. Update will follow from the Status Originator."), - G003("CreditPendingDocuments", "In a FIToFI Customer Credit Transfer: Credit to creditor’s account is pending receipt of required documents. The Status Originator has requested creditor to provide additional documentation. Update will follow from the Status Originator."), - G004("CreditPendingFunds", "In a FIToFI Customer Credit Transfer: Credit to the creditor’s account is pending, status Originator is waiting for funds provided via a cover. Update will follow from the Status Originator."), - G005("DeliveredWithServiceLevel", "Payment has been delivered to creditor agent with service level."), - G006("DeliveredWIthoutServiceLevel", "Payment has been delivered to creditor agent without service level."), - ID01("CorrespondingOriginalFileStillNotSent", "Signature file was sent to the bank but the corresponding original file has not been sent yet."), - IEDT("IncorrectExpiryDateTime", "Expiry date time of the request-to-pay is incorrect."), - IRNR("InitialRTPNeverReceived", "No initial request-to-pay has been received."), - MD01("NoMandate", "No Mandate"), - MD02("MissingMandatoryInformationInMandate", "Mandate related information data required by the scheme is missing."), - MD05("CollectionNotDue", "Creditor or creditor's agent should not have collected the direct debit"), - MD06("RefundRequestByEndCustomer", "Return of funds requested by end customer"), - MD07("EndCustomerDeceased", "End customer is deceased."), - MS02("NotSpecifiedReasonCustomerGenerated", "Reason has not been specified by end customer"), - MS03("NotSpecifiedReasonAgentGenerated", "Reason has not been specified by agent."), - NARR("Narrative", "Reason is provided as narrative information in the additional reason information."), - NERI("NoERI", "Credit transfer is tagged as an Extended Remittance Information (ERI) transaction but does not contain ERI."), - NOAR("NonAgreedRTP", "No existing agreement for receiving request-to-pay messages."), - NOAS("NoAnswerFromCustomer", "No response from Beneficiary."), - NOCM("NotCompliantGeneric", "Customer account is not compliant with regulatory requirements, for example FICA (in South Africa) or any other regulatory requirements which render an account inactive for certain processing."), - NOPG("NoPaymentGuarantee", "Requested payment guarantee (by Creditor) related to a request-to-pay cannot be provided."), - NRCH("PayerOrPayerRTPSPNotReachable", "Recipient side of the request-to-pay (payer or its request-to-pay service provider) is not reachable."), - PINS("TypeOfPaymentInstrumentNotSupported", "Type of payment requested in the request-to-pay is not supported by the payer."), - RC01("BankIdentifierIncorrect", "Bank identifier code specified in the message has an incorrect format (formerly IncorrectFormatForRoutingCode)."), - RC02("InvalidBankIdentifier", "Bank identifier is invalid or missing."), - RC03("InvalidDebtorBankIdentifier", "Debtor bank identifier is invalid or missing"), - RC04("InvalidCreditorBankIdentifier", "Creditor bank identifier is invalid or missing"), - RC05("InvalidBICIdentifier", "BIC identifier is invalid or missing."), - RC06("InvalidDebtorBICIdentifier", "Debtor BIC identifier is invalid or missing"), - RC07("InvalidCreditorBICIdentifier", "Creditor BIC identifier is invalid or missing"), - RC08("InvalidClearingSystemMemberIdentifier", "ClearingSystemMemberidentifier is invalid or missing."), - RC09("InvalidDebtorClearingSystemMemberIdentifier", "Debtor ClearingSystemMember identifier is invalid or missing"), - RC10("InvalidCreditorClearingSystemMemberIdentifier", "Creditor ClearingSystemMember identifier is invalid or missing"), - RC11("InvalidIntermediaryAgent", "Intermediary Agent is invalid or missing"), - RC12("MissingCreditorSchemeId", "Creditor Scheme Id is invalid or missing"), - RCON("RMessageConflict", "Conflict with R-Message"), - RECI("ReceiverCustomerInformation", "Further information regarding the intended recipient."), - REPR("RTPReceivedCanBeProcessed", "Request-to-pay has been received and can be processed further."), - RF01("NotUniqueTransactionReference", "Transaction reference is not unique within the message."), - RR01("MissingDebtorAccountOrIdentification", "Specification of the debtor’s account or unique identification needed for reasons of regulatory requirements is insufficient or missing"), - RR02("MissingDebtorNameOrAddress", "Specification of the debtor’s name and/or address needed for regulatory requirements is insufficient or missing."), - RR03("MissingCreditorNameOrAddress", "Specification of the creditor’s name and/or address needed for regulatory requirements is insufficient or missing."), - RR04("RegulatoryReason", "Regulatory Reason"), - RR05("RegulatoryInformationInvalid", "Regulatory or Central Bank Reporting information missing, incomplete or invalid."), - RR06("TaxInformationInvalid", "Tax information missing, incomplete or invalid."), - RR07("RemittanceInformationInvalid", "Remittance information structure does not comply with rules for payment type."), - RR08("RemittanceInformationTruncated", "Remittance information truncated to comply with rules for payment type."), - RR09("InvalidStructuredCreditorReference", "Structured creditor reference invalid or missing."), - RR10("InvalidCharacterSet", "Character set supplied not valid for the country and payment type."), - RR11("InvalidDebtorAgentServiceID", "Invalid or missing identification of a bank proprietary service."), - RR12("InvalidPartyID", "Invalid or missing identification required within a particular country or payment type."), - RTNS("RTPNotSupportedForDebtor", "Debtor does not support request-to-pay transactions."), - RUTA("ReturnUponUnableToApply", "Return following investigation request and no remediation possible."), - S000("ValidRequestForCancellationAcknowledged", "Request for Cancellation is acknowledged following validation."), - S001("UETRFlaggedForCancellation", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been identified as being associated with a Request for Cancellation."), - S002("NetworkStopOfUETR", "Unique End-to-end Transaction Reference (UETR) relating to a payment has been prevent from traveling across a messaging network."), - S003("RequestForCancellationForwarded", "Request for Cancellation has been forwarded to the payment processing/last payment processing agent."), - S004("RequestForCancellationDeliveryAcknowledgement", "Request for Cancellation has been acknowledged as delivered to payment processing/last payment processing agent."), - SL01("SpecificServiceOfferedByDebtorAgent", "Due to specific service offered by the Debtor Agent."), - SL02("SpecificServiceOfferedByCreditorAgent", "Due to specific service offered by the Creditor Agent."), - SL03("ServiceofClearingSystem", "Due to a specific service offered by the clearing system."), - SL11("CreditorNotOnWhitelistOfDebtor", "Whitelisting service offered by the Debtor Agent; Debtor has not included the Creditor on its “Whitelist” (yet). In the Whitelist the Debtor may list all allowed Creditors to debit Debtor bank account."), - SL12("CreditorOnBlacklistOfDebtor", "Blacklisting service offered by the Debtor Agent; Debtor included the Creditor on his “Blacklist”. In the Blacklist the Debtor may list all Creditors not allowed to debit Debtor bank account."), - SL13("MaximumNumberOfDirectDebitTransactionsExceeded", "Due to Maximum allowed Direct Debit Transactions per period service offered by the Debtor Agent."), - SL14("MaximumDirectDebitTransactionAmountExceeded", "Due to Maximum allowed Direct Debit Transaction amount service offered by the Debtor Agent."), - SPII("RTPServiceProviderIdentifierIncorrect", "Identifier of the request-to-pay service provider is incorrect."), - TA01("TransmissonAborted", "The transmission of the file was not successful – it had to be aborted (for technical reasons)"), - TD01("NoDataAvailable", "There is no data available (for download)"), - TD02("FileNonReadable", "The file cannot be read (e.g. unknown format)"), - TD03("IncorrectFileStructure", "The file format is incomplete or invalid"), - TK01("TokenInvalid", "Token is invalid."), - TK02("SenderTokenNotFound", "Token used for the sender does not exist."), - TK03("ReceiverTokenNotFound", "Token used for the receiver does not exist."), - TK09("TokenMissing", "Token required for request is missing."), - TKCM("TokenCounterpartyMismatch", "Token found with counterparty mismatch."), - TKSG("TokenSingleUse", "Single Use Token already used."), - TKSP("TokenSuspended", "Token found with suspended status."), - TKVE("TokenValueLimitExceeded", "Token found with value limit rule violation."), - TKXP("TokenExpired", "Token expired."), - TM01("InvalidCutOffTime", "Associated message, payment information block, or transaction was received after agreed processing cut-off time."), - TS01("TransmissionSuccessful", "The (technical) transmission of the file was successful."), - TS04("TransferToSignByHand", "The order was transferred to pass by accompanying note signed by hand"), - UCRD("UnknownCreditor", "Unknown Creditor."), - UPAY("UnduePayment", "Payment is not justified."), -} - -enum class ExternalPaymentGroupStatusCode(val isoCode: String, val description: String) { - ACCC("AcceptedSettlementCompletedCreditorAccount", "Settlement on the creditor's account has been completed."), - ACCP("AcceptedCustomerProfile", "Preceding check of technical validation was successful. Customer profile check was also successful."), - ACSC("AcceptedSettlementCompletedDebitorAccount", "Settlement on the debtor's account has been completed."), - ACSP("AcceptedSettlementInProcess", "All preceding checks such as technical validation and customer profile were successful and therefore the payment initiation has been accepted for execution."), - ACTC("AcceptedTechnicalValidation", "Authentication and syntactical and semantical validation are successful"), - ACWC("AcceptedWithChange", "Instruction is accepted but a change will be made, such as date or remittance not sent."), - PART("PartiallyAccepted", "A number of transactions have been accepted, whereas another number of transactions have not yet achieved"), - PDNG("Pending", "Payment initiation or individual transaction included in the payment initiation is pending. Further checks and status update will be performed."), - RCVD("Received", "Payment initiation has been received by the receiving agent"), - RJCT("Rejected", "Payment initiation or individual transaction included in the payment initiation has been rejected."), -} diff --git a/util/src/main/kotlin/EbicsOrderUtil.kt b/util/src/main/kotlin/EbicsOrderUtil.kt @@ -1,97 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import java.lang.IllegalArgumentException -import java.security.SecureRandom -import java.util.zip.DeflaterInputStream -import java.util.zip.InflaterInputStream - -/** - * Helpers for dealing with order compression, encryption, decryption, chunking and re-assembly. - */ -object EbicsOrderUtil { - - // Decompression only, no XML involved. - fun decodeOrderData(encodedOrderData: ByteArray): ByteArray { - return InflaterInputStream(encodedOrderData.inputStream()).use { - it.readAllBytes() - } - } - - 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() - } - } - - fun generateTransactionId(): String { - val rng = SecureRandom() - val res = ByteArray(16) - rng.nextBytes(res) - return res.toHexString().uppercase() - } - - /** - * 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 - } - - 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/util/src/main/kotlin/Encoding.kt b/util/src/main/kotlin/Encoding.kt @@ -1,152 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 net.taler.wallet.crypto - -import java.io.ByteArrayOutputStream - -class EncodingException : Exception("Invalid encoding") - -object Base32Crockford { - - private fun ByteArray.getIntAt(index: Int): Int { - val x = this[index].toInt() - return if (x >= 0) x else (x + 256) - } - - private var encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" - - fun encode(data: ByteArray): String { - val sb = StringBuilder() - var inputChunkBuffer = 0 - var pendingBitsCount = 0 - var inputCursor = 0 - var inputChunkNumber = 0 - - while (inputCursor < data.size) { - // Read input - inputChunkNumber = data.getIntAt(inputCursor++) - inputChunkBuffer = (inputChunkBuffer shl 8) or inputChunkNumber - pendingBitsCount += 8 - // Write symbols - while (pendingBitsCount >= 5) { - val symbolIndex = inputChunkBuffer.ushr(pendingBitsCount - 5) and 31 - sb.append(encTable[symbolIndex]) - pendingBitsCount -= 5 - } - } - if (pendingBitsCount >= 5) - throw Exception("base32 encoder did not write all the symbols") - - if (pendingBitsCount > 0) { - val symbolIndex = (inputChunkNumber shl (5 - pendingBitsCount)) and 31 - sb.append(encTable[symbolIndex]) - } - val enc = sb.toString() - val oneMore = ((data.size * 8) % 5) > 0 - val expectedLength = if (oneMore) { - ((data.size * 8) / 5) + 1 - } else { - (data.size * 8) / 5 - } - if (enc.length != expectedLength) - throw Exception("base32 encoding has wrong length") - return enc - } - - /** - * Decodes the input to its binary representation, throws - * net.taler.wallet.crypto.EncodingException on invalid encodings. - */ - fun decode( - encoded: String, - out: ByteArrayOutputStream - ) { - var outBitsCount = 0 - var bitsBuffer = 0 - var inputCursor = 0 - - while (inputCursor < encoded.length) { - val decodedNumber = getValue(encoded[inputCursor++]) - bitsBuffer = (bitsBuffer shl 5) or decodedNumber - outBitsCount += 5 - while (outBitsCount >= 8) { - val outputChunk = (bitsBuffer ushr (outBitsCount - 8)) and 0xFF - out.write(outputChunk) - outBitsCount -= 8 // decrease of written bits. - } - } - if ((encoded.length * 5) / 8 != out.size()) - throw Exception("base32 decoder: wrong output size") - } - - fun decode(encoded: String): ByteArray { - val out = ByteArrayOutputStream() - decode(encoded, out) - val blob = out.toByteArray() - return blob - } - - private fun getValue(chr: Char): Int { - var a = chr - when (a) { - 'O', 'o' -> a = '0' - 'i', 'I', 'l', 'L' -> a = '1' - 'u', 'U' -> a = 'V' - } - if (a in '0'..'9') - return a - '0' - if (a in 'a'..'z') - a = Character.toUpperCase(a) - var dec = 0 - if (a in 'A'..'Z') { - if ('I' < a) dec++ - if ('L' < a) dec++ - if ('O' < a) dec++ - if ('U' < a) dec++ - return a - 'A' + 10 - dec - } - throw EncodingException() - } - - /** - * Compute the length of the resulting string when encoding data of the given size - * in bytes. - * - * @param dataSize size of the data to encode in bytes - * @return size of the string that would result from encoding - */ - @Suppress("unused") - fun calculateEncodedStringLength(dataSize: Int): Int { - return (dataSize * 8 + 4) / 5 - } - - /** - * Compute the length of the resulting data in bytes when decoding a (valid) string of the - * given size. - * - * @param stringSize size of the string to decode - * @return size of the resulting data in bytes - */ - @Suppress("unused") - fun calculateDecodedDataLength(stringSize: Int): Int { - return stringSize * 5 / 8 - } -} - diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt @@ -1,67 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import io.ktor.http.* -import io.ktor.server.application.* -import io.ktor.server.request.* -import io.ktor.server.util.* -import org.slf4j.Logger -import org.slf4j.LoggerFactory - -private val logger: Logger = LoggerFactory.getLogger("libeufin-common") - -// Get the base URL of a request, returns null if any problem occurs. -fun ApplicationRequest.getBaseUrl(): String? { - return if (this.headers.contains("X-Forwarded-Host")) { - logger.info("Building X-Forwarded- base URL") - // FIXME: should tolerate a missing X-Forwarded-Prefix. - var prefix: String = this.headers["X-Forwarded-Prefix"] - ?: run { - logger.error("Reverse proxy did not define X-Forwarded-Prefix") - return null - } - if (!prefix.endsWith("/")) - prefix += "/" - URLBuilder( - protocol = URLProtocol( - name = this.headers["X-Forwarded-Proto"] ?: run { - logger.error("Reverse proxy did not define X-Forwarded-Proto") - return null - }, - defaultPort = -1 // Port must be specified with X-Forwarded-Host. - ), - host = this.headers["X-Forwarded-Host"] ?: run { - logger.error("Reverse proxy did not define X-Forwarded-Host") - return null - } - ).apply { - encodedPath = prefix - // Gets dropped otherwise. - if (!encodedPath.endsWith("/")) - encodedPath += "/" - }.buildString() - } else { - this.call.url { - parameters.clear() - encodedPath = "/" - } - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/IbanPayto.kt b/util/src/main/kotlin/IbanPayto.kt @@ -1,104 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.net.URI -import java.net.URLDecoder - -private val logger: Logger = LoggerFactory.getLogger("libeufin-common") - -// Payto information. -data class IbanPayto( - // represent query param "sender-name" or "receiver-name". - val receiverName: String?, - val iban: String, - val bic: String?, - // Typically, a wire transfer's subject. - val message: String?, - val amount: String? -) - -// Return the value of query string parameter 'name', or null if not found. -// 'params' is the list of key-value elements of all the query parameters found in the URI. -private fun getQueryParamOrNull(name: String, params: List<Pair<String, String>>?): String? { - if (params == null) return null - return params.firstNotNullOfOrNull { pair -> - URLDecoder.decode(pair.second, Charsets.UTF_8).takeIf { pair.first == name } - } -} - -// Parses a Payto URI, returning null if the input is invalid. -fun parsePayto(payto: String): IbanPayto? { - /** - * This check is due because URIs having a "payto:" prefix without - * slashes are correctly parsed by the Java 'URI' class. 'mailto' - * for example lacks the double-slash part. - */ - if (!payto.startsWith("payto://")) { - logger.error("Invalid payto URI: $payto") - return null - } - - val javaParsedUri = try { - URI(payto) - } catch (e: java.lang.Exception) { - logger.error("'${payto}' is not a valid URI") - return null - } - if (javaParsedUri.scheme != "payto") { - logger.error("'${payto}' is not payto") - return null - } - val wireMethod = javaParsedUri.host - if (wireMethod != "iban") { - logger.error("Only 'iban' is supported, not '$wireMethod'") - return null - } - val splitPath = javaParsedUri.path.split("/").filter { it.isNotEmpty() } - if (splitPath.size > 2) { - logger.error("too many path segments in iban payto URI: $payto") - return null - } - val (iban, bic) = if (splitPath.size == 1) { - Pair(splitPath[0], null) - } else Pair(splitPath[1], splitPath[0]) - - val params: List<Pair<String, String>>? = if (javaParsedUri.query != null) { - val queryString: List<String> = javaParsedUri.query.split("&") - queryString.map { - val split = it.split("="); - if (split.size != 2) { - logger.error("parameter '$it' was malformed") - return null - } - Pair(split[0], split[1]) - } - } else null - - return IbanPayto( - iban = iban, - bic = bic, - amount = getQueryParamOrNull("amount", params), - message = getQueryParamOrNull("message", params), - receiverName = getQueryParamOrNull("receiver-name", params) - ) -} -\ No newline at end of file diff --git a/util/src/main/kotlin/TalerCommon.kt b/util/src/main/kotlin/TalerCommon.kt @@ -1,100 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - * - * 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.util - -import kotlinx.serialization.* -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.json.* - -sealed class CommonError(msg: String): Exception(msg) { - class AmountFormat(msg: String): CommonError(msg) - class AmountNumberTooBig(msg: String): CommonError(msg) -} - -@Serializable(with = TalerAmount.Serializer::class) -class TalerAmount { - val value: Long - val frac: Int - val currency: String - - constructor(value: Long, frac: Int, currency: String) { - this.value = value - this.frac = frac - this.currency = currency - } - constructor(encoded: String) { - val match = PATTERN.matchEntire(encoded) ?: - throw CommonError.AmountFormat("Invalid amount format"); - val (currency, value, frac) = match.destructured - this.currency = currency - this.value = value.toLongOrNull() ?: - throw CommonError.AmountFormat("Invalid value") - if (this.value > MAX_VALUE) - throw CommonError.AmountNumberTooBig("Value specified in amount is too large") - this.frac = if (frac.isEmpty()) { - 0 - } else { - var tmp = frac.toIntOrNull() ?: - throw CommonError.AmountFormat("Invalid fractional value") - if (tmp > FRACTION_BASE) - throw CommonError.AmountFormat("Fractional calue specified in amount is too large") - repeat(8 - frac.length) { - tmp *= 10 - } - tmp - } - } - - override fun equals(other: Any?): Boolean { - return other is TalerAmount && - other.value == this.value && - other.frac == this.frac && - other.currency == this.currency - } - - override fun toString(): String { - if (frac == 0) { - return "$currency:$value" - } else { - return "$currency:$value.${frac.toString().padStart(8, '0')}" - .dropLastWhile { it == '0' } // Trim useless fractional trailing 0 - } - } - - internal object Serializer : KSerializer<TalerAmount> { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: TalerAmount) { - encoder.encodeString(value.toString()) - } - - override fun deserialize(decoder: Decoder): TalerAmount { - return TalerAmount(decoder.decodeString()) - } - } - - companion object { - const val FRACTION_BASE = 100000000 - const val MAX_VALUE = 4503599627370496L; // 2^52 - private val PATTERN = Regex("([A-Z]{1,11}):([0-9]+)(?:\\.([0-9]{1,8}))?"); - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/TalerConfig.kt b/util/src/main/kotlin/TalerConfig.kt @@ -1,487 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - * - * 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/> - */ - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.io.File -import java.nio.file.Paths -import kotlin.io.path.Path -import kotlin.io.path.isReadable -import kotlin.io.path.listDirectoryEntries - -private val logger: Logger = LoggerFactory.getLogger("libeufin-config") - -private data class Section( - val entries: MutableMap<String, String>, -) - -private val reEmptyLine = Regex("^\\s*$") -private val reComment = Regex("^\\s*#.*$") -private val reSection = Regex("^\\s*\\[\\s*([^]]*)\\s*]\\s*$") -private val reParam = Regex("^\\s*([^=]+?)\\s*=\\s*(.*?)\\s*$") -private val reDirective = Regex("^\\s*@([a-zA-Z-_]+)@\\s*(.*?)\\s*$") - -class TalerConfigError(m: String) : Exception(m) - -/** - * Information about how the configuration is loaded. - * - * The entry point for the configuration will be the first file from this list: - * - /etc/$projectName/$componentName.conf - * - /etc/$componentName.conf - */ -data class ConfigSource( - /** - * Name of the high-level project. - */ - val projectName: String = "taler", - /** - * Name of the component within the package. - */ - val componentName: String = "taler", - /** - * Name of the binary that will be located on $PATH to - * find the installation path of the package. - */ - val installPathBinary: String = "taler-config", -) - -/** - * Reader and writer for Taler-style configuration files. - * - * The configuration file format is similar to INI files - * and fully described in the taler.conf man page. - * - * @param configSource information about where to load configuration defaults from - */ -class TalerConfig( - private val configSource: ConfigSource, -) { - private val sectionMap: MutableMap<String, Section> = mutableMapOf() - - private val componentName = configSource.componentName - private val projectName = configSource.projectName - private val installPathBinary = configSource.installPathBinary - val sections: Set<String> get() = sectionMap.keys - - private fun internalLoadFromString(s: String, sourceFilename: String?) { - val lines = s.lines() - var lineNum = 0 - var currentSection: String? = null - for (line in lines) { - lineNum++ - if (reEmptyLine.matches(line)) { - continue - } - if (reComment.matches(line)) { - continue - } - - val directiveMatch = reDirective.matchEntire(line) - if (directiveMatch != null) { - if (sourceFilename == null) { - throw TalerConfigError("Directives are only supported when loading from file") - } - val directiveName = directiveMatch.groups[1]!!.value.lowercase() - val directiveArg = directiveMatch.groups[2]!!.value - when (directiveName) { - "inline" -> { - val innerFilename = normalizeInlineFilename(sourceFilename, directiveArg.trim()) - this.loadFromFilename(innerFilename) - } - - "inline-matching" -> { - val glob = directiveArg.trim() - this.loadFromGlob(sourceFilename, glob) - } - - "inline-secret" -> { - val arg = directiveArg.trim() - val sp = arg.split(" ") - if (sp.size != 2) { - throw TalerConfigError("invalid configuration, @inline-secret@ directive requires exactly two arguments") - } - val sectionName = sp[0] - val secretFilename = normalizeInlineFilename(sourceFilename, sp[1]) - loadSecret(sectionName, secretFilename) - } - - else -> { - throw TalerConfigError("unsupported directive '$directiveName'") - } - } - continue - } - - val secMatch = reSection.matchEntire(line) - if (secMatch != null) { - currentSection = secMatch.groups[1]!!.value.uppercase() - continue - } - if (currentSection == null) { - throw TalerConfigError("section expected") - } - - val paramMatch = reParam.matchEntire(line) - - if (paramMatch != null) { - val optName = paramMatch.groups[1]!!.value.uppercase() - var optVal = paramMatch.groups[2]!!.value - if (optVal.startsWith('"') && optVal.endsWith('"')) { - optVal = optVal.substring(1, optVal.length - 1) - } - val section = provideSection(currentSection) - section.entries[optName] = optVal - continue - } - throw TalerConfigError("expected section header, option assignment or directive in line $lineNum file ${sourceFilename ?: "<input>"}") - } - } - - private fun loadFromGlob(parentFilename: String, glob: String) { - val fullFileglob: String - val parentDir = Path(parentFilename).parent!!.toString() - if (glob.startsWith("/")) { - fullFileglob = glob - } else { - fullFileglob = Paths.get(parentDir, glob).toString() - } - - val head = Path(fullFileglob).parent.toString() - val tail = Path(fullFileglob).fileName.toString() - - // FIXME: Check that the Kotlin glob matches the glob from our spec - for (entry in Path(head).listDirectoryEntries(tail)) { - loadFromFilename(entry.toString()) - } - } - - private fun normalizeInlineFilename(parentFilename: String, f: String): String { - if (f[0] == '/') { - return f - } - val parentDirPath = Path(parentFilename).toRealPath().parent - if (parentDirPath == null) { - throw TalerConfigError("unable to normalize inline path, cannot resolve parent directory of $parentFilename") - } - val parentDir = parentDirPath.toString() - return Paths.get(parentDir, f).toRealPath().toString() - } - - private fun loadSecret(sectionName: String, secretFilename: String) { - if (!Path(secretFilename).isReadable()) { - logger.warn("unable to read secrets from $secretFilename") - } else { - this.loadFromFilename(secretFilename) - } - } - - private fun provideSection(name: String): Section { - val canonSecName = name.uppercase() - val existingSec = this.sectionMap[canonSecName] - if (existingSec != null) { - return existingSec - } - val newSection = Section(entries = mutableMapOf()) - this.sectionMap[canonSecName] = newSection - return newSection - } - - fun loadFromString(s: String) { - internalLoadFromString(s, null) - } - - private fun setSystemDefault(section: String, option: String, value: String) { - // FIXME: The value should be marked as a system default for diagnostics pretty printing - val sec = provideSection(section) - sec.entries[option.uppercase()] = value - } - - fun putValueString(section: String, option: String, value: String) { - val sec = provideSection(section) - sec.entries[option.uppercase()] = value - } - - /** - * Create a string representation of the loaded configuration. - */ - fun stringify(): String { - val outStr = StringBuilder() - this.sectionMap.forEach { (sectionName, section) -> - var headerWritten = false - section.entries.forEach { (optionName, entry) -> - if (!headerWritten) { - outStr.appendLine("[$sectionName]") - headerWritten = true - } - outStr.appendLine("$optionName = $entry") - } - if (headerWritten) { - outStr.appendLine() - } - } - return outStr.toString() - } - - /** - * Read values into the configuration from the given entry point - * filename. Defaults are *not* loaded automatically. - */ - fun loadFromFilename(filename: String) { - val f = File(filename) - val contents = f.readText() - internalLoadFromString(contents, filename) - } - - private fun loadDefaultsFromDir(dirname: String) { - for (filePath in Path(dirname).listDirectoryEntries()) { - loadFromFilename(filePath.toString()) - } - } - - /** - * Load configuration defaults from the file system - * and populate the PATHS section based on the installation path. - */ - fun loadDefaults() { - val installDir = getInstallPath() - val baseConfigDir = Paths.get(installDir, "share/$projectName/config.d").toString() - setSystemDefault("PATHS", "PREFIX", "${installDir}/") - setSystemDefault("PATHS", "BINDIR", "${installDir}/bin/") - setSystemDefault("PATHS", "LIBEXECDIR", "${installDir}/$projectName/libexec/") - setSystemDefault("PATHS", "DOCDIR", "${installDir}/share/doc/$projectName/") - setSystemDefault("PATHS", "ICONDIR", "${installDir}/share/icons/") - setSystemDefault("PATHS", "LOCALEDIR", "${installDir}/share/locale/") - setSystemDefault("PATHS", "LIBDIR", "${installDir}/lib/$projectName/") - setSystemDefault("PATHS", "DATADIR", "${installDir}/share/$projectName/") - loadDefaultsFromDir(baseConfigDir) - } - - private fun variableLookup(x: String, recursionDepth: Int = 0): String? { - val pathRes = this.lookupString("PATHS", x) - if (pathRes != null) { - return pathsub(pathRes, recursionDepth + 1) - } - val envVal = System.getenv(x) - if (envVal != null) { - return envVal - } - return null - } - - /** - * Substitute ${...} and $... placeholders in a string - * with values from the PATHS section in the - * configuration and environment variables - * - * This substitution is typically only done for paths. - */ - fun pathsub(x: String, recursionDepth: Int = 0): String { - if (recursionDepth > 128) { - throw TalerConfigError("recursion limit in path substitution exceeded") - } - val result = StringBuilder() - var l = 0 - val s = x - while (l < s.length) { - if (s[l] != '$') { - // normal character - result.append(s[l]) - l++; - continue - } - if (l + 1 < s.length && s[l + 1] == '{') { - // ${var} - var depth = 1 - val start = l - var p = start + 2; - var hasDefault = false - var insideNamePath = true - // Find end of the ${...} expression - while (p < s.length) { - if (s[p] == '}') { - insideNamePath = false - depth-- - } else if (s.length > p + 1 && s[p] == '$' && s[p + 1] == '{') { - depth++ - insideNamePath = false - } else if (s.length > p + 1 && insideNamePath && s[p] == ':' && s[p + 1] == '-') { - hasDefault = true - } - p++ - if (depth == 0) { - break - } - } - if (depth == 0) { - val inner = s.substring(start + 2, p - 1) - val varName: String - val varDefault: String? - if (hasDefault) { - val res = inner.split(":-", limit = 2) - varName = res[0] - varDefault = res[1] - } else { - varName = inner - varDefault = null - } - val r = variableLookup(varName, recursionDepth + 1) - if (r != null) { - result.append(r) - l = p - continue - } else if (varDefault != null) { - val resolvedDefault = pathsub(varDefault, recursionDepth + 1) - result.append(resolvedDefault) - l = p - continue - } else { - throw TalerConfigError("malformed variable expression can't resolve variable '$varName'") - } - } - throw TalerConfigError("malformed variable expression (unbalanced)") - } else { - // $var - var varEnd = l + 1 - while (varEnd < s.length && (s[varEnd].isLetterOrDigit() || s[varEnd] == '_')) { - varEnd++ - } - val varName = s.substring(l + 1, varEnd) - val res = variableLookup(varName) - if (res != null) { - result.append(res) - } - l = varEnd - } - } - return result.toString() - } - - /** - * Load configuration values from the file system. - * If no entrypoint is specified, the default entrypoint - * is used. - */ - fun load(entrypoint: String? = null) { - loadDefaults() - if (entrypoint != null) { - loadFromFilename(entrypoint) - } else { - val defaultFilename = findDefaultConfigFilename() - if (defaultFilename != null) { - loadFromFilename(defaultFilename) - } - } - } - - /** - * Determine the filename of the default configuration file. - * - * If no such file can be found, return null. - */ - private fun findDefaultConfigFilename(): String? { - val xdg = System.getenv("XDG_CONFIG_HOME") - val home = System.getenv("HOME") - - var filename: String? = null - if (xdg != null) { - filename = Paths.get(xdg, "$componentName.conf").toString() - } else if (home != null) { - filename = Paths.get(home, ".config/$componentName.conf").toString() - } - if (filename != null && File(filename).exists()) { - return filename - } - val etc1 = "/etc/$componentName.conf" - if (File(etc1).exists()) { - return etc1 - } - val etc2 = "/etc/$projectName/$componentName.conf" - if (File(etc2).exists()) { - return etc2 - } - return null - } - - /** - * Guess the path that the component was installed to. - */ - fun getInstallPath(): String { - // We use the location of the libeufin-bank - // binary to determine the installation prefix. - // If for some weird reason it's now found, we - // fall back to "/usr" as install prefix. - return getInstallPathFromBinary(installPathBinary) - } - - private fun getInstallPathFromBinary(name: String): String { - val pathEnv = System.getenv("PATH") - val paths = pathEnv.split(":") - for (p in paths) { - val possiblePath = Paths.get(p, name).toString() - if (File(possiblePath).exists()) { - return Paths.get(p, "..").toRealPath().toString() - } - } - return "/usr" - } - - /* ----- Lookup ----- */ - - /** - * Look up a string value from the configuration. - * - * Return null if the value was not found in the configuration. - */ - fun lookupString(section: String, option: String): String? { - val canonSection = section.uppercase() - val canonOption = option.uppercase() - return this.sectionMap[canonSection]?.entries?.get(canonOption) - } - - fun requireString(section: String, option: String): String = - lookupString(section, option) ?: - throw TalerConfigError("expected string in configuration section $section option $option") - - fun requireNumber(section: String, option: String): Int = - lookupString(section, option)?.toInt() ?: - throw TalerConfigError("expected number in configuration section $section option $option") - - fun lookupBoolean(section: String, option: String): Boolean? { - val entry = lookupString(section, option) ?: return null - return when (val v = entry.lowercase()) { - "yes" -> true - "no" -> false - else -> throw TalerConfigError("expected yes/no in configuration section $section option $option but got $v") - } - } - - fun requireBoolean(section: String, option: String): Boolean = - lookupBoolean(section, option) ?: - throw TalerConfigError("expected boolean in configuration section $section option $option") - - fun lookupPath(section: String, option: String): String? { - val entry = lookupString(section, option) ?: return null - return pathsub(entry) - } - - fun requirePath(section: String, option: String): String = - lookupPath(section, option) ?: - throw TalerConfigError("expected path for section $section option $option") -} diff --git a/util/src/main/kotlin/TalerErrorCode.kt b/util/src/main/kotlin/TalerErrorCode.kt @@ -1,4564 +0,0 @@ -/* - This file is part of GNU Taler - Copyright (C) 2012-2020 Taler Systems SA - - GNU Taler is free software: you can redistribute it and/or modify it - under the terms of the GNU Lesser General Public License as published - by the Free Software Foundation, either version 3 of the License, - or (at your option) any later version. - - GNU Taler 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - - SPDX-License-Identifier: LGPL3.0-or-later - - Note: the LGPL does not apply to all components of GNU Taler, - but it does apply to this file. - */ -package net.taler.common.errorcodes - -enum class TalerErrorCode(val code: Int) { - - - /** - * Special code to indicate success (no error). - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - NONE(0), - - - /** - * A non-integer error code was returned in the JSON response. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - INVALID(1), - - - /** - * An internal failure happened on the client side. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_CLIENT_INTERNAL_ERROR(2), - - - /** - * The response we got from the server was not even in JSON format. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_INVALID_RESPONSE(10), - - - /** - * An operation timed out. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_TIMEOUT(11), - - - /** - * The version string given does not follow the expected CURRENT:REVISION:AGE Format. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_VERSION_MALFORMED(12), - - - /** - * The service responded with a reply that was in JSON but did not satsify the protocol. Note that invalid cryptographic signatures should have signature-specific error codes. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_REPLY_MALFORMED(13), - - - /** - * There is an error in the client-side configuration, for example the base URL specified is malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_CONFIGURATION_INVALID(14), - - - /** - * The client made a request to a service, but received an error response it does not know how to handle. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_UNEXPECTED_REQUEST_ERROR(15), - - - /** - * The token used by the client to authorize the request does not grant the required permissions for the request. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_TOKEN_PERMISSION_INSUFFICIENT(16), - - - /** - * The HTTP method used is invalid for this endpoint. - * Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_METHOD_INVALID(20), - - - /** - * There is no endpoint defined for the URL provided by the client. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_ENDPOINT_UNKNOWN(21), - - - /** - * The JSON in the client's request was malformed (generic parse error). - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_JSON_INVALID(22), - - - /** - * Some of the HTTP headers provided by the client caused the server to not be able to handle the request. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_HTTP_HEADERS_MALFORMED(23), - - - /** - * The payto:// URI provided by the client is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_PAYTO_URI_MALFORMED(24), - - - /** - * A required parameter in the request was missing. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_PARAMETER_MISSING(25), - - - /** - * A parameter in the request was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_PARAMETER_MALFORMED(26), - - - /** - * The reserve public key given as part of a /reserves/ endpoint was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_RESERVE_PUB_MALFORMED(27), - - - /** - * The body in the request could not be decompressed by the server. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_COMPRESSION_INVALID(28), - - - /** - * The currency involved in the operation is not acceptable for this backend. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_CURRENCY_MISMATCH(30), - - - /** - * The URI is longer than the longest URI the HTTP server is willing to parse. - * Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_URI_TOO_LONG(31), - - - /** - * The body is too large to be permissible for the endpoint. - * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_UPLOAD_EXCEEDS_LIMIT(32), - - - /** - * The service refused the request due to lack of proper authorization. - * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_UNAUTHORIZED(40), - - - /** - * The service refused the request as the given authorization token is unknown. - * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_TOKEN_UNKNOWN(41), - - - /** - * The service refused the request as the given authorization token expired. - * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_TOKEN_EXPIRED(42), - - - /** - * The service refused the request as the given authorization token is malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_TOKEN_MALFORMED(43), - - - /** - * The service refused the request due to lack of proper rights on the resource. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_FORBIDDEN(44), - - - /** - * The service failed initialize its connection to the database. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_SETUP_FAILED(50), - - - /** - * The service encountered an error event to just start the database transaction. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_START_FAILED(51), - - - /** - * The service failed to store information in its database. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_STORE_FAILED(52), - - - /** - * The service failed to fetch information from its database. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_FETCH_FAILED(53), - - - /** - * The service encountered an error event to commit the database transaction (hard, unrecoverable error). - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_COMMIT_FAILED(54), - - - /** - * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.) - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_SOFT_FAILURE(55), - - - /** - * The service's database is inconsistent and violates service-internal invariants. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_DB_INVARIANT_FAILURE(56), - - - /** - * The HTTP server experienced an internal invariant failure (bug). - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_INTERNAL_INVARIANT_FAILURE(60), - - - /** - * The service could not compute a cryptographic hash over some JSON value. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_FAILED_COMPUTE_JSON_HASH(61), - - - /** - * The service could not compute an amount. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_FAILED_COMPUTE_AMOUNT(62), - - - /** - * The HTTP server had insufficient memory to parse the request. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_PARSER_OUT_OF_MEMORY(70), - - - /** - * The HTTP server failed to allocate memory. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_ALLOCATION_FAILURE(71), - - - /** - * The HTTP server failed to allocate memory for building JSON reply. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_JSON_ALLOCATION_FAILURE(72), - - - /** - * The HTTP server failed to allocate memory for making a CURL request. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_CURL_ALLOCATION_FAILURE(73), - - - /** - * The backend could not locate a required template to generate an HTML reply. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_FAILED_TO_LOAD_TEMPLATE(74), - - - /** - * The backend could not expand the template to generate an HTML reply. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - GENERIC_FAILED_TO_EXPAND_TEMPLATE(75), - - - /** - * Exchange is badly configured and thus cannot operate. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_BAD_CONFIGURATION(1000), - - - /** - * Operation specified unknown for this endpoint. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_OPERATION_UNKNOWN(1001), - - - /** - * The number of segments included in the URI does not match the number of segments expected by the endpoint. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS(1002), - - - /** - * The same coin was already used with a different denomination previously. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY(1003), - - - /** - * The public key of given to a "/coins/" endpoint of the exchange was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB(1004), - - - /** - * The exchange is not aware of the denomination key the wallet requested for the operation. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN(1005), - - - /** - * The signature of the denomination key over the coin is not valid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DENOMINATION_SIGNATURE_INVALID(1006), - - - /** - * The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request. - * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_KEYS_MISSING(1007), - - - /** - * Validity period of the denomination lies in the future. - * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE(1008), - - - /** - * Denomination key of the coin is past its expiration time for the requested operation. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_DENOMINATION_EXPIRED(1009), - - - /** - * Denomination key of the coin has been revoked. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_DENOMINATION_REVOKED(1010), - - - /** - * An operation where the exchange interacted with a security module timed out. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_SECMOD_TIMEOUT(1011), - - - /** - * The respective coin did not have sufficient residual value for the operation. The "history" in this response provides the "residual_value" of the coin, which may be less than its "original_value". - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_INSUFFICIENT_FUNDS(1012), - - - /** - * The exchange had an internal error reconstructing the transaction history of the coin that was being processed. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED(1013), - - - /** - * The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS(1014), - - - /** - * The same coin was already used with a different age hash previously. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH(1015), - - - /** - * The requested operation is not valid for the cipher used by the selected denomination. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION(1016), - - - /** - * The provided arguments for the operation use inconsistent ciphers. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_CIPHER_MISMATCH(1017), - - - /** - * The number of denominations specified in the request exceeds the limit of the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE(1018), - - - /** - * The coin is not known to the exchange (yet). - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_COIN_UNKNOWN(1019), - - - /** - * The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_CLOCK_SKEW(1020), - - - /** - * The specified amount for the coin is higher than the value of the denomination of the coin. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE(1021), - - - /** - * The exchange was not properly configured with global fees. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_GLOBAL_FEES_MISSING(1022), - - - /** - * The exchange was not properly configured with wire fees. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_WIRE_FEES_MISSING(1023), - - - /** - * The purse public key was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_PURSE_PUB_MALFORMED(1024), - - - /** - * The purse is unknown. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_PURSE_UNKNOWN(1025), - - - /** - * The purse has expired. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_PURSE_EXPIRED(1026), - - - /** - * The exchange has no information about the "reserve_pub" that was given. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_RESERVE_UNKNOWN(1027), - - - /** - * The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check. - * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_KYC_REQUIRED(1028), - - - /** - * Inconsistency between provided age commitment and attest: either none or both must be provided - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT(1029), - - - /** - * The provided attestation for the minimum age couldn't be verified by the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE(1030), - - - /** - * The purse was deleted. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_PURSE_DELETED(1031), - - - /** - * The public key of the AML officer in the URL was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED(1032), - - - /** - * The signature affirming the GET request of the AML officer is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID(1033), - - - /** - * The specified AML officer does not have access at this time. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED(1034), - - - /** - * The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later. - * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_AML_PENDING(1035), - - - /** - * The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator. - * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_AML_FROZEN(1036), - - - /** - * The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GENERIC_KYC_CONVERTER_FAILED(1037), - - - /** - * The exchange did not find information about the specified transaction in the database. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_NOT_FOUND(1100), - - - /** - * The wire hash of given to a "/deposits/" handler was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE(1101), - - - /** - * The merchant key of given to a "/deposits/" handler was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB(1102), - - - /** - * The hash of the contract terms given to a "/deposits/" handler was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS(1103), - - - /** - * The coin public key of given to a "/deposits/" handler was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB(1104), - - - /** - * The signature returned by the exchange in a /deposits/ request was malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE(1105), - - - /** - * The signature of the merchant is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID(1106), - - - /** - * The provided policy data was not accepted - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED(1107), - - - /** - * The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS(1150), - - - /** - * The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS(1151), - - - /** - * The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW(1152), - - - /** - * The exchange failed to create the signature using the denomination key. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_SIGNATURE_FAILED(1153), - - - /** - * The signature of the reserve is not valid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID(1154), - - - /** - * When computing the reserve history, we ended up with a negative overall balance, which should be impossible. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS(1155), - - - /** - * The reserve did not have sufficient funds in it to pay for a full reserve history statement. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE(1156), - - - /** - * Withdraw period of the coin to be withdrawn is in the past. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST(1158), - - - /** - * The client failed to unblind the blind signature. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_UNBLIND_FAILURE(1159), - - - /** - * The client re-used a withdraw nonce, which is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_NONCE_REUSE(1160), - - - /** - * The client provided an unknown commitment for an age-withdraw request. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN(1161), - - - /** - * The total sum of amounts from the denominations did overflow. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW(1162), - - - /** - * The total sum of value and fees from the denominations differs from the committed amount with fees. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT(1163), - - - /** - * The original commitment differs from the calculated hash - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH(1164), - - - /** - * The maximum age in the commitment is too large for the reserve - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE(1165), - - - /** - * The batch withdraw included a planchet that was already withdrawn. This is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET(1175), - - - /** - * The signature made by the coin over the deposit permission is not valid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID(1205), - - - /** - * The same coin was already deposited for the same merchant and contract with other details. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT(1206), - - - /** - * The stated value of the coin after the deposit fee is subtracted would be negative. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE(1207), - - - /** - * The stated refund deadline is after the wire deadline. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE(1208), - - - /** - * The stated wire deadline is "never", which makes no sense. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER(1209), - - - /** - * The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON(1210), - - - /** - * The hash of the given wire address does not match the wire hash specified in the proposal data. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT(1211), - - - /** - * The signature provided by the exchange is not valid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE(1221), - - - /** - * The deposited amount is smaller than the deposit fee, which would result in a negative contribution. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT(1222), - - - /** - * The proof of policy fulfillment was invalid. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT(1240), - - - /** - * The coin history was requested with a bad signature. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_COIN_HISTORY_BAD_SIGNATURE(1251), - - - /** - * The reserve history was requested with a bad signature. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE(1252), - - - /** - * The exchange encountered melt fees exceeding the melted coin's contribution. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION(1302), - - - /** - * The signature made with the coin to be melted is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MELT_COIN_SIGNATURE_INVALID(1303), - - - /** - * The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup). - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE(1305), - - - /** - * The signature returned by the exchange in a melt request was malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE(1306), - - - /** - * The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION(1353), - - - /** - * Failed to produce the blinded signatures over the coins to be returned. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR(1354), - - - /** - * The exchange is unaware of the refresh session specified in the request. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN(1355), - - - /** - * The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID(1356), - - - /** - * The number of envelopes given does not match the number of denomination keys given. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH(1358), - - - /** - * The exchange encountered a numeric overflow totaling up the cost for the refresh operation. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW(1359), - - - /** - * The exchange's cost calculation shows that the melt amount is below the costs of the transaction. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT(1360), - - - /** - * The signature made with the coin over the link data is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID(1361), - - - /** - * The refresh session hash given to a /refreshes/ handler was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_INVALID_RCH(1362), - - - /** - * Operation specified invalid for this endpoint. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID(1363), - - - /** - * The client provided age commitment data, but age restriction is not supported on this server. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED(1364), - - - /** - * The client provided invalid age commitment data: missing, not an array, or array of invalid size. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID(1365), - - - /** - * The coin specified in the link request is unknown to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_LINK_COIN_UNKNOWN(1400), - - - /** - * The public key of given to a /transfers/ handler was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_TRANSFERS_GET_WTID_MALFORMED(1450), - - - /** - * The exchange did not find information about the specified wire transfer identifier in the database. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND(1451), - - - /** - * The exchange did not find information about the wire transfer fees it charged. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND(1452), - - - /** - * The exchange found a wire fee that was above the total transfer value (and thus could not have been charged). - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT(1453), - - - /** - * The wait target of the URL was not in the set of expected values. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSES_INVALID_WAIT_TARGET(1475), - - - /** - * The signature on the purse status returned by the exchange was invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE(1476), - - - /** - * The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_COIN_NOT_FOUND(1500), - - - /** - * We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The "history" in the response proves this. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT(1501), - - - /** - * The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund). - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_DEPOSIT_NOT_FOUND(1502), - - - /** - * The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.) - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_MERCHANT_ALREADY_PAID(1503), - - - /** - * The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_FEE_TOO_LOW(1504), - - - /** - * The refunded amount is smaller than the refund fee, which would result in a negative refund. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_FEE_ABOVE_AMOUNT(1505), - - - /** - * The signature of the merchant is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID(1506), - - - /** - * Merchant backend failed to create the refund confirmation signature. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED(1507), - - - /** - * The signature returned by the exchange in a refund request was malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE(1508), - - - /** - * The failure proof returned by the exchange is incorrect. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE(1509), - - - /** - * Conflicting refund granted before with different amount but same refund transaction ID. - * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_REFUND_INCONSISTENT_AMOUNT(1510), - - - /** - * The given coin signature is invalid for the request. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_SIGNATURE_INVALID(1550), - - - /** - * The exchange could not find the corresponding withdraw operation. The request is denied. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND(1551), - - - /** - * The coin's remaining balance is zero. The request is denied. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_COIN_BALANCE_ZERO(1552), - - - /** - * The exchange failed to reproduce the coin's blinding. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_BLINDING_FAILED(1553), - - - /** - * The coin's remaining balance is zero. The request is denied. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE(1554), - - - /** - * The coin's denomination has not been revoked yet. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_NOT_ELIGIBLE(1555), - - - /** - * The given coin signature is invalid for the request. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID(1575), - - - /** - * The exchange could not find the corresponding melt operation. The request is denied. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND(1576), - - - /** - * The exchange failed to reproduce the coin's blinding. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED(1578), - - - /** - * The coin's denomination has not been revoked yet. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE(1580), - - - /** - * This exchange does not allow clients to request /keys for times other than the current (exchange) time. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN(1600), - - - /** - * A signature in the server's response was malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WIRE_SIGNATURE_INVALID(1650), - - - /** - * No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED(1651), - - - /** - * The payto:// URI stored in the exchange database for its bank account is malformed. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED(1652), - - - /** - * No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_WIRE_FEES_NOT_CONFIGURED(1653), - - - /** - * This purse was previously created with different meta data. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA(1675), - - - /** - * This purse was previously merged with different meta data. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA(1676), - - - /** - * The reserve has insufficient funds to create another purse. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS(1677), - - - /** - * The purse fee specified for the request is lower than the purse fee charged by the exchange at this time. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW(1678), - - - /** - * The payment request cannot be deleted anymore, as it either already completed or timed out. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DELETE_ALREADY_DECIDED(1679), - - - /** - * The signature affirming the purse deletion is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID(1680), - - - /** - * Withdrawal from the reserve requires age restriction to be set. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED(1681), - - - /** - * The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE(1700), - - - /** - * The response from the denomination key helper process was malformed. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DENOMINATION_HELPER_BUG(1701), - - - /** - * The helper refuses to sign with the key, because it is too early: the validity period has not yet started. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_DENOMINATION_HELPER_TOO_EARLY(1702), - - - /** - * The signature of the exchange on the reply was invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID(1725), - - - /** - * The exchange failed to talk to the process responsible for its private signing keys. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE(1750), - - - /** - * The response from the online signing key helper process was malformed. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_SIGNKEY_HELPER_BUG(1751), - - - /** - * The helper refuses to sign with the key, because it is too early: the validity period has not yet started. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_SIGNKEY_HELPER_TOO_EARLY(1752), - - - /** - * The purse expiration time is in the past at the time of its creation. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW(1775), - - - /** - * The purse expiration time is set to never, which is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER(1776), - - - /** - * The signature affirming the merge of the purse is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID(1777), - - - /** - * The signature by the reserve affirming the merge is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID(1778), - - - /** - * The signature by the reserve affirming the open operation is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE(1785), - - - /** - * The signature by the reserve affirming the close operation is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE(1786), - - - /** - * The signature by the reserve affirming the attestion request is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE(1787), - - - /** - * The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT(1788), - - - /** - * The reserve balance is insufficient to pay for the open operation. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS(1789), - - - /** - * The auditor that was supposed to be disabled is unknown to this exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND(1800), - - - /** - * The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected). - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT(1801), - - - /** - * The signature to add or enable the auditor does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID(1802), - - - /** - * The signature to disable the auditor does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID(1803), - - - /** - * The signature to revoke the denomination does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID(1804), - - - /** - * The signature to revoke the online signing key does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID(1805), - - - /** - * The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected). - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT(1806), - - - /** - * The signingkey specified is unknown to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN(1807), - - - /** - * The signature to publish wire account does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID(1808), - - - /** - * The signature to add the wire account does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID(1809), - - - /** - * The signature to disable the wire account does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID(1810), - - - /** - * The wire account to be disabled is unknown to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND(1811), - - - /** - * The signature to affirm wire fees does not validate. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID(1812), - - - /** - * The signature conflicts with a previous signature affirming different fees. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH(1813), - - - /** - * The signature affirming the denomination key is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID(1814), - - - /** - * The signature affirming the signing key is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID(1815), - - - /** - * The signature conflicts with a previous signature affirming different fees. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH(1816), - - - /** - * The signature affirming the fee structure is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID(1817), - - - /** - * The signature affirming the profit drain is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID(1818), - - - /** - * The signature affirming the AML decision is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID(1825), - - - /** - * The AML officer specified is not allowed to make AML decisions right now. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AML_DECISION_INVALID_OFFICER(1826), - - - /** - * There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT(1827), - - - /** - * There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AML_DECISION_UNKNOWN_CHECK(1828), - - - /** - * The signature affirming the change in the AML officer status is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID(1830), - - - /** - * A more recent decision about the AML officer status is known to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT(1831), - - - /** - * The purse was previously created with different meta data. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA(1850), - - - /** - * The purse was previously created with a different contract. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED(1851), - - - /** - * A coin signature for a deposit into the purse is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID(1852), - - - /** - * The purse expiration time is in the past. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW(1853), - - - /** - * The purse expiration time is "never". - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER(1854), - - - /** - * The purse signature over the purse meta data is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID(1855), - - - /** - * The signature over the encrypted contract is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID(1856), - - - /** - * The signature from the exchange over the confirmation is invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID(1857), - - - /** - * The coin was previously deposited with different meta data. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA(1858), - - - /** - * The encrypted contract was previously uploaded with different meta data. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA(1859), - - - /** - * The deposited amount is less than the purse fee. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE(1860), - - - /** - * The signature using the merge key is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE(1876), - - - /** - * The signature using the reserve key is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE(1877), - - - /** - * The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_NOT_FULL(1878), - - - /** - * The signature from the exchange over the confirmation is invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID(1879), - - - /** - * The exchange of the target account is not a partner of this exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN(1880), - - - /** - * The signature affirming the new partner is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID(1890), - - - /** - * Conflicting data for the partner already exists with the exchange. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT(1891), - - - /** - * The auditor signature over the denomination meta data is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID(1900), - - - /** - * The auditor that was specified is unknown to this exchange. - * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AUDITORS_AUDITOR_UNKNOWN(1901), - - - /** - * The auditor that was specified is no longer used by this exchange. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_AUDITORS_AUDITOR_INACTIVE(1902), - - - /** - * The signature affirming the wallet's KYC request was invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_WALLET_SIGNATURE_INVALID(1925), - - - /** - * The exchange received an unexpected malformed response from its KYC backend. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE(1926), - - - /** - * The backend signaled an unexpected failure. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_PROOF_BACKEND_ERROR(1927), - - - /** - * The backend signaled an authorization failure. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED(1928), - - - /** - * The exchange is unaware of having made an the authorization request. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN(1929), - - - /** - * The payto-URI hash did not match. Hence the request was denied. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED(1930), - - - /** - * The request used a logic specifier that is not known to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN(1931), - - - /** - * The request requires a logic which is no longer configured at the exchange. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_LOGIC_GONE(1932), - - - /** - * The logic plugin had a bug in its interaction with the KYC provider. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_LOGIC_BUG(1933), - - - /** - * The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator. - * Returned with an HTTP status code of #MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED (511). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED(1934), - - - /** - * There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed. - * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT(1935), - - - /** - * The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY(1936), - - - /** - * The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work. - * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED(1937), - - - /** - * The request to the webhook lacked proper authorization or authentication data. - * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED(1938), - - - /** - * The exchange does not know a contract under the given contract public key. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_CONTRACTS_UNKNOWN(1950), - - - /** - * The URL does not encode a valid exchange public key in its path. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB(1951), - - - /** - * The returned encrypted contract did not decrypt. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_CONTRACTS_DECRYPTION_FAILED(1952), - - - /** - * The signature on the encrypted contract did not validate. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_CONTRACTS_SIGNATURE_INVALID(1953), - - - /** - * The decrypted contract was malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_CONTRACTS_DECODING_FAILED(1954), - - - /** - * A coin signature for a deposit into the purse is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID(1975), - - - /** - * It is too late to deposit coins into the purse. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY(1976), - - - /** - * TOTP key is not valid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - EXCHANGE_TOTP_KEY_INVALID(1980), - - - /** - * The backend could not find the merchant instance specified in the request. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_INSTANCE_UNKNOWN(2000), - - - /** - * The start and end-times in the wire fee structure leave a hole. This is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE(2001), - - - /** - * The merchant was unable to obtain a valid answer to /wire from the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED(2002), - - - /** - * The proposal is not known to the backend. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_ORDER_UNKNOWN(2005), - - - /** - * The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_PRODUCT_UNKNOWN(2006), - - - /** - * The reward ID is unknown. This could happen if the reward has expired. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_REWARD_ID_UNKNOWN(2007), - - - /** - * The contract obtained from the merchant backend was malformed. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID(2008), - - - /** - * The order we found does not match the provided contract hash. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER(2009), - - - /** - * The exchange failed to provide a valid response to the merchant's /keys request. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE(2010), - - - /** - * The exchange failed to respond to the merchant on time. - * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_EXCHANGE_TIMEOUT(2011), - - - /** - * The merchant failed to talk to the exchange. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE(2012), - - - /** - * The exchange returned a maformed response. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED(2013), - - - /** - * The exchange returned an unexpected response status. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS(2014), - - - /** - * The merchant refused the request due to lack of authorization. - * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_UNAUTHORIZED(2015), - - - /** - * The merchant instance specified in the request was deleted. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_INSTANCE_DELETED(2016), - - - /** - * The backend could not find the inbound wire transfer specified in the request. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_TRANSFER_UNKNOWN(2017), - - - /** - * The backend could not find the template(id) because it is not exist. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_TEMPLATE_UNKNOWN(2018), - - - /** - * The backend could not find the webhook(id) because it is not exist. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_WEBHOOK_UNKNOWN(2019), - - - /** - * The backend could not find the webhook(serial) because it is not exist. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN(2020), - - - /** - * The backend could not find the OTP device(id) because it is not exist. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN(2021), - - - /** - * The account is not known to the backend. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_ACCOUNT_UNKNOWN(2022), - - - /** - * The wire hash was malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_H_WIRE_MALFORMED(2023), - - - /** - * The currency specified in the operation does not work with the current state of the given resource. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GENERIC_CURRENCY_MISMATCH(2024), - - - /** - * The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response. - * Returned with an HTTP status code of #MHD_HTTP_OK (200). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE(2100), - - - /** - * The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE(2103), - - - /** - * The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE(2104), - - - /** - * The claim token used to authenticate the client is invalid for this order. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GET_ORDERS_ID_INVALID_TOKEN(2105), - - - /** - * The contract terms hash used to authenticate the client is invalid for this order. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH(2106), - - - /** - * The exchange responded saying that funds were insufficient (for example, due to double-spending). - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS(2150), - - - /** - * The denomination key used for payment is not listed among the denomination keys of the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND(2151), - - - /** - * The denomination key used for payment is not audited by an auditor approved by the merchant. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE(2152), - - - /** - * There was an integer overflow totaling up the amounts or deposit fees in the payment. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW(2153), - - - /** - * The deposit fees exceed the total value of the payment. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT(2154), - - - /** - * After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover. - * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES(2155), - - - /** - * Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. - * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT(2156), - - - /** - * The signature over the contract of one of the coins was invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID(2157), - - - /** - * When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED(2158), - - - /** - * The refund deadline in the contract is after the transfer deadline. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE(2159), - - - /** - * The order was already paid (maybe by another wallet). - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID(2160), - - - /** - * The payment is too late, the offer has expired. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED(2161), - - - /** - * The "merchant" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING(2162), - - - /** - * Failed to locate merchant's account information matching the wire hash given in the proposal. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN(2163), - - - /** - * The deposit time for the denomination has expired. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED(2165), - - - /** - * The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high). - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED(2166), - - - /** - * The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds. - * Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_REFUNDED(2167), - - - /** - * According to our database, we have refunded more than we were paid (which should not be possible). - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS(2168), - - - /** - * Legacy stuff. Remove me with protocol v1. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - DEAD_QQQ_PAY_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2169), - - - /** - * The payment failed at the exchange. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED(2170), - - - /** - * The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING(2171), - - - /** - * The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH(2172), - - - /** - * The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED(2173), - - - /** - * The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING(2174), - - - /** - * The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED(2175), - - - /** - * The contract hash does not match the given order ID. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH(2200), - - - /** - * The signature of the merchant is not valid for the given contract hash. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID(2201), - - - /** - * The merchant failed to send the exchange the refund request. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED(2251), - - - /** - * The merchant failed to find the exchange to process the lookup. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED(2252), - - - /** - * The merchant could not find the contract. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND(2253), - - - /** - * The payment was already completed and thus cannot be aborted anymore. - * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE(2254), - - - /** - * The hash provided by the wallet does not match the order. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH(2255), - - - /** - * The array of coins cannot be empty. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY(2256), - - - /** - * We could not claim the order because the backend is unaware of it. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND(2300), - - - /** - * We could not claim the order because someone else claimed it first. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED(2301), - - - /** - * The client-side experienced an internal failure. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE(2302), - - - /** - * The backend failed to sign the refund request. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED(2350), - - - /** - * The client failed to unblind the signature returned by the merchant. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_REWARD_PICKUP_UNBLIND_FAILURE(2400), - - - /** - * The exchange returned a failure code for the withdraw operation. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_REWARD_PICKUP_EXCHANGE_ERROR(2403), - - - /** - * The merchant failed to add up the amounts to compute the pick up value. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_REWARD_PICKUP_SUMMATION_FAILED(2404), - - - /** - * The reward expired. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_REWARD_PICKUP_HAS_EXPIRED(2405), - - - /** - * The requested withdraw amount exceeds the amount remaining to be picked up. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_REWARD_PICKUP_AMOUNT_EXCEEDS_REWARD_REMAINING(2406), - - - /** - * The merchant did not find the specified denomination key in the exchange's key set. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_REWARD_PICKUP_DENOMINATION_UNKNOWN(2407), - - - /** - * The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE(2500), - - - /** - * The proposal had no timestamp and the merchant backend failed to obtain the current local time. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME(2501), - - - /** - * The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR(2502), - - - /** - * A conflicting order (sharing the same order identifier) already exists at this merchant backend instance. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS(2503), - - - /** - * The order creation request is invalid because the given wire deadline is before the refund deadline. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE(2504), - - - /** - * The order creation request is invalid because the delivery date given is in the past. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST(2505), - - - /** - * The order creation request is invalid because a wire deadline of "never" is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER(2506), - - - /** - * The order ceration request is invalid because the given payment deadline is in the past. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST(2507), - - - /** - * The order creation request is invalid because the given refund deadline is in the past. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST(2508), - - - /** - * The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD(2509), - - - /** - * One of the paths to forget is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT(2510), - - - /** - * One of the paths to forget was not marked as forgettable. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE(2511), - - - /** - * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT(2520), - - - /** - * The order provided to the backend could not be deleted as the order was already paid. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID(2521), - - - /** - * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT(2530), - - - /** - * Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID(2531), - - - /** - * The refund delay was set to 0 and thus no refunds are ever allowed for this order. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT(2532), - - - /** - * The exchange says it does not know this transfer. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN(2550), - - - /** - * We internally failed to execute the /track/transfer request. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR(2551), - - - /** - * The amount transferred differs between what was submitted and what the exchange claimed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS(2552), - - - /** - * The exchange gave conflicting information about a coin which has been wire transferred. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS(2553), - - - /** - * The exchange charged a different wire fee than what it originally advertised, and it is higher. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE(2554), - - - /** - * We did not find the account that the transfer was made to. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND(2555), - - - /** - * The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED(2556), - - - /** - * The backend was previously informed about a wire transfer with the same ID but a different amount. Multiple wire transfers with the same ID are not allowed. If the new amount is correct, the old transfer should first be deleted. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION(2557), - - - /** - * We are waiting for the exchange to provide us with key material before checking the wire transfer. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS(2258), - - - /** - * We are waiting for the exchange to provide us with the list of aggregated transactions. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST(2259), - - - /** - * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange. - * Returned with an HTTP status code of #MHD_HTTP_OK (200). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE(2260), - - - /** - * The exchange indicated in the wire transfer claims to know nothing about the wire transfer. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND(2261), - - - /** - * The interaction with the exchange is delayed due to rate limiting. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED(2262), - - - /** - * We experienced a transient failure in our interaction with the exchange. - * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE(2263), - - - /** - * The response from the exchange was unacceptable and should be reviewed with an auditor. - * Returned with an HTTP status code of #MHD_HTTP_OK (200). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE(2264), - - - /** - * The amount transferred differs between what was submitted and what the exchange claimed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS(2563), - - - /** - * The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS(2600), - - - /** - * The merchant backend cannot create an instance because the authentication configuration field is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH(2601), - - - /** - * The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH(2602), - - - /** - * The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED(2603), - - - /** - * The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED(2625), - - - /** - * The bank account referenced in the requested operation was not found. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT(2626), - - - /** - * The bank account specified in the request already exists at the merchant. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_ACCOUNT_EXISTS(2627), - - - /** - * The product ID exists. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS(2650), - - - /** - * The update would have reduced the total amount of product lost, which is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED(2660), - - - /** - * The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS(2661), - - - /** - * The update would have reduced the total amount of product in stock, which is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED(2662), - - - /** - * The update would have reduced the total amount of product sold, which is not allowed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED(2663), - - - /** - * The lock request is for more products than we have left (unlocked) in stock. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS(2670), - - - /** - * The deletion request is for a product that is locked. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK(2680), - - - /** - * The requested wire method is not supported by the exchange. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD(2700), - - - /** - * The requested exchange does not allow rewards. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_RESERVES_REWARDS_NOT_ALLOWED(2701), - - - /** - * The reserve could not be deleted because it is unknown. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE(2710), - - - /** - * The reserve that was used to fund the rewards has expired. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED(2750), - - - /** - * The reserve that was used to fund the rewards was not found in the DB. - * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_UNKNOWN(2751), - - - /** - * The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS(2752), - - - /** - * The backend failed to find a reserve needed to authorize the reward. - * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND(2753), - - - /** - * The merchant backend encountered a failure in computing the deposit total. - * Returned with an HTTP status code of #MHD_HTTP_OK (200). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE(2800), - - - /** - * The template ID already exists. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS(2850), - - - /** - * The OTP device ID already exists. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS(2851), - - - /** - * Amount given in the using template and in the template contract. There is a conflict. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT(2860), - - - /** - * Subject given in the using template and in the template contract. There is a conflict. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT(2861), - - - /** - * Amount not given in the using template and in the template contract. There is a conflict. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT(2862), - - - /** - * Subject not given in the using template and in the template contract. There is a conflict. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY(2863), - - - /** - * The webhook ID elready exists. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS(2900), - - - /** - * The webhook serial elready exists. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - MERCHANT_PRIVATE_POST_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS(2910), - - - /** - * The signature from the exchange on the deposit confirmation is invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID(3100), - - - /** - * The exchange key used for the signature on the deposit confirmation was revoked. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED(3101), - - - /** - * Wire transfer attempted with credit and debit party being the same bank account. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_SAME_ACCOUNT(5101), - - - /** - * Wire transfer impossible, due to financial limitation of the party that attempted the payment. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_UNALLOWED_DEBIT(5102), - - - /** - * Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NEGATIVE_NUMBER_AMOUNT(5103), - - - /** - * A too big number was used (as value and/or fraction) to instantiate an amount object. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NUMBER_TOO_BIG(5104), - - - /** - * The bank account referenced in the requested operation was not found. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_UNKNOWN_ACCOUNT(5106), - - - /** - * The transaction referenced in the requested operation (typically a reject operation), was not found. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TRANSACTION_NOT_FOUND(5107), - - - /** - * Bank received a malformed amount string. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_BAD_FORMAT_AMOUNT(5108), - - - /** - * The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_REJECT_NO_RIGHTS(5109), - - - /** - * This error code is returned when no known exception types captured the exception. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_UNMANAGED_EXCEPTION(5110), - - - /** - * This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_SOFT_EXCEPTION(5111), - - - /** - * The request UID for a request to transfer funds has already been used, but with different details for the transfer. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TRANSFER_REQUEST_UID_REUSED(5112), - - - /** - * The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT(5113), - - - /** - * The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_DUPLICATE_RESERVE_PUB_SUBJECT(5114), - - - /** - * The client requested a transaction that is so far in the past, that it has been forgotten by the bank. - * Returned with an HTTP status code of #MHD_HTTP_GONE (410). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_ANCIENT_TRANSACTION_GONE(5115), - - - /** - * The client attempted to abort a transaction that was already confirmed. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_ABORT_CONFIRM_CONFLICT(5116), - - - /** - * The client attempted to confirm a transaction that was already aborted. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_CONFIRM_ABORT_CONFLICT(5117), - - - /** - * The client attempted to register an account with the same name. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_REGISTER_CONFLICT(5118), - - - /** - * The client attempted to confirm a withdrawal operation before the wallet posted the required details. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_POST_WITHDRAWAL_OPERATION_REQUIRED(5119), - - - /** - * The client tried to register a new account under a reserved username (like 'admin' for example). - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_RESERVED_USERNAME_CONFLICT(5120), - - - /** - * The client tried to register a new account with an username already in use. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_REGISTER_USERNAME_REUSE(5121), - - - /** - * The client tried to register a new account with a payto:// URI already in use. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_REGISTER_PAYTO_URI_REUSE(5122), - - - /** - * The client tried to delete an account with a non null balance. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_ACCOUNT_BALANCE_NOT_ZERO(5123), - - - /** - * The client tried to create a transaction or an operation that credit an unknown account. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_UNKNOWN_CREDITOR(5124), - - - /** - * The client tried to create a transaction or an operation that debit an unknown account. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_UNKNOWN_DEBTOR(5125), - - - /** - * The client tried to perform an action prohibited for exchange accounts. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_ACCOUNT_IS_EXCHANGE(5126), - - - /** - * The client tried to perform an action reserved for exchange accounts. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_ACCOUNT_IS_NOT_EXCHANGE(5127), - - - /** - * Received currency conversion is wrong. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_BAD_CONVERSION(5128), - - - /** - * The account referenced in this operation is missing tan info for the chosen channel. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_MISSING_TAN_INFO(5129), - - - /** - * The client attempted to confirm a transaction with incomplete info. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_CONFIRM_INCOMPLETE(5130), - - - /** - * The request rate is too high. The server is refusing requests to guard against brute-force attacks. - * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TAN_RATE_LIMITED(5131), - - - /** - * This TAN channel is not supported. - * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TAN_CHANNEL_NOT_SUPPORTED(5132), - - - /** - * Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TAN_CHANNEL_SCRIPT_FAILED(5133), - - - /** - * The client's response to the challenge was invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TAN_CHALLENGE_FAILED(5134), - - - /** - * A non-admin user has tried to change their legal name. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NON_ADMIN_PATCH_LEGAL_NAME(5135), - - - /** - * A non-admin user has tried to change their debt limit. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NON_ADMIN_PATCH_DEBT_LIMIT(5136), - - - /** - * A non-admin user has tried to change their password whihout providing the current one. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD(5137), - - - /** - * Provided old password does not match current password. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_PATCH_BAD_OLD_PASSWORD(5138), - - - /** - * An admin user has tried to become an exchange. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_PATCH_ADMIN_EXCHANGE(5139), - - - /** - * A non-admin user has tried to change their cashout account. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NON_ADMIN_PATCH_CASHOUT(5140), - - - /** - * A non-admin user has tried to change their contact info. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NON_ADMIN_PATCH_CONTACT(5141), - - - /** - * The client tried to create a transaction that credit the admin account. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_ADMIN_CREDITOR(5142), - - - /** - * The referenced challenge was not found. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_CHALLENGE_NOT_FOUND(5143), - - - /** - * The referenced challenge has expired. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_TAN_CHALLENGE_EXPIRED(5144), - - - /** - * A non-admin user has tried to create an account with 2fa. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - BANK_NON_ADMIN_SET_TAN_CHANNEL(5145), - - - /** - * The sync service failed find the account in its database. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_ACCOUNT_UNKNOWN(6100), - - - /** - * The SHA-512 hash provided in the If-None-Match header is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_BAD_IF_NONE_MATCH(6101), - - - /** - * The SHA-512 hash provided in the If-Match header is malformed or missing. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_BAD_IF_MATCH(6102), - - - /** - * The signature provided in the "Sync-Signature" header is malformed or missing. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_BAD_SYNC_SIGNATURE(6103), - - - /** - * The signature provided in the "Sync-Signature" header does not match the account, old or new Etags. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_INVALID_SIGNATURE(6104), - - - /** - * The "Content-length" field for the upload is not a number. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_MALFORMED_CONTENT_LENGTH(6105), - - - /** - * The "Content-length" field for the upload is too big based on the server's terms of service. - * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_EXCESSIVE_CONTENT_LENGTH(6106), - - - /** - * The server is out of memory to handle the upload. Trying again later may succeed. - * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH(6107), - - - /** - * The uploaded data does not match the Etag. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_INVALID_UPLOAD(6108), - - - /** - * HTTP server experienced a timeout while awaiting promised payment. - * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_PAYMENT_GENERIC_TIMEOUT(6109), - - - /** - * Sync could not setup the payment request with its own backend. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_PAYMENT_CREATE_BACKEND_ERROR(6110), - - - /** - * The sync service failed find the backup to be updated in its database. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_PREVIOUS_BACKUP_UNKNOWN(6111), - - - /** - * The "Content-length" field for the upload is missing. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_MISSING_CONTENT_LENGTH(6112), - - - /** - * Sync had problems communicating with its payment backend. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_GENERIC_BACKEND_ERROR(6113), - - - /** - * Sync experienced a timeout communicating with its payment backend. - * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). - * (A value of 0 indicates that the error is generated client-side). - */ - SYNC_GENERIC_BACKEND_TIMEOUT(6114), - - - /** - * The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange. - * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE(7000), - - - /** - * The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_UNEXPECTED_EXCEPTION(7001), - - - /** - * The wallet received a response from a server, but the response can't be parsed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_RECEIVED_MALFORMED_RESPONSE(7002), - - - /** - * The wallet tried to make a network request, but it received no response. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_NETWORK_ERROR(7003), - - - /** - * The wallet tried to make a network request, but it was throttled. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_HTTP_REQUEST_THROTTLED(7004), - - - /** - * The wallet made a request to a service, but received an error response it does not know how to handle. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_UNEXPECTED_REQUEST_ERROR(7005), - - - /** - * The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT(7006), - - - /** - * The wallet does not support the operation requested by a client. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CORE_API_OPERATION_UNKNOWN(7007), - - - /** - * The given taler://pay URI is invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_INVALID_TALER_PAY_URI(7008), - - - /** - * The signature on a coin by the exchange's denomination key is invalid after unblinding it. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_EXCHANGE_COIN_SIGNATURE_INVALID(7009), - - - /** - * The exchange does not know about the reserve (yet), and thus withdrawal can't progress. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE(7010), - - - /** - * The wallet core service is not available. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CORE_NOT_AVAILABLE(7011), - - - /** - * The bank has aborted a withdrawal operation, and thus a withdrawal can't complete. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK(7012), - - - /** - * An HTTP request made by the wallet timed out. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_HTTP_REQUEST_GENERIC_TIMEOUT(7013), - - - /** - * The order has already been claimed by another wallet. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_ORDER_ALREADY_CLAIMED(7014), - - - /** - * A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_WITHDRAWAL_GROUP_INCOMPLETE(7015), - - - /** - * The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_REWARD_COIN_SIGNATURE_INVALID(7016), - - - /** - * The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE(7017), - - - /** - * The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH(7018), - - - /** - * The merchant's signature on the contract terms is invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CONTRACT_TERMS_SIGNATURE_INVALID(7019), - - - /** - * The contract terms given by the merchant are malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CONTRACT_TERMS_MALFORMED(7020), - - - /** - * A pending operation failed, and thus the request can't be completed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_PENDING_OPERATION_FAILED(7021), - - - /** - * A payment was attempted, but the merchant had an internal server error (5xx). - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_PAY_MERCHANT_SERVER_ERROR(7022), - - - /** - * The crypto worker failed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CRYPTO_WORKER_ERROR(7023), - - - /** - * The crypto worker received a bad request. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_CRYPTO_WORKER_BAD_REQUEST(7024), - - - /** - * A KYC step is required before withdrawal can proceed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_WITHDRAWAL_KYC_REQUIRED(7025), - - - /** - * The wallet does not have sufficient balance to create a deposit group. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE(7026), - - - /** - * The wallet does not have sufficient balance to create a peer push payment. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE(7027), - - - /** - * The wallet does not have sufficient balance to pay for an invoice. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE(7028), - - - /** - * A group of refresh operations has errors and will be tried again later. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_REFRESH_GROUP_INCOMPLETE(7029), - - - /** - * The exchange's self-reported base URL does not match the one that the wallet is using. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_EXCHANGE_BASE_URL_MISMATCH(7030), - - - /** - * The order has already been paid by another wallet. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - WALLET_ORDER_ALREADY_PAID(7031), - - - /** - * We encountered a timeout with our payment backend. - * Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_BACKEND_TIMEOUT(8000), - - - /** - * The backend requested payment, but the request is malformed. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST(8001), - - - /** - * The backend got an unexpected reply from the payment processor. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_BACKEND_ERROR(8002), - - - /** - * The "Content-length" field for the upload is missing. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH(8003), - - - /** - * The "Content-length" field for the upload is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH(8004), - - - /** - * The backend failed to setup an order with the payment processor. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR(8005), - - - /** - * The backend was not authorized to check for payment with the payment processor. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED(8006), - - - /** - * The backend could not check payment status with the payment processor. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED(8007), - - - /** - * The Anastasis provider could not be reached. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_GENERIC_PROVIDER_UNREACHABLE(8008), - - - /** - * HTTP server experienced a timeout while awaiting promised payment. - * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_PAYMENT_GENERIC_TIMEOUT(8009), - - - /** - * The key share is unknown to the provider. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_UNKNOWN(8108), - - - /** - * The authorization method used for the key share is no longer supported by the provider. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED(8109), - - - /** - * The client needs to respond to the challenge. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED(8110), - - - /** - * The client's response to the challenge was invalid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_CHALLENGE_FAILED(8111), - - - /** - * The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_CHALLENGE_UNKNOWN(8112), - - - /** - * The backend failed to initiate the authorization process. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED(8114), - - - /** - * The authorization succeeded, but the key share is no longer available. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_KEY_SHARE_GONE(8115), - - - /** - * The backend forgot the order we asked the client to pay for - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_ORDER_DISAPPEARED(8116), - - - /** - * The backend itself reported a bad exchange interaction. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD(8117), - - - /** - * The backend reported a payment status we did not expect. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS(8118), - - - /** - * The backend failed to setup the order for payment. - * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR(8119), - - - /** - * The decryption of the key share failed with the provided key. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_DECRYPTION_FAILED(8120), - - - /** - * The request rate is too high. The server is refusing requests to guard against brute-force attacks. - * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_RATE_LIMITED(8121), - - - /** - * A request to issue a challenge is not valid for this authentication method. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD(8123), - - - /** - * The backend failed to store the key share because the UUID is already in use. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS(8150), - - - /** - * The backend failed to store the key share because the authorization method is not supported. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED(8151), - - - /** - * The provided phone number is not an acceptable number. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_SMS_PHONE_INVALID(8200), - - - /** - * Failed to run the SMS transmission helper process. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_SMS_HELPER_EXEC_FAILED(8201), - - - /** - * Provider failed to send SMS. Helper terminated with a non-successful result. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_SMS_HELPER_COMMAND_FAILED(8202), - - - /** - * The provided email address is not an acceptable address. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_EMAIL_INVALID(8210), - - - /** - * Failed to run the E-mail transmission helper process. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_EMAIL_HELPER_EXEC_FAILED(8211), - - - /** - * Provider failed to send E-mail. Helper terminated with a non-successful result. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_EMAIL_HELPER_COMMAND_FAILED(8212), - - - /** - * The provided postal address is not an acceptable address. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POST_INVALID(8220), - - - /** - * Failed to run the mail transmission helper process. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POST_HELPER_EXEC_FAILED(8221), - - - /** - * Provider failed to send mail. Helper terminated with a non-successful result. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POST_HELPER_COMMAND_FAILED(8222), - - - /** - * The provided IBAN address is not an acceptable IBAN. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_IBAN_INVALID(8230), - - - /** - * The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_IBAN_MISSING_TRANSFER(8231), - - - /** - * The backend did not find a TOTP key in the data provided. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TOTP_KEY_MISSING(8240), - - - /** - * The key provided does not satisfy the format restrictions for an Anastasis TOTP key. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_TOTP_KEY_INVALID(8241), - - - /** - * The given if-none-match header is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POLICY_BAD_IF_NONE_MATCH(8301), - - - /** - * The server is out of memory to handle the upload. Trying again later may succeed. - * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH(8304), - - - /** - * The signature provided in the "Anastasis-Policy-Signature" header is malformed or missing. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POLICY_BAD_SIGNATURE(8305), - - - /** - * The given if-match header is malformed. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POLICY_BAD_IF_MATCH(8306), - - - /** - * The uploaded data does not match the Etag. - * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POLICY_INVALID_UPLOAD(8307), - - - /** - * The provider is unaware of the requested policy. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_POLICY_NOT_FOUND(8350), - - - /** - * The given action is invalid for the current state of the reducer. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_ACTION_INVALID(8400), - - - /** - * The given state of the reducer is invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_STATE_INVALID(8401), - - - /** - * The given input to the reducer is invalid. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_INPUT_INVALID(8402), - - - /** - * The selected authentication method does not work for the Anastasis provider. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED(8403), - - - /** - * The given input and action do not work for the current state. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE(8404), - - - /** - * We experienced an unexpected failure interacting with the backend. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_BACKEND_FAILURE(8405), - - - /** - * The contents of a resource file did not match our expectations. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_RESOURCE_MALFORMED(8406), - - - /** - * A required resource file is missing. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_RESOURCE_MISSING(8407), - - - /** - * An input did not match the regular expression. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_INPUT_REGEX_FAILED(8408), - - - /** - * An input did not match the custom validation logic. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED(8409), - - - /** - * Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED(8410), - - - /** - * Anastasis provider reported a fatal failure. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED(8411), - - - /** - * Anastasis provider failed to respond to the configuration request. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED(8412), - - - /** - * The policy we downloaded is malformed. Must have been a client error while creating the backup. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_POLICY_MALFORMED(8413), - - - /** - * We failed to obtain the policy, likely due to a network issue. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_NETWORK_FAILED(8414), - - - /** - * The recovered secret did not match the required syntax. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_SECRET_MALFORMED(8415), - - - /** - * The challenge data provided is too large for the available providers. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG(8416), - - - /** - * The provided core secret is too large for some of the providers. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_SECRET_TOO_BIG(8417), - - - /** - * The provider returned in invalid configuration. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG(8418), - - - /** - * The reducer encountered an internal error, likely a bug that needs to be reported. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_INTERNAL_ERROR(8419), - - - /** - * The reducer already synchronized with all providers. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED(8420), - - - /** - * A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - LIBEUFIN_NEXUS_GENERIC_ERROR(9000), - - - /** - * An uncaught exception happened in the LibEuFin nexus service. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION(9001), - - - /** - * A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - LIBEUFIN_SANDBOX_GENERIC_ERROR(9500), - - - /** - * An uncaught exception happened in the LibEuFin sandbox service. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION(9501), - - - /** - * This validation method is not supported by the service. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - TALDIR_METHOD_NOT_SUPPORTED(9600), - - - /** - * Number of allowed attempts for initiating a challenge exceeded. - * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). - * (A value of 0 indicates that the error is generated client-side). - */ - TALDIR_REGISTER_RATE_LIMITED(9601), - - - /** - * The client is unknown or unauthorized. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_GENERIC_CLIENT_UNKNOWN(9750), - - - /** - * The client is not authorized to use the given redirect URI. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI(9751), - - - /** - * The service failed to execute its helper process to send the challenge. - * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_HELPER_EXEC_FAILED(9752), - - - /** - * The grant is unknown to the service (it could also have expired). - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_GRANT_UNKNOWN(9753), - - - /** - * The code given is not even well-formed. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE(9754), - - - /** - * The service is not aware of the referenced validation process. - * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_GENERIC_VALIDATION_UNKNOWN(9755), - - - /** - * The code given is not valid. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_CLIENT_FORBIDDEN_INVALID_CODE(9756), - - - /** - * Too many attempts have been made, validation is temporarily disabled for this address. - * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_TOO_MANY_ATTEMPTS(9757), - - - /** - * The PIN code provided is incorrect. - * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_INVALID_PIN(9758), - - - /** - * The token cannot be valid as no address was ever provided by the client. - * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409). - * (A value of 0 indicates that the error is generated client-side). - */ - CHALLENGER_MISSING_ADDRESS(9759), - - - /** - * End of error code range. - * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0). - * (A value of 0 indicates that the error is generated client-side). - */ - END(9999), - - -} diff --git a/util/src/main/kotlin/XMLUtil.kt b/util/src/main/kotlin/XMLUtil.kt @@ -1,560 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import com.sun.xml.bind.marshaller.NamespacePrefixMapper -import io.ktor.http.* -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.w3c.dom.Document -import org.w3c.dom.Node -import org.w3c.dom.NodeList -import org.w3c.dom.ls.LSInput -import org.w3c.dom.ls.LSResourceResolver -import org.xml.sax.ErrorHandler -import org.xml.sax.InputSource -import org.xml.sax.SAXException -import org.xml.sax.SAXParseException -import tech.libeufin.util.ebics_h004.EbicsResponse -import java.io.* -import java.security.PrivateKey -import java.security.PublicKey -import java.security.interfaces.RSAPrivateCrtKey -import javax.xml.XMLConstants -import javax.xml.bind.JAXBContext -import javax.xml.bind.JAXBElement -import javax.xml.bind.Marshaller -import javax.xml.crypto.* -import javax.xml.crypto.dom.DOMURIReference -import javax.xml.crypto.dsig.* -import javax.xml.crypto.dsig.dom.DOMSignContext -import javax.xml.crypto.dsig.dom.DOMValidateContext -import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec -import javax.xml.crypto.dsig.spec.TransformParameterSpec -import javax.xml.namespace.NamespaceContext -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.OutputKeys -import javax.xml.transform.Source -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult -import javax.xml.transform.stream.StreamSource -import javax.xml.validation.SchemaFactory -import javax.xml.validation.Validator -import javax.xml.xpath.XPath -import javax.xml.xpath.XPathConstants -import javax.xml.xpath.XPathFactory - -private val logger: Logger = LoggerFactory.getLogger("libeufin-xml") - -class DefaultNamespaces : NamespacePrefixMapper() { - override fun getPreferredPrefix(namespaceUri: String?, suggestion: String?, requirePrefix: Boolean): String? { - if (namespaceUri == "http://www.w3.org/2000/09/xmldsig#") return "ds" - if (namespaceUri == XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI) return "xsi" - return null - } -} - -class DOMInputImpl : LSInput { - var fPublicId: String? = null - var fSystemId: String? = null - var fBaseSystemId: String? = null - var fByteStream: InputStream? = null - var fCharStream: Reader? = null - var fData: String? = null - var fEncoding: String? = null - var fCertifiedText = false - - override fun getByteStream(): InputStream? { - return fByteStream - } - - override fun setByteStream(byteStream: InputStream) { - fByteStream = byteStream - } - - override fun getCharacterStream(): Reader? { - return fCharStream - } - - override fun setCharacterStream(characterStream: Reader) { - fCharStream = characterStream - } - - override fun getStringData(): String? { - return fData - } - - override fun setStringData(stringData: String) { - fData = stringData - } - - override fun getEncoding(): String? { - return fEncoding - } - - override fun setEncoding(encoding: String) { - fEncoding = encoding - } - - override fun getPublicId(): String? { - return fPublicId - } - - override fun setPublicId(publicId: String) { - fPublicId = publicId - } - - override fun getSystemId(): String? { - return fSystemId - } - - override fun setSystemId(systemId: String) { - fSystemId = systemId - } - - override fun getBaseURI(): String? { - return fBaseSystemId - } - - override fun setBaseURI(baseURI: String) { - fBaseSystemId = baseURI - } - - override fun getCertifiedText(): Boolean { - return fCertifiedText - } - - override fun setCertifiedText(certifiedText: Boolean) { - fCertifiedText = certifiedText - } -} - - -/** - * Helpers for dealing with XML in EBICS. - */ -class XMLUtil private constructor() { - /** - * This URI dereferencer allows handling the resource reference used for - * XML signatures in EBICS. - */ - private class EbicsSigUriDereferencer : URIDereferencer { - override fun dereference(myRef: URIReference?, myCtx: XMLCryptoContext?): Data { - val ebicsXpathExpr = "//*[@authenticate='true']" - if (myRef !is DOMURIReference) - throw Exception("invalid type") - 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 - ) - if (nodeSet !is NodeList) - throw Exception("invalid type") - if (nodeSet.length <= 0) { - throw Exception("no nodes to sign") - } - val nodeList = ArrayList<Node>() - for (i in 0 until nodeSet.length) { - val node = nodeSet.item(i) - nodeList.add(node) - } - return NodeSetData { nodeList.iterator() } - } - } - - companion object { - private var cachedEbicsValidator: Validator? = null - private fun getEbicsValidator(): Validator { - val currentValidator = cachedEbicsValidator - if (currentValidator != null) - return currentValidator - val classLoader = ClassLoader.getSystemClassLoader() - val sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "file") - sf.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "") - sf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) - sf.errorHandler = object : ErrorHandler { - override fun warning(p0: SAXParseException?) { - println("Warning: $p0") - } - - override fun error(p0: SAXParseException?) { - println("Error: $p0") - } - - override fun fatalError(p0: SAXParseException?) { - println("Fatal error: $p0") - } - } - sf.resourceResolver = object : LSResourceResolver { - override fun resolveResource( - type: String?, - namespaceURI: String?, - publicId: String?, - systemId: String?, - baseUri: String? - ): LSInput? { - if (type != "http://www.w3.org/2001/XMLSchema") { - return null - } - val res = classLoader.getResourceAsStream("xsd/$systemId") ?: return null - return DOMInputImpl().apply { - fPublicId = publicId - fSystemId = systemId - fBaseSystemId = baseUri - fByteStream = res - fEncoding = "UTF-8" - } - } - } - val schemaInputs: Array<Source> = listOf( - "xsd/ebics_H004.xsd", - "xsd/ebics_H005.xsd", - "xsd/ebics_hev.xsd", - "xsd/camt.052.001.02.xsd", - "xsd/camt.053.001.02.xsd", - "xsd/camt.054.001.02.xsd", - "xsd/pain.001.001.03.xsd", - // "xsd/pain.001.001.03.ch.02.xsd", // Swiss 2013 version. - "xsd/pain.001.001.09.ch.03.xsd" // Swiss 2019 version. - ).map { - val stream = - classLoader.getResourceAsStream(it) ?: throw FileNotFoundException("Schema file $it not found.") - StreamSource(stream) - }.toTypedArray() - val bundle = sf.newSchema(schemaInputs) - val newValidator = bundle.newValidator() - cachedEbicsValidator = newValidator - return newValidator - } - - /** - * - * @param xmlDoc the XML document to validate - * @return true when validation passes, false otherwise - */ - @Synchronized fun validate(xmlDoc: StreamSource): Boolean { - try { - getEbicsValidator().validate(xmlDoc) - } catch (e: Exception) { - /** - * Would be convenient to return also the error - * message to the caller, so that it can link it - * to a document ID in the logs. - */ - logger.warn("Validation failed: ${e}") - return false - } - return true; - } - - /** - * Validates the DOM against the Schema(s) of this object. - * @param domDocument DOM to validate - * @return true/false if the document is valid/invalid - */ - @Synchronized fun validateFromDom(domDocument: Document): Boolean { - try { - getEbicsValidator().validate(DOMSource(domDocument)) - } catch (e: SAXException) { - e.printStackTrace() - return false - } - return true - } - - /** - * Craft object to be passed to the XML validator. - * @param xmlString XML body, as read from the POST body. - * @return InputStream object, as wanted by the validator. - */ - fun validateFromString(xmlString: String): Boolean { - val xmlInputStream: InputStream = ByteArrayInputStream(xmlString.toByteArray()) - val xmlSource = StreamSource(xmlInputStream) - return validate(xmlSource) - } - - inline fun <reified T> convertJaxbToString( - obj: T, - withSchemaLocation: String? = null - ): String { - val sw = StringWriter() - val jc = JAXBContext.newInstance(T::class.java) - val m = jc.createMarshaller() - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - if (withSchemaLocation != null) { - m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) - } - m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) - m.marshal(obj, sw) - return sw.toString() - } - - inline fun <reified T> convertJaxbToDocument( - obj: T, - withSchemaLocation: String? = null - ): Document { - val dbf: DocumentBuilderFactory = DocumentBuilderFactory.newInstance() - dbf.isNamespaceAware = true - val doc = dbf.newDocumentBuilder().newDocument() - val jc = JAXBContext.newInstance(T::class.java) - val m = jc.createMarshaller() - m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true) - if (withSchemaLocation != null) { - m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, withSchemaLocation) - } - m.setProperty("com.sun.xml.bind.namespacePrefixMapper", DefaultNamespaces()) - m.marshal(obj, doc) - return doc - } - - /** - * Convert a XML string to the JAXB representation. - * - * @param documentString the string to convert into JAXB. - * @return the JAXB object reflecting the original XML document. - */ - inline fun <reified T> convertStringToJaxb(documentString: String): JAXBElement<T> { - val jc = JAXBContext.newInstance(T::class.java) - val u = jc.createUnmarshaller() - return u.unmarshal( /* Marshalling the object into the document. */ - StreamSource(StringReader(documentString)), - T::class.java - ) - } - - /** - * Extract String from DOM. - * - * @param document the DOM to extract the string from. - * @return the final String, or null if errors occur. - */ - fun convertDomToString(document: Document): String { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - - /* Make string writer. */ - val sw = StringWriter() - - /* Extract string. */ - t.transform(DOMSource(document), StreamResult(sw)) - return sw.toString() - } - - /** - * Convert a node to a string without the XML declaration or - * indentation. - */ - fun convertNodeToString(node: Node): String { - /* Make Transformer. */ - val tf = TransformerFactory.newInstance() - val t = tf.newTransformer() - t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); - /* Make string writer. */ - val sw = StringWriter() - /* Extract string. */ - t.transform(DOMSource(node), StreamResult(sw)) - return sw.toString() - } - - /** - * Convert a DOM document to the JAXB representation. - * - * @param finalType class type of the output - * @param document the document to convert into JAXB. - * @return the JAXB object reflecting the original XML document. - */ - fun <T> convertDomToJaxb(finalType: Class<T>, document: Document): JAXBElement<T> { - val jc = JAXBContext.newInstance(finalType) - /* Marshalling the object into the document. */ - val m = jc.createUnmarshaller() - return m.unmarshal(document, finalType) // document "went" into Jaxb - } - - /** - * Parse string into XML DOM. - * @param xmlString the string to parse. - * @return the DOM representing @a xmlString - */ - fun parseStringIntoDom(xmlString: String): Document { - val factory = DocumentBuilderFactory.newInstance().apply { - isNamespaceAware = true - } - val xmlInputStream = ByteArrayInputStream(xmlString.toByteArray()) - val builder = factory.newDocumentBuilder() - return builder.parse(InputSource(xmlInputStream)) - } - - fun signEbicsResponse(ebicsResponse: EbicsResponse, privateKey: RSAPrivateCrtKey): String { - val doc = convertJaxbToDocument(ebicsResponse) - signEbicsDocument(doc, privateKey) - val signedDoc = XMLUtil.convertDomToString(doc) - // logger.debug("response: $signedDoc") - return signedDoc - } - - /** - * Sign an EBICS document with the authentication and identity signature. - */ - fun signEbicsDocument( - doc: Document, - signingPriv: PrivateKey, - withEbics3: Boolean = false - ) { - val xpath = XPathFactory.newInstance().newXPath() - xpath.namespaceContext = object : NamespaceContext { - override fun getNamespaceURI(p0: String?): String { - return when (p0) { - "ebics" -> if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" - else -> throw IllegalArgumentException() - } - } - - override fun getPrefix(p0: String?): String { - throw UnsupportedOperationException() - } - - override fun getPrefixes(p0: String?): MutableIterator<String> { - throw UnsupportedOperationException() - } - } - val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc, XPathConstants.NODE) - if (authSigNode !is Node) - throw java.lang.Exception("no AuthSignature") - val fac = XMLSignatureFactory.getInstance("DOM") - val c14n = fac.newTransform(CanonicalizationMethod.INCLUSIVE, null as TransformParameterSpec?) - val ref: Reference = - fac.newReference( - "#xpointer(//*[@authenticate='true'])", - fac.newDigestMethod(DigestMethod.SHA256, null), - listOf(c14n), - null, - null - ) - val canon: CanonicalizationMethod = - fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE, null as C14NMethodParameterSpec?) - val signatureMethod = fac.newSignatureMethod("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", null) - val si: SignedInfo = fac.newSignedInfo(canon, signatureMethod, listOf(ref)) - val sig: XMLSignature = fac.newXMLSignature(si, null) - val dsc = DOMSignContext(signingPriv, authSigNode) - dsc.defaultNamespacePrefix = "ds" - dsc.uriDereferencer = EbicsSigUriDereferencer() - dsc.setProperty("javax.xml.crypto.dsig.cacheReference", true) - sig.sign(dsc) - val innerSig = authSigNode.firstChild - while (innerSig.hasChildNodes()) { - authSigNode.appendChild(innerSig.firstChild) - } - authSigNode.removeChild(innerSig) - } - - fun verifyEbicsDocument( - doc: Document, - signingPub: PublicKey, - withEbics3: Boolean = false - ): Boolean { - val xpath = XPathFactory.newInstance().newXPath() - xpath.namespaceContext = object : NamespaceContext { - override fun getNamespaceURI(p0: String?): String { - return when (p0) { - "ebics" -> if (withEbics3) "urn:org:ebics:H005" else "urn:org:ebics:H004" - else -> throw IllegalArgumentException() - } - } - - override fun getPrefix(p0: String?): String { - throw UnsupportedOperationException() - } - - override fun getPrefixes(p0: String?): MutableIterator<String> { - throw UnsupportedOperationException() - } - } - val doc2: Document = doc.cloneNode(true) as Document - val authSigNode = xpath.compile("/*[1]/ebics:AuthSignature").evaluate(doc2, XPathConstants.NODE) - if (authSigNode !is Node) - throw java.lang.Exception("no AuthSignature") - val sigEl = doc2.createElementNS("http://www.w3.org/2000/09/xmldsig#", "ds:Signature") - authSigNode.parentNode.insertBefore(sigEl, authSigNode) - while (authSigNode.hasChildNodes()) { - sigEl.appendChild(authSigNode.firstChild) - } - authSigNode.parentNode.removeChild(authSigNode) - val fac = XMLSignatureFactory.getInstance("DOM") - val dvc = DOMValidateContext(signingPub, sigEl) - dvc.setProperty("javax.xml.crypto.dsig.cacheReference", true) - dvc.uriDereferencer = EbicsSigUriDereferencer() - val sig = fac.unmarshalXMLSignature(dvc) - // FIXME: check that parameters are okay! - val valResult = sig.validate(dvc) - sig.signedInfo.references[0].validate(dvc) - return valResult - } - - fun getNodeFromXpath(doc: Document, query: String): Node { - val xpath = XPathFactory.newInstance().newXPath() - val ret = xpath.evaluate(query, doc, XPathConstants.NODE) - ?: throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") - return ret as Node - } - - fun getStringFromXpath(doc: Document, query: String): String { - val xpath = XPathFactory.newInstance().newXPath() - val ret = xpath.evaluate(query, doc, XPathConstants.STRING) as String - if (ret.isEmpty()) { - throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $query") - } - return ret - } - } -} - -fun Document.pickString(xpath: String): String { - return XMLUtil.getStringFromXpath(this, xpath) -} - -fun Document.pickStringWithRootNs(xpathQuery: String): String { - val doc = this - val xpath = XPathFactory.newInstance().newXPath() - xpath.namespaceContext = object : NamespaceContext { - override fun getNamespaceURI(p0: String?): String { - return when (p0) { - "root" -> doc.documentElement.namespaceURI - else -> throw IllegalArgumentException() - } - } - - override fun getPrefix(p0: String?): String { - throw UnsupportedOperationException() - } - - override fun getPrefixes(p0: String?): MutableIterator<String> { - throw UnsupportedOperationException() - } - } - val ret = xpath.evaluate(xpathQuery, this, XPathConstants.STRING) as String - if (ret.isEmpty()) { - throw EbicsProtocolError(HttpStatusCode.NotFound, "Unsuccessful XPath query string: $xpathQuery") - } - return ret -} -\ No newline at end of file diff --git a/util/src/main/kotlin/XmlCombinators.kt b/util/src/main/kotlin/XmlCombinators.kt @@ -1,184 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2020 Taler Systems S.A. - * - * 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.util - -import com.sun.xml.txw2.output.IndentingXMLStreamWriter -import org.w3c.dom.Document -import org.w3c.dom.Element -import java.io.StringWriter -import javax.xml.stream.XMLOutputFactory -import javax.xml.stream.XMLStreamWriter - -class XmlElementBuilder(val w: XMLStreamWriter) { - /** - * First consumes all the path's components, and _then_ starts applying f. - */ - fun element(path: MutableList<String>, f: XmlElementBuilder.() -> Unit = {}) { - /* the wanted path got constructed, go on with f's logic now. */ - if (path.isEmpty()) { - f() - return - } - w.writeStartElement(path.removeAt(0)) - 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) - } -} - -class XmlDocumentBuilder { - - private var maybeWriter: XMLStreamWriter? = null - internal var writer: XMLStreamWriter - get() { - val w = maybeWriter - return w ?: throw AssertionError("no writer set") - } - set(w: XMLStreamWriter) { - maybeWriter = w - } - - fun namespace(uri: String) { - writer.setDefaultNamespace(uri) - } - - 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) - elementBuilder.f() - writer.writeEndElement() - } -} - -fun constructXml(indent: Boolean = false, f: XmlDocumentBuilder.() -> Unit): String { - val b = XmlDocumentBuilder() - val factory = XMLOutputFactory.newFactory() - factory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true) - val stream = StringWriter() - var writer = factory.createXMLStreamWriter(stream) - if (indent) { - writer = IndentingXMLStreamWriter(writer) - } - b.writer = writer - /** - * NOTE: commenting out because it wasn't obvious how to output the - * "standalone = 'yes' directive". Manual forge was therefore preferred. - */ - // writer.writeStartDocument() - f(b) - writer.writeEndDocument() - return "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n${stream.buffer.toString()}" -} - -class DestructionError(m: String) : Exception(m) - -private fun Element.getChildElements(ns: String, tag: String): List<Element> { - val elements = mutableListOf<Element>() - for (i in 0..this.childNodes.length) { - val el = this.childNodes.item(i) - if (el !is Element) { - continue - } - if (ns != "*" && el.namespaceURI != ns) { - continue - } - if (tag != "*" && el.localName != tag) { - continue - } - elements.add(el) - } - return elements -} - -class XmlElementDestructor internal constructor(val focusElement: Element) { - fun <T> requireOnlyChild(f: XmlElementDestructor.(e: Element) -> T): T { - val children = focusElement.getChildElements("*", "*") - if (children.size != 1) throw DestructionError("expected singleton child tag") - val destr = XmlElementDestructor(children[0]) - return f(destr, children[0]) - } - - fun <T> mapEachChildNamed(s: String, f: XmlElementDestructor.() -> T): List<T> { - val res = mutableListOf<T>() - val els = focusElement.getChildElements("*", s) - for (child in els) { - val destr = XmlElementDestructor(child) - res.add(f(destr)) - } - return res - } - - fun <T> requireUniqueChildNamed(s: String, f: XmlElementDestructor.() -> T): T { - val cl = focusElement.getChildElements("*", s) - if (cl.size != 1) { - throw DestructionError("expected exactly one unique $s child, got ${cl.size} instead at ${focusElement}") - } - val el = cl[0] - val destr = XmlElementDestructor(el) - return f(destr) - } - - fun <T> maybeUniqueChildNamed(s: String, f: XmlElementDestructor.() -> T): T? { - val cl = focusElement.getChildElements("*", s) - if (cl.size > 1) { - throw DestructionError("expected at most one unique $s child, got ${cl.size} instead") - } - if (cl.size == 1) { - val el = cl[0] - val destr = XmlElementDestructor(el) - return f(destr) - } - return null - } -} - -class XmlDocumentDestructor internal constructor(val d: Document) { - fun <T> requireRootElement(name: String, f: XmlElementDestructor.() -> T): T { - if (this.d.documentElement.tagName != name) { - throw DestructionError("expected '$name' tag") - } - val destr = XmlElementDestructor(d.documentElement) - return f(destr) - } -} - -fun <T> destructXml(d: Document, f: XmlDocumentDestructor.() -> T): T { - return f(XmlDocumentDestructor(d)) -} diff --git a/util/src/main/kotlin/ebics_h004/EbicsKeyManagementResponse.kt b/util/src/main/kotlin/ebics_h004/EbicsKeyManagementResponse.kt @@ -1,102 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.NormalizedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter - - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "body"]) -@XmlRootElement(name = "ebicsKeyManagementResponse") -class EbicsKeyManagementResponse { - @get:XmlElement(required = true) - lateinit var header: Header - - @get:XmlElement(required = true) - lateinit var body: Body - - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["_static", "mutable"]) - class Header { - @get:XmlElement(name = "static", required = true) - lateinit var _static: EmptyStaticHeader - - @get:XmlElement(required = true) - lateinit var mutable: MutableHeaderType - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderID", "returnCode", "reportText"]) - class MutableHeaderType { - @get:XmlElement(name = "OrderID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - var orderID: String? = null - - @get:XmlElement(name = "ReturnCode", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var returnCode: String - - @get:XmlElement(name = "ReportText", required = true) - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - @get:XmlSchemaType(name = "normalizedString") - lateinit var reportText: String - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "") - class EmptyStaticHeader - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) - class Body { - @get:XmlElement(name = "DataTransfer") - var dataTransfer: DataTransfer? = null - - @get:XmlElement(name = "ReturnCode", required = true) - lateinit var returnCode: ReturnCode - - @get:XmlElement(name = "TimestampBankParameter") - var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - class ReturnCode { - @get:XmlValue - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var value: String - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dataEncryptionInfo", "orderData"]) - class DataTransfer { - @get:XmlElement(name = "DataEncryptionInfo") - var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null - - @get:XmlElement(name = "OrderData", required = true) - lateinit var orderData: OrderData - } - - @XmlAccessorType(XmlAccessType.NONE) - class OrderData { - @get:XmlValue - lateinit var value: String - } -} diff --git a/util/src/main/kotlin/ebics_h004/EbicsNpkdRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsNpkdRequest.kt @@ -1,135 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import org.apache.xml.security.binding.xmldsig.SignatureType -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.XMLGregorianCalendar - - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) -@XmlRootElement(name = "ebicsNoPubKeyDigestsRequest") -class EbicsNpkdRequest { - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @get:XmlElement(name = "header", required = true) - lateinit var header: Header - - @get:XmlElement(name = "AuthSignature", required = true) - lateinit var authSignature: SignatureType - - @get:XmlElement(required = true) - lateinit var body: EmptyBody - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["static", "mutable"]) - class Header { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlElement(name = "static", required = true) - lateinit var static: StaticHeaderType - - @get:XmlElement(required = true) - lateinit var mutable: EmptyMutableHeader - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "StaticHeader", - propOrder = ["hostID", "nonce", "timestamp", "partnerID", "userID", "systemID", "product", "orderDetails", "securityMedium"] - ) - class StaticHeaderType { - @get:XmlElement(name = "HostID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var hostID: String - - @get:XmlElement(name = "Nonce", type = String::class) - @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) - @get:XmlSchemaType(name = "hexBinary") - lateinit var nonce: ByteArray - - @get:XmlElement(name = "Timestamp") - @get:XmlSchemaType(name = "dateTime") - var timestamp: XMLGregorianCalendar? = null - - @get:XmlElement(name = "PartnerID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var partnerID: String - - @get:XmlElement(name = "UserID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var userID: String - - @get:XmlElement(name = "SystemID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var systemID: String? = null - - @get:XmlElement(name = "Product") - val product: EbicsTypes.Product? = null - - @get:XmlElement(name = "OrderDetails", required = true) - lateinit var orderDetails: OrderDetails - - @get:XmlElement(name = "SecurityMedium", required = true) - lateinit var securityMedium: String - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderType", "orderAttribute"]) - class OrderDetails { - @get:XmlElement(name = "OrderType", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var orderType: String - - @get:XmlElement(name = "OrderAttribute", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var orderAttribute: String - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "") - class EmptyMutableHeader - - @XmlAccessorType(XmlAccessType.NONE) - class EmptyBody - - companion object { - fun createRequest( - hostId: String, - partnerId: String, - userId: String, - aNonce: ByteArray, - date: XMLGregorianCalendar - ): EbicsNpkdRequest { - return EbicsNpkdRequest().apply { - version = "H004" - revision = 1 - header = Header().apply { - authenticate = true - mutable = EmptyMutableHeader() - static = StaticHeaderType().apply { - hostID = hostId - partnerID = partnerId - userID = userId - securityMedium = "0000" - orderDetails = OrderDetails() - orderDetails.orderType = "HPB" - orderDetails.orderAttribute = "DZHNN" - nonce = aNonce - timestamp = date - } - } - body = EmptyBody() - authSignature = SignatureType() - } - } - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h004/EbicsRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -1,505 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import org.apache.xml.security.binding.xmldsig.SignatureType -import tech.libeufin.util.CryptoUtil -import java.math.BigInteger -import java.security.interfaces.RSAPublicKey -import java.util.* -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.XMLGregorianCalendar - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) -@XmlRootElement(name = "ebicsRequest") -class EbicsRequest { - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @get:XmlElement(name = "header", required = true) - lateinit var header: Header - - @get:XmlElement(name = "AuthSignature", required = true) - lateinit var authSignature: SignatureType - - @get:XmlElement(name = "body") - lateinit var body: Body - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["static", "mutable"]) - class Header { - @get:XmlElement(name = "static", required = true) - lateinit var static: StaticHeaderType - - @get:XmlElement(required = true) - lateinit var mutable: MutableHeader - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = [ - "hostID", "nonce", "timestamp", "partnerID", "userID", "systemID", - "product", "orderDetails", "bankPubKeyDigests", "securityMedium", - "numSegments", "transactionID" - ] - ) - class StaticHeaderType { - @get:XmlElement(name = "HostID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var hostID: String - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "Nonce", type = String::class) - @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) - @get:XmlSchemaType(name = "hexBinary") - var nonce: ByteArray? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "Timestamp") - @get:XmlSchemaType(name = "dateTime") - var timestamp: XMLGregorianCalendar? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "PartnerID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var partnerID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "UserID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var userID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "SystemID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var systemID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "Product") - var product: EbicsTypes.Product? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "OrderDetails") - var orderDetails: OrderDetails? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "BankPubKeyDigests") - var bankPubKeyDigests: BankPubKeyDigests? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "SecurityMedium") - var securityMedium: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "NumSegments") - var numSegments: BigInteger? = null - - /** - * Present only in the transaction / finalization phase. - */ - @get:XmlElement(name = "TransactionID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var transactionID: String? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["transactionPhase", "segmentNumber"]) - class MutableHeader { - @get:XmlElement(name = "TransactionPhase", required = true) - @get:XmlSchemaType(name = "token") - lateinit var transactionPhase: EbicsTypes.TransactionPhaseType - - /** - * Number of the currently transmitted segment, if this message - * contains order data. - */ - @get:XmlElement(name = "SegmentNumber") - var segmentNumber: EbicsTypes.SegmentNumber? = null - - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["orderType", "orderID", "orderAttribute", "orderParams"] - ) - class OrderDetails { - @get:XmlElement(name = "OrderType", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var orderType: String - - /** - * Only present if this ebicsRequest is an upload order - * relating to an already existing order. - */ - @get:XmlElement(name = "OrderID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var orderID: String? = null - - @get:XmlElement(name = "OrderAttribute", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var orderAttribute: String - - /** - * Present only in the initialization phase. - */ - @get:XmlElements( - XmlElement( - name = "StandardOrderParams", - type = StandardOrderParams::class - ), - XmlElement( - name = "GenericOrderParams", - type = GenericOrderParams::class - ) - ) - var orderParams: OrderParams? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["preValidation", "dataTransfer", "transferReceipt"]) - class Body { - @get:XmlElement(name = "PreValidation") - var preValidation: PreValidation? = null - - @get:XmlElement(name = "DataTransfer") - var dataTransfer: DataTransfer? = null - - @get:XmlElement(name = "TransferReceipt") - var transferReceipt: TransferReceipt? = null - } - - /** - * FIXME: not implemented yet - */ - @XmlAccessorType(XmlAccessType.NONE) - class PreValidation { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - class SignatureData { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlValue - var value: ByteArray? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "orderData", "hostId"]) - class DataTransfer { - - @get:XmlElement(name = "DataEncryptionInfo") - var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null - - @get:XmlElement(name = "SignatureData") - var signatureData: SignatureData? = null - - @get:XmlElement(name = "OrderData") - var orderData: String? = null - - @get:XmlElement(name = "HostID") - var hostId: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["receiptCode"]) - class TransferReceipt { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlElement(name = "ReceiptCode") - var receiptCode: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - abstract class OrderParams - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dateRange"]) - class StandardOrderParams : OrderParams() { - @get:XmlElement(name = "DateRange") - var dateRange: DateRange? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["parameterList"]) - class GenericOrderParams : OrderParams() { - @get:XmlElement(type = EbicsTypes.Parameter::class) - var parameterList: List<EbicsTypes.Parameter> = LinkedList() - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["start", "end"]) - class DateRange { - @get:XmlElement(name = "Start") - @get:XmlSchemaType(name = "date") - lateinit var start: XMLGregorianCalendar - - @get:XmlElement(name = "End") - @get:XmlSchemaType(name = "date") - lateinit var end: XMLGregorianCalendar - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["authentication", "encryption"]) - class BankPubKeyDigests { - @get:XmlElement(name = "Authentication") - lateinit var authentication: EbicsTypes.PubKeyDigest - - @get:XmlElement(name = "Encryption") - lateinit var encryption: EbicsTypes.PubKeyDigest - } - - companion object { - - fun createForDownloadReceiptPhase( - transactionId: String?, - hostId: String - - ): EbicsRequest { - return EbicsRequest().apply { - header = Header().apply { - version = "H004" - revision = 1 - authenticate = true - static = StaticHeaderType().apply { - hostID = hostId - transactionID = transactionId - } - mutable = MutableHeader().apply { - transactionPhase = EbicsTypes.TransactionPhaseType.RECEIPT - } - } - authSignature = SignatureType() - - body = Body().apply { - transferReceipt = TransferReceipt().apply { - authenticate = true - receiptCode = 0 // always true at this point. - } - } - } - } - - fun createForDownloadInitializationPhase( - userId: String, - partnerId: String, - hostId: String, - nonceArg: ByteArray, - date: XMLGregorianCalendar, - bankEncPub: RSAPublicKey, - bankAuthPub: RSAPublicKey, - myOrderType: String, - myOrderParams: OrderParams - ): EbicsRequest { - return EbicsRequest().apply { - version = "H004" - revision = 1 - authSignature = SignatureType() - body = Body() - header = Header().apply { - authenticate = true - static = StaticHeaderType().apply { - userID = userId - partnerID = partnerId - hostID = hostId - nonce = nonceArg - timestamp = date - partnerID = partnerId - orderDetails = OrderDetails().apply { - orderType = myOrderType - orderAttribute = "DZHNN" - orderParams = myOrderParams - } - bankPubKeyDigests = BankPubKeyDigests().apply { - authentication = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "X002" - value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) - } - encryption = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) - } - securityMedium = "0000" - } - } - mutable = MutableHeader().apply { - transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - } - } - } - } - - fun createForUploadInitializationPhase( - encryptedTransactionKey: ByteArray, - encryptedSignatureData: ByteArray, - hostId: String, - nonceArg: ByteArray, - partnerId: String, - userId: String, - date: XMLGregorianCalendar, - bankAuthPub: RSAPublicKey, - bankEncPub: RSAPublicKey, - segmentsNumber: BigInteger, - aOrderType: String, - aOrderParams: OrderParams - ): EbicsRequest { - - return EbicsRequest().apply { - header = Header().apply { - version = "H004" - revision = 1 - authenticate = true - static = StaticHeaderType().apply { - hostID = hostId - nonce = nonceArg - timestamp = date - partnerID = partnerId - userID = userId - orderDetails = OrderDetails().apply { - orderType = aOrderType - orderAttribute = "OZHNN" - orderParams = aOrderParams - } - bankPubKeyDigests = BankPubKeyDigests().apply { - authentication = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "X002" - value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) - } - encryption = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) - } - } - securityMedium = "0000" - numSegments = segmentsNumber - } - mutable = MutableHeader().apply { - transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - } - } - authSignature = SignatureType() - body = Body().apply { - dataTransfer = DataTransfer().apply { - signatureData = SignatureData().apply { - authenticate = true - value = encryptedSignatureData - } - dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { - transactionKey = encryptedTransactionKey - authenticate = true - encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) - } - } - } - } - } - } - - fun createForUploadTransferPhase( - hostId: String, - transactionId: String?, - segNumber: BigInteger, - encryptedData: String - ): EbicsRequest { - return EbicsRequest().apply { - header = Header().apply { - version = "H004" - revision = 1 - authenticate = true - static = StaticHeaderType().apply { - hostID = hostId - transactionID = transactionId - } - mutable = MutableHeader().apply { - transactionPhase = EbicsTypes.TransactionPhaseType.TRANSFER - segmentNumber = EbicsTypes.SegmentNumber().apply { - lastSegment = true - value = segNumber - } - } - } - - authSignature = SignatureType() - body = Body().apply { - dataTransfer = DataTransfer().apply { - orderData = encryptedData - } - } - } - } - - fun createForDownloadTransferPhase( - hostID: String, - transactionID: String?, - segmentNumber: Int, - numSegments: Int - ): EbicsRequest { - return EbicsRequest().apply { - version = "H004" - revision = 1 - authSignature = SignatureType() - body = Body() - header = Header().apply { - authenticate = true - static = StaticHeaderType().apply { - this.hostID = hostID - this.transactionID = transactionID - } - mutable = MutableHeader().apply { - transactionPhase = - EbicsTypes.TransactionPhaseType.TRANSFER - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.value = BigInteger.valueOf(segmentNumber.toLong()) - this.lastSegment = segmentNumber == numSegments - } - } - } - } - } - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h004/EbicsResponse.kt b/util/src/main/kotlin/ebics_h004/EbicsResponse.kt @@ -1,350 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import org.apache.xml.security.binding.xmldsig.SignatureType -import org.apache.xml.security.binding.xmldsig.SignedInfoType -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.XMLUtil -import java.math.BigInteger -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.NormalizedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import kotlin.math.min - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) -@XmlRootElement(name = "ebicsResponse") -class EbicsResponse { - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @get:XmlElement(required = true) - lateinit var header: Header - - @get:XmlElement(name = "AuthSignature", required = true) - lateinit var authSignature: SignatureType - - @get:XmlElement(required = true) - lateinit var body: Body - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["_static", "mutable"]) - class Header { - @get:XmlElement(name = "static", required = true) - lateinit var _static: StaticHeaderType - - @get:XmlElement(required = true) - lateinit var mutable: MutableHeaderType - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) - class Body { - @get:XmlElement(name = "DataTransfer") - var dataTransfer: DataTransferResponseType? = null - - @get:XmlElement(name = "ReturnCode", required = true) - lateinit var returnCode: ReturnCode - - @get:XmlElement(name = "TimestampBankParameter") - var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["transactionPhase", "segmentNumber", "orderID", "returnCode", "reportText"] - ) - class MutableHeaderType { - @get:XmlElement(name = "TransactionPhase", required = true) - @get:XmlSchemaType(name = "token") - lateinit var transactionPhase: EbicsTypes.TransactionPhaseType - - @get:XmlElement(name = "SegmentNumber") - var segmentNumber: EbicsTypes.SegmentNumber? = null - - @get:XmlElement(name = "OrderID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - var orderID: String? = null - - @get:XmlElement(name = "ReturnCode", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var returnCode: String - - @get:XmlElement(name = "ReportText", required = true) - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - @get:XmlSchemaType(name = "normalizedString") - lateinit var reportText: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class OrderData { - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class ReturnCode { - @get:XmlValue - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var value: String - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"]) - class DataTransferResponseType { - @get:XmlElement(name = "DataEncryptionInfo") - var dataEncryptionInfo: EbicsTypes.DataEncryptionInfo? = null - - @get:XmlElement(name = "OrderData", required = true) - lateinit var orderData: OrderData - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) - class StaticHeaderType { - @get:XmlElement(name = "TransactionID") - var transactionID: String? = null - - @get:XmlElement(name = "NumSegments") - @get:XmlSchemaType(name = "positiveInteger") - var numSegments: BigInteger? = null - } - - companion object { - - fun createForUploadWithError( - errorText: String, errorCode: String, phase: EbicsTypes.TransactionPhaseType - ): EbicsResponse { - val resp = EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = EbicsResponse.Header().apply { - this.authenticate = true - this.mutable = EbicsResponse.MutableHeaderType().apply { - this.reportText = errorText - this.returnCode = errorCode - this.transactionPhase = phase - } - _static = EbicsResponse.StaticHeaderType() - } - this.authSignature = SignatureType() - this.body = EbicsResponse.Body().apply { - this.returnCode = EbicsResponse.ReturnCode().apply { - this.authenticate = true - this.value = errorCode - } - } - } - return resp - } - - fun createForUploadInitializationPhase(transactionID: String, orderID: String): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - this.orderID = orderID - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } - } - - fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = 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 = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } - } - - fun createForUploadTransferPhase( - transactionID: String, - segmentNumber: Int, - lastSegment: Boolean, - orderID: String - ): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.TRANSFER - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.value = BigInteger.valueOf(segmentNumber.toLong()) - if (lastSegment) { - this.lastSegment = true - } - } - this.orderID = orderID - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } - } - - /** - * @param requestedSegment requested segment as a 1-based index - */ - fun createForDownloadTransferPhase( - transactionID: String, - numSegments: Int, - segmentSize: Int, - encodedData: String, - requestedSegment: Int - ): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - this.numSegments = BigInteger.valueOf(numSegments.toLong()) - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.TRANSFER - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.lastSegment = numSegments == requestedSegment - this.value = BigInteger.valueOf(requestedSegment.toLong()) - } - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - this.dataTransfer = DataTransferResponseType().apply { - this.orderData = OrderData().apply { - val start = segmentSize * (requestedSegment - 1) - this.value = encodedData.substring(start, min(start + segmentSize, encodedData.length)) - } - } - } - } - } - - fun createForDownloadInitializationPhase( - transactionID: String, - numSegments: Int, - segmentSize: Int, - enc: CryptoUtil.EncryptionResult, - encodedData: String - ): EbicsResponse { - return EbicsResponse().apply { - this.version = "H004" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - this.numSegments = BigInteger.valueOf(numSegments.toLong()) - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.lastSegment = (numSegments == 1) - this.value = BigInteger.valueOf(1) - } - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - this.dataTransfer = 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 = OrderData().apply { - this.value = encodedData.substring(0, min(segmentSize, encodedData.length)) - } - } - } - } - } - } -} diff --git a/util/src/main/kotlin/ebics_h004/EbicsTypes.kt b/util/src/main/kotlin/ebics_h004/EbicsTypes.kt @@ -1,402 +0,0 @@ -/* - * 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.util.ebics_h004 - -import org.apache.xml.security.binding.xmldsig.RSAKeyValueType -import org.w3c.dom.Element -import java.math.BigInteger -import java.util.* -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.NormalizedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import javax.xml.datatype.XMLGregorianCalendar - - -/** - * EBICS type definitions that are shared between other requests / responses / order types. - */ -object EbicsTypes { - /** - * EBICS client product. Identifies the software that accesses the EBICS host. - */ - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "Product", propOrder = ["value"]) - class Product { - @get:XmlValue - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - lateinit var value: String - - @get:XmlAttribute(name = "Language", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var language: String - - @get:XmlAttribute(name = "InstituteID") - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - var instituteID: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["value"]) - class SegmentNumber { - @XmlValue - lateinit var value: BigInteger - - @XmlAttribute(name = "lastSegment") - var lastSegment: Boolean? = null - } - - - @XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"]) - @XmlAccessorType(XmlAccessType.NONE) - class DataEncryptionInfo { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlElement(name = "EncryptionPubKeyDigest", required = true) - lateinit var encryptionPubKeyDigest: PubKeyDigest - - @get:XmlElement(name = "TransactionKey", required = true) - lateinit var transactionKey: ByteArray - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["value"]) - class PubKeyDigest { - /** - * Version of the *digest* of the public key. - */ - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @XmlAttribute(name = "Algorithm", required = true) - @XmlSchemaType(name = "anyURI") - lateinit var algorithm: String - - @get:XmlValue - lateinit var value: ByteArray - } - - @Suppress("UNUSED_PARAMETER") - enum class TransactionPhaseType(value: String) { - @XmlEnumValue("Initialisation") - INITIALISATION("Initialisation"), - - /** - * Auftragsdatentransfer - * - */ - @XmlEnumValue("Transfer") - TRANSFER("Transfer"), - - /** - * Quittungstransfer - * - */ - @XmlEnumValue("Receipt") - RECEIPT("Receipt"); - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "") - class TimestampBankParameter { - @get:XmlValue - lateinit var value: XMLGregorianCalendar - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - - - @XmlType( - name = "PubKeyValueType", propOrder = [ - "rsaKeyValue", - "timeStamp" - ] - ) - @XmlAccessorType(XmlAccessType.NONE) - class PubKeyValueType { - @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) - lateinit var rsaKeyValue: RSAKeyValueType - - @get:XmlElement(name = "TimeStamp", required = false) - @get:XmlSchemaType(name = "dateTime") - var timeStamp: XMLGregorianCalendar? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "AuthenticationPubKeyInfoType", propOrder = [ - "x509Data", - "pubKeyValue", - "authenticationVersion" - ] - ) - class AuthenticationPubKeyInfoType { - @get:XmlAnyElement() - var x509Data: Element? = null - - @get:XmlElement(name = "PubKeyValue", required = true) - lateinit var pubKeyValue: PubKeyValueType - - @get:XmlElement(name = "AuthenticationVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var authenticationVersion: String - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "EncryptionPubKeyInfoType", propOrder = [ - "x509Data", - "pubKeyValue", - "encryptionVersion" - ] - ) - class EncryptionPubKeyInfoType { - @get:XmlAnyElement() - var x509Data: Element? = null - - @get:XmlElement(name = "PubKeyValue", required = true) - lateinit var pubKeyValue: PubKeyValueType - - @get:XmlElement(name = "EncryptionVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var encryptionVersion: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class FileFormatType { - @get:XmlAttribute(name = "CountryCode") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var language: String - - @get:XmlValue - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - lateinit var value: String - } - - /** - * Generic key-value pair. - */ - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["name", "value"]) - class Parameter { - @get:XmlAttribute(name = "Type", required = true) - lateinit var type: String - - @get:XmlElement(name = "Name", required = true) - lateinit var name: String - - @get:XmlElement(name = "Value", required = true) - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["addressInfo", "bankInfo", "accountInfoList", "orderInfoList"]) - class PartnerInfo { - @get:XmlElement(name = "AddressInfo", required = true) - lateinit var addressInfo: AddressInfo - - @get:XmlElement(name = "BankInfo", required = true) - lateinit var bankInfo: BankInfo - - @get:XmlElement(name = "AccountInfo", type = AccountInfo::class) - var accountInfoList: List<AccountInfo>? = LinkedList<AccountInfo>() - - @get:XmlElement(name = "OrderInfo", type = AuthOrderInfoType::class) - var orderInfoList: List<AuthOrderInfoType> = LinkedList<AuthOrderInfoType>() - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["orderType", "fileFormat", "transferType", "orderFormat", "description", "numSigRequired"] - ) - class AuthOrderInfoType { - @get:XmlElement(name = "OrderType") - lateinit var orderType: String - - @get:XmlElement(name = "FileFormat") - val fileFormat: FileFormatType? = null - - @get:XmlElement(name = "TransferType") - lateinit var transferType: String - - @get:XmlElement(name = "OrderFormat", required = false) - var orderFormat: String? = null - - @get:XmlElement(name = "Description") - lateinit var description: String - - @get:XmlElement(name = "NumSigRequired") - var numSigRequired: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - class UserIDType { - @get:XmlValue - lateinit var value: String; - - @get:XmlAttribute(name = "Status") - var status: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["userID", "name", "permissionList"]) - class UserInfo { - @get:XmlElement(name = "UserID", required = true) - lateinit var userID: UserIDType - - @get:XmlElement(name = "Name") - var name: String? = null - - @get:XmlElement(name = "Permission", type = UserPermission::class) - var permissionList: List<UserPermission>? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderTypes", "fileFormat", "accountID", "maxAmount"]) - class UserPermission { - @get:XmlAttribute(name = "AuthorizationLevel") - var authorizationLevel: String? = null - - @get:XmlElement(name = "OrderTypes") - var orderTypes: String? = null - - @get:XmlElement(name = "FileFormat") - val fileFormat: FileFormatType? = null - - @get:XmlElement(name = "AccountID") - val accountID: String? = null - - @get:XmlElement(name = "MaxAmount") - val maxAmount: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["name", "street", "postCode", "city", "region", "country"]) - class AddressInfo { - @get:XmlElement(name = "Name") - var name: String? = null - - @get:XmlElement(name = "Street") - var street: String? = null - - @get:XmlElement(name = "PostCode") - var postCode: String? = null - - @get:XmlElement(name = "City") - var city: String? = null - - @get:XmlElement(name = "Region") - var region: String? = null - - @get:XmlElement(name = "Country") - var country: String? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - class BankInfo { - @get:XmlElement(name = "HostID") - lateinit var hostID: String - - @get:XmlElement(type = Parameter::class) - var parameters: List<Parameter>? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["accountNumberList", "bankCodeList", "accountHolder"]) - class AccountInfo { - @get:XmlAttribute(name = "Currency") - var currency: String? = null - - @get:XmlAttribute(name = "ID") - lateinit var id: String - - @get:XmlAttribute(name = "Description") - var description: String? = null - - @get:XmlElements( - XmlElement(name = "AccountNumber", type = GeneralAccountNumber::class), - XmlElement(name = "NationalAccountNumber", type = NationalAccountNumber::class) - ) - var accountNumberList: List<AbstractAccountNumber>? = LinkedList<AbstractAccountNumber>() - - @get:XmlElements( - XmlElement(name = "BankCode", type = GeneralBankCode::class), - XmlElement(name = "NationalBankCode", type = NationalBankCode::class) - ) - var bankCodeList: List<AbstractBankCode>? = LinkedList<AbstractBankCode>() - - @get:XmlElement(name = "AccountHolder") - var accountHolder: String? = null - } - - interface AbstractAccountNumber - - @XmlAccessorType(XmlAccessType.NONE) - class GeneralAccountNumber : AbstractAccountNumber { - @get:XmlAttribute(name = "international") - var international: Boolean = true - - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class NationalAccountNumber : AbstractAccountNumber { - @get:XmlAttribute(name = "format") - lateinit var format: String - - @get:XmlValue - lateinit var value: String - } - - interface AbstractBankCode - - @XmlAccessorType(XmlAccessType.NONE) - class GeneralBankCode : AbstractBankCode { - @get:XmlAttribute(name = "prefix") - var prefix: String? = null - - @get:XmlAttribute(name = "international") - var international: Boolean = true - - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class NationalBankCode : AbstractBankCode { - @get:XmlValue - lateinit var value: String - - @get:XmlAttribute(name = "format") - lateinit var format: String - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h004/EbicsUnsecuredRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsUnsecuredRequest.kt @@ -1,223 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import org.apache.xml.security.binding.xmldsig.RSAKeyValueType -import tech.libeufin.util.EbicsOrderUtil -import tech.libeufin.util.ebics_s001.SignatureTypes -import java.security.interfaces.RSAPrivateCrtKey -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "body"]) -@XmlRootElement(name = "ebicsUnsecuredRequest") -class EbicsUnsecuredRequest { - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @get:XmlElement(name = "header", required = true) - lateinit var header: Header - - @get:XmlElement(required = true) - lateinit var body: Body - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["static", "mutable"]) - class Header { - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "") - class EmptyMutableHeader - - @get:XmlElement(name = "static", required = true) - lateinit var static: StaticHeaderType - - @get:XmlElement(required = true) - lateinit var mutable: EmptyMutableHeader - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dataTransfer"]) - class Body { - @get:XmlElement(name = "DataTransfer", required = true) - lateinit var dataTransfer: UnsecuredDataTransfer - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderData"]) - class UnsecuredDataTransfer { - @get:XmlElement(name = "OrderData", required = true) - lateinit var orderData: OrderData - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "") - class OrderData { - @get:XmlValue - lateinit var value: ByteArray - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["hostID", "partnerID", "userID", "systemID", "product", "orderDetails", "securityMedium"] - ) - class StaticHeaderType { - @get:XmlElement(name = "HostID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var hostID: String - - @get:XmlElement(name = "PartnerID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var partnerID: String - - @get:XmlElement(name = "UserID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var userID: String - - @get:XmlElement(name = "SystemID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var systemID: String? = null - - @get:XmlElement(name = "Product") - val product: EbicsTypes.Product? = null - - @get:XmlElement(name = "OrderDetails", required = true) - lateinit var orderDetails: OrderDetails - - @get:XmlElement(name = "SecurityMedium", required = true) - lateinit var securityMedium: String - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderType", "orderAttribute"]) - class OrderDetails { - @get:XmlElement(name = "OrderType", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var orderType: String - - @get:XmlElement(name = "OrderAttribute", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var orderAttribute: String - } - - companion object { - - fun createHia( - hostId: String, - userId: String, - partnerId: String, - authKey: RSAPrivateCrtKey, - encKey: RSAPrivateCrtKey - - ): EbicsUnsecuredRequest { - - return EbicsUnsecuredRequest().apply { - - version = "H004" - revision = 1 - header = Header().apply { - authenticate = true - static = StaticHeaderType().apply { - orderDetails = OrderDetails().apply { - orderAttribute = "DZNNN" - orderType = "HIA" - securityMedium = "0000" - hostID = hostId - userID = userId - partnerID = partnerId - } - } - mutable = Header.EmptyMutableHeader() - } - body = Body().apply { - dataTransfer = UnsecuredDataTransfer().apply { - orderData = OrderData().apply { - value = EbicsOrderUtil.encodeOrderDataXml( - HIARequestOrderData().apply { - authenticationPubKeyInfo = EbicsTypes.AuthenticationPubKeyInfoType() - .apply { - pubKeyValue = EbicsTypes.PubKeyValueType().apply { - rsaKeyValue = RSAKeyValueType().apply { - exponent = authKey.publicExponent.toByteArray() - modulus = authKey.modulus.toByteArray() - } - } - authenticationVersion = "X002" - } - encryptionPubKeyInfo = EbicsTypes.EncryptionPubKeyInfoType() - .apply { - pubKeyValue = EbicsTypes.PubKeyValueType().apply { - rsaKeyValue = RSAKeyValueType().apply { - exponent = encKey.publicExponent.toByteArray() - modulus = encKey.modulus.toByteArray() - } - } - encryptionVersion = "E002" - - } - partnerID = partnerId - userID = userId - } - ) - } - } - } - } - } - - fun createIni( - hostId: String, - userId: String, - partnerId: String, - signKey: RSAPrivateCrtKey - - ): EbicsUnsecuredRequest { - return EbicsUnsecuredRequest().apply { - version = "H004" - revision = 1 - header = Header().apply { - authenticate = true - static = StaticHeaderType().apply { - orderDetails = OrderDetails().apply { - orderAttribute = "DZNNN" - orderType = "INI" - securityMedium = "0000" - hostID = hostId - userID = userId - partnerID = partnerId - } - } - mutable = Header.EmptyMutableHeader() - } - body = Body().apply { - dataTransfer = UnsecuredDataTransfer().apply { - orderData = OrderData().apply { - value = EbicsOrderUtil.encodeOrderDataXml( - SignatureTypes.SignaturePubKeyOrderData().apply { - signaturePubKeyInfo = SignatureTypes.SignaturePubKeyInfoType().apply { - signatureVersion = "A006" - pubKeyValue = SignatureTypes.PubKeyValueType().apply { - rsaKeyValue = org.apache.xml.security.binding.xmldsig.RSAKeyValueType().apply { - exponent = signKey.publicExponent.toByteArray() - modulus = signKey.modulus.toByteArray() - } - } - } - userID = userId - partnerID = partnerId - } - ) - } - } - } - } - } - } -} diff --git a/util/src/main/kotlin/ebics_h004/HIARequestOrderData.kt b/util/src/main/kotlin/ebics_h004/HIARequestOrderData.kt @@ -1,33 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter - - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType( - name = "HIARequestOrderDataType", - propOrder = ["authenticationPubKeyInfo", "encryptionPubKeyInfo", "partnerID", "userID", "any"] -) -@XmlRootElement(name = "HIARequestOrderData") -class HIARequestOrderData { - @get:XmlElement(name = "AuthenticationPubKeyInfo", required = true) - lateinit var authenticationPubKeyInfo: EbicsTypes.AuthenticationPubKeyInfoType - - @get:XmlElement(name = "EncryptionPubKeyInfo", required = true) - lateinit var encryptionPubKeyInfo: EbicsTypes.EncryptionPubKeyInfoType - - @get:XmlElement(name = "PartnerID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var partnerID: String - - @get:XmlElement(name = "UserID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var userID: String - - @get:XmlAnyElement(lax = true) - var any: List<Any>? = null -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h004/HKDResponseOrderData.kt b/util/src/main/kotlin/ebics_h004/HKDResponseOrderData.kt @@ -1,15 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import java.security.Permission -import javax.xml.bind.annotation.* - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["partnerInfo", "userInfoList"]) -@XmlRootElement(name = "HTDResponseOrderData") -class HKDResponseOrderData { - @get:XmlElement(name = "PartnerInfo", required = true) - lateinit var partnerInfo: EbicsTypes.PartnerInfo - - @get:XmlElement(name = "UserInfo", type = EbicsTypes.UserInfo::class, required = true) - lateinit var userInfoList: List<EbicsTypes.UserInfo> -} diff --git a/util/src/main/kotlin/ebics_h004/HPBResponseOrderData.kt b/util/src/main/kotlin/ebics_h004/HPBResponseOrderData.kt @@ -1,21 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter - - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["authenticationPubKeyInfo", "encryptionPubKeyInfo", "hostID"]) -@XmlRootElement(name = "HPBResponseOrderData") -class HPBResponseOrderData { - @get:XmlElement(name = "AuthenticationPubKeyInfo", required = true) - lateinit var authenticationPubKeyInfo: EbicsTypes.AuthenticationPubKeyInfoType - - @get:XmlElement(name = "EncryptionPubKeyInfo", required = true) - lateinit var encryptionPubKeyInfo: EbicsTypes.EncryptionPubKeyInfoType - - @get:XmlElement(name = "HostID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var hostID: String -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h004/HTDResponseOrderData.kt b/util/src/main/kotlin/ebics_h004/HTDResponseOrderData.kt @@ -1,14 +0,0 @@ -package tech.libeufin.util.ebics_h004 - -import javax.xml.bind.annotation.* - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["partnerInfo", "userInfo"]) -@XmlRootElement(name = "HTDResponseOrderData") -class HTDResponseOrderData { - @get:XmlElement(name = "PartnerInfo", required = true) - lateinit var partnerInfo: EbicsTypes.PartnerInfo - - @get:XmlElement(name = "UserInfo", required = true) - lateinit var userInfo: EbicsTypes.UserInfo -} diff --git a/util/src/main/kotlin/ebics_h004/package-info.java b/util/src/main/kotlin/ebics_h004/package-info.java @@ -1,12 +0,0 @@ -/** - * This package-info.java file defines the default namespace for the JAXB bindings - * defined in the package. - */ - -@XmlSchema( - namespace = "urn:org:ebics:H004", - elementFormDefault = XmlNsForm.QUALIFIED -) -package tech.libeufin.util.ebics_h004; -import javax.xml.bind.annotation.XmlSchema; -import javax.xml.bind.annotation.XmlNsForm; -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Request.kt b/util/src/main/kotlin/ebics_h005/Ebics3Request.kt @@ -1,586 +0,0 @@ -package tech.libeufin.util.ebics_h005 - -import org.apache.xml.security.binding.xmldsig.SignatureType -import tech.libeufin.util.CryptoUtil -import java.math.BigInteger -import java.security.interfaces.RSAPublicKey -import java.util.* -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.XMLGregorianCalendar - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) -@XmlRootElement(name = "ebicsRequest") -class Ebics3Request { - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @get:XmlElement(name = "header", required = true) - lateinit var header: Header - - @get:XmlElement(name = "AuthSignature", required = true) - lateinit var authSignature: SignatureType - - @get:XmlElement(name = "body") - lateinit var body: Body - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["static", "mutable"]) - class Header { - @get:XmlElement(name = "static", required = true) - lateinit var static: StaticHeaderType - - @get:XmlElement(required = true) - lateinit var mutable: MutableHeader - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = [ - "hostID", "nonce", "timestamp", "partnerID", "userID", "systemID", - "product", "orderDetails", "bankPubKeyDigests", "securityMedium", - "numSegments", "transactionID" - ] - ) - class StaticHeaderType { - @get:XmlElement(name = "HostID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var hostID: String - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "Nonce", type = String::class) - @get:XmlJavaTypeAdapter(HexBinaryAdapter::class) - @get:XmlSchemaType(name = "hexBinary") - var nonce: ByteArray? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "Timestamp") - @get:XmlSchemaType(name = "dateTime") - var timestamp: XMLGregorianCalendar? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "PartnerID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var partnerID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "UserID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var userID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "SystemID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var systemID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "Product") - var product: Ebics3Types.Product? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "OrderDetails") - var orderDetails: OrderDetails? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "BankPubKeyDigests") - var bankPubKeyDigests: BankPubKeyDigests? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "SecurityMedium") - var securityMedium: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElement(name = "NumSegments") - var numSegments: BigInteger? = null - - /** - * Present only in the transaction / finalization phase. - */ - @get:XmlElement(name = "TransactionID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var transactionID: String? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["transactionPhase", "segmentNumber"]) - class MutableHeader { - @get:XmlElement(name = "TransactionPhase", required = true) - @get:XmlSchemaType(name = "token") - lateinit var transactionPhase: Ebics3Types.TransactionPhaseType - - /** - * Number of the currently transmitted segment, if this message - * contains order data. - */ - @get:XmlElement(name = "SegmentNumber") - var segmentNumber: Ebics3Types.SegmentNumber? = null - - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["adminOrderType", "btdOrderParams", "btuOrderParams", "orderID", "orderParams"] - ) - class OrderDetails { - @get:XmlElement(name = "AdminOrderType", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var adminOrderType: String - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["serviceName", "scope", "serviceOption", "container", "messageName"]) - class Service { - @get:XmlElement(name = "ServiceName", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var serviceName: String - - @get:XmlElement(name = "Scope", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var scope: String - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["value"]) - class MessageName { - @XmlValue - lateinit var value: String - - @XmlAttribute(name = "version") - var version: String? = null - } - - @get:XmlElement(name = "MsgName", required = true) - lateinit var messageName: MessageName - - @get:XmlElement(name = "ServiceOption", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var serviceOption: String? = null - - @XmlAccessorType(XmlAccessType.NONE) - class Container { - @XmlAttribute(name = "containerType") - lateinit var containerType: String - } - - @get:XmlElement(name = "Container", required = true) - var container: Container? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["service", "signatureFlag", "dateRange"]) - class BTUOrderParams { - @get:XmlElement(name = "Service", required = true) - lateinit var service: Service - - /** - * This element activates the ES signature scheme (disabling - * hence the EDS). It *would* admit a @requestEDS attribute, - * but its omission means false. - */ - @get:XmlElement(name = "SignatureFlag", required = true) - var signatureFlag: Boolean = true - - @get:XmlElement(name = "DateRange", required = true) - var dateRange: DateRange? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["service", "dateRange"]) - class BTDOrderParams { - @get:XmlElement(name = "Service", required = true) - lateinit var service: Service - - @get:XmlElement(name = "DateRange", required = true) - var dateRange: DateRange? = null - } - - @get:XmlElement(name = "BTUOrderParams", required = true) - var btuOrderParams: BTUOrderParams? = null - - @get:XmlElement(name = "BTDOrderParams", required = true) - var btdOrderParams: BTDOrderParams? = null - - /** - * Only present if this ebicsRequest is an upload order - * relating to an already existing order. - */ - @get:XmlElement(name = "OrderID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - var orderID: String? = null - - /** - * Present only in the initialization phase. - */ - @get:XmlElements( - XmlElement( - name = "StandardOrderParams", - type = StandardOrderParams::class // OrderParams inheritor - ), - XmlElement( - name = "GenericOrderParams", - type = GenericOrderParams::class // OrderParams inheritor - ) - ) - // Same as the 2.5 version. - var orderParams: OrderParams? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["preValidation", "dataTransfer", "transferReceipt"]) - class Body { - @get:XmlElement(name = "PreValidation") - var preValidation: PreValidation? = null - - @get:XmlElement(name = "DataTransfer") - var dataTransfer: DataTransfer? = null - - @get:XmlElement(name = "TransferReceipt") - var transferReceipt: TransferReceipt? = null - } - - /** - * FIXME: not implemented yet - */ - @XmlAccessorType(XmlAccessType.NONE) - class PreValidation { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - class SignatureData { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlValue - var value: ByteArray? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(propOrder = ["dataEncryptionInfo", "signatureData", "dataDigest", "orderData", "hostId"]) - class DataTransfer { - - @get:XmlElement(name = "DataEncryptionInfo") - var dataEncryptionInfo: Ebics3Types.DataEncryptionInfo? = null - - @get:XmlElement(name = "SignatureData") - var signatureData: SignatureData? = null - - @XmlAccessorType(XmlAccessType.NONE) - class DataDigest { - @get:XmlAttribute(name = "SignatureVersion", required = true) - var signatureVersion: String = "A006" - - @get:XmlValue - var value: ByteArray? = null - } - - @get:XmlElement(name = "DataDigest") - var dataDigest: DataDigest? = null - - @get:XmlElement(name = "OrderData") - var orderData: String? = null - - @get:XmlElement(name = "HostID") - var hostId: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["receiptCode"]) - class TransferReceipt { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlElement(name = "ReceiptCode") - var receiptCode: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - abstract class OrderParams - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dateRange"]) - class StandardOrderParams : OrderParams() { - @get:XmlElement(name = "DateRange") - var dateRange: DateRange? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["parameterList"]) - class GenericOrderParams : OrderParams() { - @get:XmlElement(type = Ebics3Types.Parameter::class) - var parameterList: List<Ebics3Types.Parameter> = LinkedList() - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["start", "end"]) - class DateRange { - @get:XmlElement(name = "Start") - @get:XmlSchemaType(name = "date") - lateinit var start: XMLGregorianCalendar - - @get:XmlElement(name = "End") - @get:XmlSchemaType(name = "date") - lateinit var end: XMLGregorianCalendar - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["authentication", "encryption"]) - class BankPubKeyDigests { - @get:XmlElement(name = "Authentication") - lateinit var authentication: Ebics3Types.PubKeyDigest - - @get:XmlElement(name = "Encryption") - lateinit var encryption: Ebics3Types.PubKeyDigest - } - - companion object { - - fun createForDownloadReceiptPhase( - transactionId: String?, - hostId: String - ): Ebics3Request { - return Ebics3Request().apply { - header = Header().apply { - version = "H005" - revision = 1 - authenticate = true - static = StaticHeaderType().apply { - hostID = hostId - transactionID = transactionId - } - mutable = MutableHeader().apply { - transactionPhase = Ebics3Types.TransactionPhaseType.RECEIPT - } - } - authSignature = SignatureType() - - body = Body().apply { - transferReceipt = TransferReceipt().apply { - authenticate = true - receiptCode = 0 // always true at this point. - } - } - } - } - - fun createForDownloadInitializationPhase( - userId: String, - partnerId: String, - hostId: String, - nonceArg: ByteArray, - date: XMLGregorianCalendar, - bankEncPub: RSAPublicKey, - bankAuthPub: RSAPublicKey, - myOrderParams: OrderDetails.BTDOrderParams - ): Ebics3Request { - return Ebics3Request().apply { - version = "H005" - revision = 1 - authSignature = SignatureType() - body = Body() - header = Header().apply { - authenticate = true - static = StaticHeaderType().apply { - userID = userId - partnerID = partnerId - hostID = hostId - nonce = nonceArg - timestamp = date - partnerID = partnerId - orderDetails = OrderDetails().apply { - this.adminOrderType = "BTD" - this.btdOrderParams = myOrderParams - } - bankPubKeyDigests = BankPubKeyDigests().apply { - authentication = Ebics3Types.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "X002" - value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) - } - encryption = Ebics3Types.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) - } - securityMedium = "0000" - } - } - mutable = MutableHeader().apply { - transactionPhase = - Ebics3Types.TransactionPhaseType.INITIALISATION - } - } - } - } - - fun createForUploadInitializationPhase( - encryptedTransactionKey: ByteArray, - encryptedSignatureData: ByteArray, - aDataDigest: ByteArray, - hostId: String, - nonceArg: ByteArray, - partnerId: String, - userId: String, - date: XMLGregorianCalendar, - bankAuthPub: RSAPublicKey, - bankEncPub: RSAPublicKey, - segmentsNumber: BigInteger, - aOrderService: OrderDetails.Service, - ): Ebics3Request { - - return Ebics3Request().apply { - header = Header().apply { - version = "H005" - revision = 1 - authenticate = true - static = StaticHeaderType().apply { - hostID = hostId - nonce = nonceArg - timestamp = date - partnerID = partnerId - userID = userId - orderDetails = OrderDetails().apply { - this.adminOrderType = "BTU" - this.btuOrderParams = OrderDetails.BTUOrderParams().apply { - service = aOrderService - } - } - bankPubKeyDigests = BankPubKeyDigests().apply { - authentication = Ebics3Types.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "X002" - value = CryptoUtil.getEbicsPublicKeyHash(bankAuthPub) - } - encryption = Ebics3Types.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) - } - } - securityMedium = "0000" - numSegments = segmentsNumber - } - mutable = MutableHeader().apply { - transactionPhase = - Ebics3Types.TransactionPhaseType.INITIALISATION - } - } - authSignature = SignatureType() - body = Body().apply { - dataTransfer = DataTransfer().apply { - signatureData = SignatureData().apply { - authenticate = true - value = encryptedSignatureData - } - dataDigest = DataTransfer.DataDigest().apply { - value = aDataDigest - } - dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply { - transactionKey = encryptedTransactionKey - authenticate = true - encryptionPubKeyDigest = Ebics3Types.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(bankEncPub) - } - } - } - } - } - } - - fun createForUploadTransferPhase( - hostId: String, - transactionId: String?, - segNumber: BigInteger, - encryptedData: String - ): Ebics3Request { - return Ebics3Request().apply { - header = Header().apply { - version = "H005" - revision = 1 - authenticate = true - static = StaticHeaderType().apply { - hostID = hostId - transactionID = transactionId - } - mutable = MutableHeader().apply { - transactionPhase = Ebics3Types.TransactionPhaseType.TRANSFER - segmentNumber = Ebics3Types.SegmentNumber().apply { - lastSegment = true - value = segNumber - } - } - } - - authSignature = SignatureType() - body = Body().apply { - dataTransfer = DataTransfer().apply { - orderData = encryptedData - } - } - } - } - - fun createForDownloadTransferPhase( - hostID: String, - transactionID: String?, - segmentNumber: Int, - numSegments: Int - ): Ebics3Request { - return Ebics3Request().apply { - version = "H005" - revision = 1 - authSignature = SignatureType() - body = Body() - header = Header().apply { - authenticate = true - static = StaticHeaderType().apply { - this.hostID = hostID - this.transactionID = transactionID - } - mutable = MutableHeader().apply { - transactionPhase = - Ebics3Types.TransactionPhaseType.TRANSFER - this.segmentNumber = Ebics3Types.SegmentNumber().apply { - this.value = BigInteger.valueOf(segmentNumber.toLong()) - this.lastSegment = segmentNumber == numSegments - } - } - } - } - } - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Response.kt b/util/src/main/kotlin/ebics_h005/Ebics3Response.kt @@ -1,349 +0,0 @@ -package tech.libeufin.util.ebics_h005 - -import org.apache.xml.security.binding.xmldsig.SignatureType -import org.apache.xml.security.binding.xmldsig.SignedInfoType -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.XMLUtil -import tech.libeufin.util.ebics_h004.EbicsTypes -import java.math.BigInteger -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.NormalizedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import kotlin.math.min - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType(name = "", propOrder = ["header", "authSignature", "body"]) -@XmlRootElement(name = "ebicsResponse") -class Ebics3Response { - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @get:XmlAttribute(name = "Revision") - var revision: Int? = null - - @get:XmlElement(required = true) - lateinit var header: Header - - @get:XmlElement(name = "AuthSignature", required = true) - lateinit var authSignature: SignatureType - - @get:XmlElement(required = true) - lateinit var body: Body - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["_static", "mutable"]) - class Header { - @get:XmlElement(name = "static", required = true) - lateinit var _static: StaticHeaderType - - @get:XmlElement(required = true) - lateinit var mutable: MutableHeaderType - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["dataTransfer", "returnCode", "timestampBankParameter"]) - class Body { - @get:XmlElement(name = "DataTransfer") - var dataTransfer: DataTransferResponseType? = null - - @get:XmlElement(name = "ReturnCode", required = true) - lateinit var returnCode: ReturnCode - - @get:XmlElement(name = "TimestampBankParameter") - var timestampBankParameter: EbicsTypes.TimestampBankParameter? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["transactionPhase", "segmentNumber", "orderID", "returnCode", "reportText"] - ) - class MutableHeaderType { - @get:XmlElement(name = "TransactionPhase", required = true) - @get:XmlSchemaType(name = "token") - lateinit var transactionPhase: EbicsTypes.TransactionPhaseType - - @get:XmlElement(name = "SegmentNumber") - var segmentNumber: EbicsTypes.SegmentNumber? = null - - @get:XmlElement(name = "OrderID") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - var orderID: String? = null - - @get:XmlElement(name = "ReturnCode", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var returnCode: String - - @get:XmlElement(name = "ReportText", required = true) - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - @get:XmlSchemaType(name = "normalizedString") - lateinit var reportText: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class OrderData { - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class ReturnCode { - @get:XmlValue - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var value: String - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "DataTransferResponseType", propOrder = ["dataEncryptionInfo", "orderData"]) - class DataTransferResponseType { - @get:XmlElement(name = "DataEncryptionInfo") - var dataEncryptionInfo: Ebics3Types.DataEncryptionInfo? = null - - @get:XmlElement(name = "OrderData", required = true) - lateinit var orderData: OrderData - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "ResponseStaticHeaderType", propOrder = ["transactionID", "numSegments"]) - class StaticHeaderType { - @get:XmlElement(name = "TransactionID") - var transactionID: String? = null - - @get:XmlElement(name = "NumSegments") - @get:XmlSchemaType(name = "positiveInteger") - var numSegments: BigInteger? = null - } - - companion object { - - fun createForUploadWithError( - errorText: String, errorCode: String, phase: EbicsTypes.TransactionPhaseType - ): Ebics3Response { - val resp = Ebics3Response().apply { - this.version = "H005" - this.revision = 1 - this.header = Ebics3Response.Header().apply { - this.authenticate = true - this.mutable = Ebics3Response.MutableHeaderType().apply { - this.reportText = errorText - this.returnCode = errorCode - this.transactionPhase = phase - } - _static = Ebics3Response.StaticHeaderType() - } - this.authSignature = SignatureType() - this.body = Ebics3Response.Body().apply { - this.returnCode = Ebics3Response.ReturnCode().apply { - this.authenticate = true - this.value = errorCode - } - } - } - return resp - } - - fun createForUploadInitializationPhase(transactionID: String, orderID: String): Ebics3Response { - return Ebics3Response().apply { - this.version = "H005" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - this.orderID = orderID - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } - } - - fun createForDownloadReceiptPhase(transactionID: String, positiveAck: Boolean): Ebics3Response { - return Ebics3Response().apply { - this.version = "H005" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = 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 = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } - } - - fun createForUploadTransferPhase( - transactionID: String, - segmentNumber: Int, - lastSegment: Boolean, - orderID: String - ): Ebics3Response { - return Ebics3Response().apply { - this.version = "H005" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.TRANSFER - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.value = BigInteger.valueOf(segmentNumber.toLong()) - if (lastSegment) { - this.lastSegment = true - } - } - this.orderID = orderID - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - } - } - } - - /** - * @param requestedSegment requested segment as a 1-based index - */ - fun createForDownloadTransferPhase( - transactionID: String, - numSegments: Int, - segmentSize: Int, - encodedData: String, - requestedSegment: Int - ): Ebics3Response { - return Ebics3Response().apply { - this.version = "H005" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - this.numSegments = BigInteger.valueOf(numSegments.toLong()) - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.TRANSFER - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.lastSegment = numSegments == requestedSegment - this.value = BigInteger.valueOf(requestedSegment.toLong()) - } - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - this.dataTransfer = DataTransferResponseType().apply { - this.orderData = OrderData().apply { - val start = segmentSize * (requestedSegment - 1) - this.value = encodedData.substring(start, min(start + segmentSize, encodedData.length)) - } - } - } - } - } - fun createForDownloadInitializationPhase( - transactionID: String, - numSegments: Int, - segmentSize: Int, - enc: CryptoUtil.EncryptionResult, - encodedData: String - ): Ebics3Response { - return Ebics3Response().apply { - this.version = "H005" - this.revision = 1 - this.header = Header().apply { - this.authenticate = true - this._static = StaticHeaderType().apply { - this.transactionID = transactionID - this.numSegments = BigInteger.valueOf(numSegments.toLong()) - } - this.mutable = MutableHeaderType().apply { - this.transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - this.segmentNumber = EbicsTypes.SegmentNumber().apply { - this.lastSegment = (numSegments == 1) - this.value = BigInteger.valueOf(1) - } - this.reportText = "[EBICS_OK] OK" - this.returnCode = "000000" - } - } - this.authSignature = SignatureType() - this.body = Body().apply { - this.returnCode = ReturnCode().apply { - this.authenticate = true - this.value = "000000" - } - this.dataTransfer = DataTransferResponseType().apply { - this.dataEncryptionInfo = Ebics3Types.DataEncryptionInfo().apply { - this.authenticate = true - this.encryptionPubKeyDigest = Ebics3Types.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 = OrderData().apply { - this.value = encodedData.substring(0, min(segmentSize, encodedData.length)) - } - } - } - } - } - } -} diff --git a/util/src/main/kotlin/ebics_h005/Ebics3Types.kt b/util/src/main/kotlin/ebics_h005/Ebics3Types.kt @@ -1,401 +0,0 @@ -/* - * 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.util.ebics_h005 - -import org.apache.xml.security.binding.xmldsig.RSAKeyValueType -import org.w3c.dom.Element -import java.math.BigInteger -import java.util.* -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.NormalizedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import javax.xml.datatype.XMLGregorianCalendar - - -/** - * EBICS type definitions that are shared between other requests / responses / order types. - */ -object Ebics3Types { - /** - * EBICS client product. Identifies the software that accesses the EBICS host. - */ - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "Product", propOrder = ["value"]) - class Product { - @get:XmlValue - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - lateinit var value: String - - @get:XmlAttribute(name = "Language", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var language: String - - @get:XmlAttribute(name = "InstituteID") - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - var instituteID: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["value"]) - class SegmentNumber { - @XmlValue - lateinit var value: BigInteger - - @XmlAttribute(name = "lastSegment") - var lastSegment: Boolean? = null - } - - @XmlType(name = "", propOrder = ["encryptionPubKeyDigest", "transactionKey"]) - @XmlAccessorType(XmlAccessType.NONE) - class DataEncryptionInfo { - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - - @get:XmlElement(name = "EncryptionPubKeyDigest", required = true) - lateinit var encryptionPubKeyDigest: PubKeyDigest - - @get:XmlElement(name = "TransactionKey", required = true) - lateinit var transactionKey: ByteArray - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["value"]) - class PubKeyDigest { - /** - * Version of the *digest* of the public key. - */ - @get:XmlAttribute(name = "Version", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var version: String - - @XmlAttribute(name = "Algorithm", required = true) - @XmlSchemaType(name = "anyURI") - lateinit var algorithm: String - - @get:XmlValue - lateinit var value: ByteArray - } - - @Suppress("UNUSED_PARAMETER") - enum class TransactionPhaseType(value: String) { - @XmlEnumValue("Initialisation") - INITIALISATION("Initialisation"), - - /** - * Auftragsdatentransfer - * - */ - @XmlEnumValue("Transfer") - TRANSFER("Transfer"), - - /** - * Quittungstransfer - * - */ - @XmlEnumValue("Receipt") - RECEIPT("Receipt"); - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "") - class TimestampBankParameter { - @get:XmlValue - lateinit var value: XMLGregorianCalendar - - @get:XmlAttribute(name = "authenticate", required = true) - var authenticate: Boolean = false - } - - - - @XmlType( - name = "PubKeyValueType", propOrder = [ - "rsaKeyValue", - "timeStamp" - ] - ) - @XmlAccessorType(XmlAccessType.NONE) - class PubKeyValueType { - @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) - lateinit var rsaKeyValue: RSAKeyValueType - - @get:XmlElement(name = "TimeStamp", required = false) - @get:XmlSchemaType(name = "dateTime") - var timeStamp: XMLGregorianCalendar? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "AuthenticationPubKeyInfoType", propOrder = [ - "x509Data", - "pubKeyValue", - "authenticationVersion" - ] - ) - class AuthenticationPubKeyInfoType { - @get:XmlAnyElement() - var x509Data: Element? = null - - @get:XmlElement(name = "PubKeyValue", required = true) - lateinit var pubKeyValue: PubKeyValueType - - @get:XmlElement(name = "AuthenticationVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var authenticationVersion: String - } - - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "EncryptionPubKeyInfoType", propOrder = [ - "x509Data", - "pubKeyValue", - "encryptionVersion" - ] - ) - class EncryptionPubKeyInfoType { - @get:XmlAnyElement() - var x509Data: Element? = null - - @get:XmlElement(name = "PubKeyValue", required = true) - lateinit var pubKeyValue: PubKeyValueType - - @get:XmlElement(name = "EncryptionVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var encryptionVersion: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class FileFormatType { - @get:XmlAttribute(name = "CountryCode") - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var language: String - - @get:XmlValue - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - lateinit var value: String - } - - /** - * Generic key-value pair. - */ - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["name", "value"]) - class Parameter { - @get:XmlAttribute(name = "Type", required = true) - lateinit var type: String - - @get:XmlElement(name = "Name", required = true) - lateinit var name: String - - @get:XmlElement(name = "Value", required = true) - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["addressInfo", "bankInfo", "accountInfoList", "orderInfoList"]) - class PartnerInfo { - @get:XmlElement(name = "AddressInfo", required = true) - lateinit var addressInfo: AddressInfo - - @get:XmlElement(name = "BankInfo", required = true) - lateinit var bankInfo: BankInfo - - @get:XmlElement(name = "AccountInfo", type = AccountInfo::class) - var accountInfoList: List<AccountInfo>? = LinkedList<AccountInfo>() - - @get:XmlElement(name = "OrderInfo", type = AuthOrderInfoType::class) - var orderInfoList: List<AuthOrderInfoType> = LinkedList<AuthOrderInfoType>() - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["orderType", "fileFormat", "transferType", "orderFormat", "description", "numSigRequired"] - ) - class AuthOrderInfoType { - @get:XmlElement(name = "OrderType") - lateinit var orderType: String - - @get:XmlElement(name = "FileFormat") - val fileFormat: FileFormatType? = null - - @get:XmlElement(name = "TransferType") - lateinit var transferType: String - - @get:XmlElement(name = "OrderFormat", required = false) - var orderFormat: String? = null - - @get:XmlElement(name = "Description") - lateinit var description: String - - @get:XmlElement(name = "NumSigRequired") - var numSigRequired: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - class UserIDType { - @get:XmlValue - lateinit var value: String; - - @get:XmlAttribute(name = "Status") - var status: Int? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["userID", "name", "permissionList"]) - class UserInfo { - @get:XmlElement(name = "UserID", required = true) - lateinit var userID: UserIDType - - @get:XmlElement(name = "Name") - var name: String? = null - - @get:XmlElement(name = "Permission", type = UserPermission::class) - var permissionList: List<UserPermission>? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["orderTypes", "fileFormat", "accountID", "maxAmount"]) - class UserPermission { - @get:XmlAttribute(name = "AuthorizationLevel") - var authorizationLevel: String? = null - - @get:XmlElement(name = "OrderTypes") - var orderTypes: String? = null - - @get:XmlElement(name = "FileFormat") - val fileFormat: FileFormatType? = null - - @get:XmlElement(name = "AccountID") - val accountID: String? = null - - @get:XmlElement(name = "MaxAmount") - val maxAmount: String? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["name", "street", "postCode", "city", "region", "country"]) - class AddressInfo { - @get:XmlElement(name = "Name") - var name: String? = null - - @get:XmlElement(name = "Street") - var street: String? = null - - @get:XmlElement(name = "PostCode") - var postCode: String? = null - - @get:XmlElement(name = "City") - var city: String? = null - - @get:XmlElement(name = "Region") - var region: String? = null - - @get:XmlElement(name = "Country") - var country: String? = null - } - - - @XmlAccessorType(XmlAccessType.NONE) - class BankInfo { - @get:XmlElement(name = "HostID") - lateinit var hostID: String - - @get:XmlElement(type = Parameter::class) - var parameters: List<Parameter>? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["accountNumberList", "bankCodeList", "accountHolder"]) - class AccountInfo { - @get:XmlAttribute(name = "Currency") - var currency: String? = null - - @get:XmlAttribute(name = "ID") - lateinit var id: String - - @get:XmlAttribute(name = "Description") - var description: String? = null - - @get:XmlElements( - XmlElement(name = "AccountNumber", type = GeneralAccountNumber::class), - XmlElement(name = "NationalAccountNumber", type = NationalAccountNumber::class) - ) - var accountNumberList: List<AbstractAccountNumber>? = LinkedList<AbstractAccountNumber>() - - @get:XmlElements( - XmlElement(name = "BankCode", type = GeneralBankCode::class), - XmlElement(name = "NationalBankCode", type = NationalBankCode::class) - ) - var bankCodeList: List<AbstractBankCode>? = LinkedList<AbstractBankCode>() - - @get:XmlElement(name = "AccountHolder") - var accountHolder: String? = null - } - - interface AbstractAccountNumber - - @XmlAccessorType(XmlAccessType.NONE) - class GeneralAccountNumber : AbstractAccountNumber { - @get:XmlAttribute(name = "international") - var international: Boolean = true - - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class NationalAccountNumber : AbstractAccountNumber { - @get:XmlAttribute(name = "format") - lateinit var format: String - - @get:XmlValue - lateinit var value: String - } - - interface AbstractBankCode - - @XmlAccessorType(XmlAccessType.NONE) - class GeneralBankCode : AbstractBankCode { - @get:XmlAttribute(name = "prefix") - var prefix: String? = null - - @get:XmlAttribute(name = "international") - var international: Boolean = true - - @get:XmlValue - lateinit var value: String - } - - @XmlAccessorType(XmlAccessType.NONE) - class NationalBankCode : AbstractBankCode { - @get:XmlValue - lateinit var value: String - - @get:XmlAttribute(name = "format") - lateinit var format: String - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_h005/package-info.java b/util/src/main/kotlin/ebics_h005/package-info.java @@ -1,13 +0,0 @@ -/** - * This package-info.java file defines the default namespace for the JAXB bindings - * defined in the package. - */ - -@XmlSchema( - namespace = "urn:org:ebics:H005", - elementFormDefault = XmlNsForm.QUALIFIED -) -package tech.libeufin.util.ebics_h005; -import javax.xml.bind.annotation.XmlNs; -import javax.xml.bind.annotation.XmlNsForm; -import javax.xml.bind.annotation.XmlSchema; -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_hev/EbicsMessages.kt b/util/src/main/kotlin/ebics_hev/EbicsMessages.kt @@ -1,92 +0,0 @@ -/* - * 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.util.ebics_hev - -import java.util.* -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.NormalizedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType( - name = "HEVRequestDataType" -) -@XmlRootElement(name = "ebicsHEVRequest") -class HEVRequest{ - @get:XmlElement(name = "HostID", required = true) - lateinit var hostId: String -} - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType( - name = "HEVResponseDataType", - propOrder = ["systemReturnCode", "versionNumber", "any"] -) -@XmlRootElement(name = "ebicsHEVResponse") -class HEVResponse { - @get:XmlElement(name = "SystemReturnCode", required = true) - lateinit var systemReturnCode: SystemReturnCodeType - - @get:XmlElement(name = "VersionNumber", namespace = "http://www.ebics.org/H000") - var versionNumber: List<VersionNumber> = LinkedList() - - @get:XmlAnyElement(lax = true) - var any: List<Any>? = null - - @XmlAccessorType(XmlAccessType.NONE) - class VersionNumber { - @get:XmlValue - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var value: String - - @get:XmlAttribute(name = "ProtocolVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var protocolVersion: String - - companion object { - fun create(protocolVersion: String, versionNumber: String): VersionNumber { - return VersionNumber().apply { - this.protocolVersion = protocolVersion - this.value = versionNumber - } - } - } - } -} - - -@XmlAccessorType(XmlAccessType.NONE) -@XmlType( - name = "SystemReturnCodeType", - propOrder = [ - "returnCode", - "reportText" - ] -) -class SystemReturnCodeType { - @get:XmlElement(name = "ReturnCode", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var returnCode: String - - @get:XmlElement(name = "ReportText", required = true) - @get:XmlJavaTypeAdapter(NormalizedStringAdapter::class) - lateinit var reportText: String -} diff --git a/util/src/main/kotlin/ebics_hev/package-info.java b/util/src/main/kotlin/ebics_hev/package-info.java @@ -1,13 +0,0 @@ -/** - * This package-info.java file defines the default namespace for the JAXB bindings - * defined in the package. - */ - -@XmlSchema( - namespace = "http://www.ebics.org/H000", - elementFormDefault = XmlNsForm.QUALIFIED -) -package tech.libeufin.util.ebics_hev; - -import javax.xml.bind.annotation.XmlNsForm; -import javax.xml.bind.annotation.XmlSchema; diff --git a/util/src/main/kotlin/ebics_s001/SignatureTypes.kt b/util/src/main/kotlin/ebics_s001/SignatureTypes.kt @@ -1,92 +0,0 @@ -/* - * 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.util.ebics_s001 - -import org.apache.xml.security.binding.xmldsig.RSAKeyValueType -import org.apache.xml.security.binding.xmldsig.X509DataType -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import javax.xml.datatype.XMLGregorianCalendar - - -object SignatureTypes { - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "PubKeyValueType", namespace = "http://www.ebics.org/S001", propOrder = [ - "rsaKeyValue", - "timeStamp" - ] - ) - class PubKeyValueType { - @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) - lateinit var rsaKeyValue: RSAKeyValueType - - @get:XmlElement(name = "TimeStamp") - @get:XmlSchemaType(name = "dateTime") - var timeStamp: XMLGregorianCalendar? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = [ - "x509Data", - "pubKeyValue", - "signatureVersion" - ] - ) - class SignaturePubKeyInfoType { - @get:XmlElement(name = "X509Data") - var x509Data: X509DataType? = null - - @get:XmlElement(name = "PubKeyValue", required = true) - lateinit var pubKeyValue: PubKeyValueType - - @get:XmlElement(name = "SignatureVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var signatureVersion: String - } - - /** - * EBICS INI payload. - */ - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["signaturePubKeyInfo", "partnerID", "userID"] - ) - @XmlRootElement(name = "SignaturePubKeyOrderData") - class SignaturePubKeyOrderData { - @get:XmlElement(name = "SignaturePubKeyInfo", required = true) - lateinit var signaturePubKeyInfo: SignaturePubKeyInfoType - - @get:XmlElement(name = "PartnerID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var partnerID: String - - @get:XmlElement(name = "UserID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var userID: String - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s001/UserSignatureData.kt b/util/src/main/kotlin/ebics_s001/UserSignatureData.kt @@ -1,27 +0,0 @@ -package tech.libeufin.util.ebics_s001 - -import javax.xml.bind.annotation.* - -@XmlAccessorType(XmlAccessType.NONE) -@XmlRootElement(name = "UserSignatureData") -@XmlType(name = "", propOrder = ["orderSignatureList"]) -class UserSignatureData { - @XmlElement(name = "OrderSignatureData", type = OrderSignatureData::class) - var orderSignatureList: List<OrderSignatureData>? = null - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["signatureVersion", "signatureValue", "partnerID", "userID"]) - class OrderSignatureData { - @XmlElement(name = "SignatureVersion") - lateinit var signatureVersion: String - - @XmlElement(name = "SignatureValue") - lateinit var signatureValue: ByteArray - - @XmlElement(name = "PartnerID") - lateinit var partnerID: String - - @XmlElement(name = "UserID") - lateinit var userID: String - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s001/package-info.java b/util/src/main/kotlin/ebics_s001/package-info.java @@ -1,13 +0,0 @@ -/** - * This package-info.java file defines the default namespace for the JAXB bindings - * defined in the package. - */ - -@XmlSchema( - namespace = "http://www.ebics.org/S001", - elementFormDefault = XmlNsForm.QUALIFIED -) -package tech.libeufin.util.ebics_s001; - -import javax.xml.bind.annotation.XmlNsForm; -import javax.xml.bind.annotation.XmlSchema; -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s002/SignatureTypes.kt b/util/src/main/kotlin/ebics_s002/SignatureTypes.kt @@ -1,91 +0,0 @@ -/* - * 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.util.ebics_s002 - -import org.apache.xml.security.binding.xmldsig.RSAKeyValueType -import org.apache.xml.security.binding.xmldsig.X509DataType -import javax.xml.bind.annotation.* -import javax.xml.bind.annotation.adapters.CollapsedStringAdapter -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter -import javax.xml.datatype.XMLGregorianCalendar - - -object SignatureTypes { - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "PubKeyValueType", namespace = "http://www.ebics.org/S002", propOrder = [ - "rsaKeyValue", - "timeStamp" - ] - ) - class PubKeyValueType { - @get:XmlElement(name = "RSAKeyValue", namespace = "http://www.w3.org/2000/09/xmldsig#", required = true) - lateinit var rsaKeyValue: RSAKeyValueType - - @get:XmlElement(name = "TimeStamp") - @get:XmlSchemaType(name = "dateTime") - var timeStamp: XMLGregorianCalendar? = null - } - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = [ - "x509Data", - "pubKeyValue", - "signatureVersion" - ] - ) - class SignaturePubKeyInfoType { - @get:XmlElement(name = "X509Data") - var x509Data: X509DataType? = null - - @get:XmlElement(name = "PubKeyValue", required = true) - lateinit var pubKeyValue: PubKeyValueType - - @get:XmlElement(name = "SignatureVersion", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - lateinit var signatureVersion: String - } - - /** - * EBICS INI payload. - */ - @XmlAccessorType(XmlAccessType.NONE) - @XmlType( - name = "", - propOrder = ["signaturePubKeyInfo", "partnerID", "userID"] - ) - @XmlRootElement(name = "SignaturePubKeyOrderData") - class SignaturePubKeyOrderData { - @get:XmlElement(name = "SignaturePubKeyInfo", required = true) - lateinit var signaturePubKeyInfo: SignaturePubKeyInfoType - - @get:XmlElement(name = "PartnerID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var partnerID: String - - @get:XmlElement(name = "UserID", required = true) - @get:XmlJavaTypeAdapter(CollapsedStringAdapter::class) - @get:XmlSchemaType(name = "token") - lateinit var userID: String - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt b/util/src/main/kotlin/ebics_s002/UserSignatureDataEbics3.kt @@ -1,27 +0,0 @@ -package tech.libeufin.util.ebics_s002 - -import javax.xml.bind.annotation.* - -@XmlAccessorType(XmlAccessType.NONE) -@XmlRootElement(name = "UserSignatureData") -@XmlType(name = "", propOrder = ["orderSignatureList"]) -class UserSignatureDataEbics3 { - @XmlElement(name = "OrderSignatureData", type = OrderSignatureData::class) - var orderSignatureList: List<OrderSignatureData>? = null - - @XmlAccessorType(XmlAccessType.NONE) - @XmlType(name = "", propOrder = ["signatureVersion", "signatureValue", "partnerID", "userID"]) - class OrderSignatureData { - @XmlElement(name = "SignatureVersion") - lateinit var signatureVersion: String - - @XmlElement(name = "SignatureValue") - lateinit var signatureValue: ByteArray - - @XmlElement(name = "PartnerID") - lateinit var partnerID: String - - @XmlElement(name = "UserID") - lateinit var userID: String - } -} -\ No newline at end of file diff --git a/util/src/main/kotlin/ebics_s002/package-info.java b/util/src/main/kotlin/ebics_s002/package-info.java @@ -1,13 +0,0 @@ -/** - * This package-info.java file defines the default namespace for the JAXB bindings - * defined in the package. - */ - -@XmlSchema( - namespace = "http://www.ebics.org/S002", - elementFormDefault = XmlNsForm.QUALIFIED -) -package tech.libeufin.util.ebics_s002; - -import javax.xml.bind.annotation.XmlNsForm; -import javax.xml.bind.annotation.XmlSchema; -\ No newline at end of file diff --git a/util/src/main/kotlin/iban.kt b/util/src/main/kotlin/iban.kt @@ -1,46 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -fun getIban(): String { - val ccNoCheck = "131400" // DE00 - val bban = (0..10).map { - (0..9).random() - }.joinToString("") // 10 digits account number - var checkDigits: String = "98".toBigInteger().minus("$bban$ccNoCheck".toBigInteger().mod("97".toBigInteger())).toString() - if (checkDigits.length == 1) { - checkDigits = "0${checkDigits}" - } - return "DE$checkDigits$bban" -} - -// Taken from the ISO20022 XSD schema -private val bicRegex = Regex("^[A-Z]{6}[A-Z2-9][A-NP-Z0-9]([A-Z0-9]{3})?$") - -fun validateBic(bic: String): Boolean { - return bicRegex.matches(bic) -} - -// Taken from the ISO20022 XSD schema -private val ibanRegex = Regex("^[A-Z]{2}[0-9]{2}[a-zA-Z0-9]{1,30}$") - -fun validateIban(iban: String): Boolean { - return ibanRegex.matches(iban) -} -\ No newline at end of file diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt @@ -1,90 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import net.taler.wallet.crypto.Base32Crockford -import java.math.BigInteger -import java.util.* - -fun ByteArray.toHexString(): String { - return this.joinToString("") { - java.lang.String.format("%02X", it) - } -} - -private fun toDigit(hexChar: Char): Int { - val digit = Character.digit(hexChar, 16) - require(digit != -1) { "Invalid Hexadecimal Character: $hexChar" } - return digit -} - -private fun hexToByte(hexString: String): Byte { - val firstDigit: Int = toDigit(hexString[0]) - val secondDigit: Int = toDigit(hexString[1]) - return ((firstDigit shl 4) + secondDigit).toByte() -} - -fun decodeHexString(hexString: String): ByteArray { - val hs = hexString.replace(" ", "").replace("\n", "") - require(hs.length % 2 != 1) { "Invalid hexadecimal String supplied." } - val bytes = ByteArray(hs.length / 2) - var i = 0 - while (i < hs.length) { - bytes[i / 2] = hexToByte(hs.substring(i, i + 2)) - i += 2 - } - return bytes -} - -fun bytesToBase64(bytes: ByteArray): String { - return Base64.getEncoder().encodeToString(bytes) -} - -fun base64ToBytes(encoding: String): ByteArray { - return Base64.getDecoder().decode(encoding) -} - -// used mostly in RSA math, never as amount. -fun BigInteger.toUnsignedHexString(): String { - val signedValue = this.toByteArray() - require(this.signum() > 0) { "number must be positive" } - val start = if (signedValue[0] == 0.toByte()) { - 1 - } else { - 0 - } - val bytes = Arrays.copyOfRange(signedValue, start, signedValue.size) - return bytes.toHexString() -} - -fun getQueryParam(uriQueryString: String, param: String): String? { - uriQueryString.split('&').forEach { - val kv = it.split('=') - if (kv[0] == param) - return kv[1] - } - return null -} - -fun String.splitOnce(pat: String): Pair<String, String>? { - val split = split(pat, limit=2); - if (split.size != 2) return null - return Pair(split[0], split[1]) -} -\ No newline at end of file diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt @@ -1,138 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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.util - -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import java.time.* -import java.time.format.DateTimeFormatter -import java.time.temporal.ChronoUnit - -private val logger: Logger = LoggerFactory.getLogger("libeufin-common") - -/** - * Converts the 'this' Instant to the number of nanoseconds - * since the Epoch. It returns the result as Long, or null - * if one arithmetic overflow occurred. - */ -private fun Instant.toNanos(): Long? { - val oneSecNanos = ChronoUnit.SECONDS.duration.toNanos() - val nanoBase: Long = this.epochSecond * oneSecNanos - if (nanoBase != 0L && nanoBase / this.epochSecond != oneSecNanos) { - logger.error("Multiplication overflow: could not convert Instant to nanos.") - return null - } - val res = nanoBase + this.nano - if (res < nanoBase) { - logger.error("Addition overflow: could not convert Instant to nanos.") - return null - } - return res -} - -/** - * This function converts an Instant input to the - * number of microseconds since the Epoch, except that - * it yields Long.MAX if the Input is Instant.MAX. - * - * Takes the name after the way timestamps are designed - * in the database: micros since Epoch, or Long.MAX for - * "never". - * - * Returns the Long representation of 'this' or null - * if that would overflow. - */ -fun Instant.toDbMicros(): Long? { - if (this == Instant.MAX) - return Long.MAX_VALUE - val nanos = this.toNanos() ?: run { - logger.error("Could not obtain micros to store to database, convenience conversion to nanos overflew.") - return null - } - return nanos / 1000L -} - -/** - * This helper is typically used to convert a timestamp expressed - * in microseconds from the DB back to the Web application. In case - * of _any_ error, it logs it and returns null. - */ -fun Long.microsToJavaInstant(): Instant? { - if (this == Long.MAX_VALUE) - return Instant.MAX - return try { - Instant.EPOCH.plus(this, ChronoUnit.MICROS) - } catch (e: Exception) { - logger.error(e.message) - return null - } -} - -/** - * Parses timestamps found in camt.054 documents. They have - * the following format: yyy-MM-ddThh:mm:ss, without any timezone. - * - * @param timeFromXml input time string from the XML - * @return [Instant] in the UTC timezone - */ -fun parseCamtTime(timeFromCamt: String): Instant { - val t = LocalDateTime.parse(timeFromCamt) - val utc = ZoneId.of("UTC") - return t.toInstant(utc.rules.getOffset(t)) -} - -/** - * Parses a date string as found in the booking date of - * camt.054 reports. They have this format: yyyy-MM-dd. - * - * @param bookDate input to parse - * @return [Instant] to the UTC. - */ -fun parseBookDate(bookDate: String): Instant { - val l = LocalDate.parse(bookDate) - return Instant.from(l.atStartOfDay(ZoneId.of("UTC"))) -} - -/** - * Returns the minimum instant between two. - * - * @param a input [Instant] - * @param b input [Instant] - * @return the minimum [Instant] or null if even one is null. - */ -fun minTimestamp(a: Instant?, b: Instant?): Instant? { - if (a == null || b == null) return null - if (a.isBefore(b)) return a - return b // includes the case where a == b. -} - -/** - * Returns the max instant between two. - * - * @param a input [Instant] - * @param b input [Instant] - * @return the max [Instant] or null if both are null - */ -fun maxTimestamp(a: Instant?, b: Instant?): Instant? { - if (a == null) return b - if (b == null) return a - if (a.isAfter(b)) return a - return b // includes the case where a == b -} -\ No newline at end of file diff --git a/util/src/test/kotlin/AmountTest.kt b/util/src/test/kotlin/AmountTest.kt @@ -1,62 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import java.time.Instant -import java.util.* -import kotlin.test.* -import org.junit.Test -import tech.libeufin.util.* - -class AmountTest { - @Test - fun parse() { - assertEquals(TalerAmount("EUR:4"), TalerAmount(4L, 0, "EUR")) - assertEquals(TalerAmount("EUR:0.02"), TalerAmount(0L, 2000000, "EUR")) - assertEquals(TalerAmount("EUR:4.12"), TalerAmount(4L, 12000000, "EUR")) - assertEquals(TalerAmount("LOCAL:4444.1000"), TalerAmount(4444L, 10000000, "LOCAL")) - assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999"), TalerAmount(TalerAmount.MAX_VALUE, 99999999, "EUR")) - - assertException("Invalid amount format") {TalerAmount("")} - assertException("Invalid amount format") {TalerAmount("EUR")} - assertException("Invalid amount format") {TalerAmount("eur:12")} - assertException("Invalid amount format") {TalerAmount(" EUR:12")} - assertException("Invalid amount format") {TalerAmount("EUR:1.")} - assertException("Invalid amount format") {TalerAmount("EUR:.1")} - assertException("Invalid amount format") {TalerAmount("AZERTYUIOPQSD:12")} - assertException("Value specified in amount is too large") {TalerAmount("EUR:${Long.MAX_VALUE}")} - assertException("Invalid amount format") {TalerAmount("EUR:4.000000000")} - assertException("Invalid amount format") {TalerAmount("EUR:4.4a")} - } - - @Test - fun parseRoundTrip() { - for (amount in listOf("EUR:4", "EUR:0.02", "EUR:4.12")) { - assertEquals(amount, TalerAmount(amount).toString()) - } - } - - fun assertException(msg: String, lambda: () -> Unit) { - try { - lambda() - throw Exception("Expected failure") - } catch (e: Exception) { - assert(e.message!!.startsWith(msg)) { "${e.message}" } - } - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/CryptoUtilTest.kt b/util/src/test/kotlin/CryptoUtilTest.kt @@ -1,217 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import net.taler.wallet.crypto.Base32Crockford -import org.junit.Ignore -import org.junit.Test -import tech.libeufin.util.* -import java.io.File -import java.security.KeyPairGenerator -import java.security.interfaces.RSAPrivateCrtKey -import java.util.* -import javax.crypto.EncryptedPrivateKeyInfo -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class CryptoUtilTest { - - @Test - fun loadFromModulusAndExponent() { - val keyPair = CryptoUtil.generateRsaKeyPair(1024) - val pub2 = CryptoUtil.loadRsaPublicKeyFromComponents( - keyPair.public.modulus.toByteArray(), - keyPair.public.publicExponent.toByteArray() - ) - assertEquals(keyPair.public, pub2) - } - - @Test - fun keyGeneration() { - val gen: KeyPairGenerator = KeyPairGenerator.getInstance("RSA") - gen.initialize(2048) - val pair = gen.genKeyPair() - println(pair.private) - assertTrue(pair.private is RSAPrivateCrtKey) - } - - @Test - fun testCryptoUtilBasics() { - val keyPair = CryptoUtil.generateRsaKeyPair(1024) - val encodedPriv = keyPair.private.encoded - val encodedPub = keyPair.public.encoded - val otherKeyPair = - CryptoUtil.RsaCrtKeyPair(CryptoUtil.loadRsaPrivateKey(encodedPriv), CryptoUtil.loadRsaPublicKey(encodedPub)) - assertEquals(keyPair.private, otherKeyPair.private) - assertEquals(keyPair.public, otherKeyPair.public) - } - - @Test - fun testEbicsE002() { - val data = "Hello, World!".toByteArray() - val keyPair = CryptoUtil.generateRsaKeyPair(1024) - val enc = CryptoUtil.encryptEbicsE002(data, keyPair.public) - val dec = CryptoUtil.decryptEbicsE002(enc, keyPair.private) - assertTrue(data.contentEquals(dec)) - } - - @Test - fun testEbicsA006() { - val keyPair = CryptoUtil.generateRsaKeyPair(1024) - val data = "Hello, World".toByteArray(Charsets.UTF_8) - val sig = CryptoUtil.signEbicsA006(data, keyPair.private) - assertTrue(CryptoUtil.verifyEbicsA006(sig, data, keyPair.public)) - } - - @Test - fun testPassphraseEncryption() { - - val keyPair = CryptoUtil.generateRsaKeyPair(1024) - - /* encrypt with original key */ - val data = "Hello, World!".toByteArray(Charsets.UTF_8) - val secret = CryptoUtil.encryptEbicsE002(data, keyPair.public) - - /* encrypt and decrypt private key */ - val encPriv = CryptoUtil.encryptKey(keyPair.private.encoded, "secret") - val plainPriv = CryptoUtil.decryptKey(EncryptedPrivateKeyInfo(encPriv), "secret") - - /* decrypt with decrypted private key */ - val revealed = CryptoUtil.decryptEbicsE002(secret, plainPriv) - - assertEquals( - String(revealed, charset = Charsets.UTF_8), - String(data, charset = Charsets.UTF_8) - ) - } - - @Test - fun testEbicsPublicKeyHashing() { - val exponentStr = "01 00 01" - val moduloStr = """ - EB BD B8 E3 73 45 60 06 44 A1 AD 6A 25 33 65 F5 - 9C EB E5 93 E0 51 72 77 90 6B F0 58 A8 89 EB 00 - C6 0B 37 38 F3 3C 55 F2 4D 83 D0 33 C3 A8 F0 3C - 82 4E AF 78 51 D6 F4 71 6A CC 9C 10 2A 58 C9 5F - 3D 30 B4 31 D7 1B 79 6D 43 AA F9 75 B5 7E 0B 4A - 55 52 1D 7C AC 8F 92 B0 AE 9F CF 5F 16 5C 6A D1 - 88 DB E2 48 E7 78 43 F9 18 63 29 45 ED 6C 08 6C - 16 1C DE F3 02 01 23 8A 58 35 43 2B 2E C5 3F 6F - 33 B7 A3 46 E1 75 BD 98 7C 6D 55 DE 71 11 56 3D - 7A 2C 85 42 98 42 DF 94 BF E8 8B 76 84 13 3E CA - 0E 8D 12 57 D6 8A CF 82 DE B7 D7 BB BC 45 AE 25 - 95 76 00 19 08 AA D2 C8 A7 D8 10 37 88 96 B9 98 - 14 B4 B0 65 F3 36 CE 93 F7 46 12 58 9F E7 79 33 - D5 BE 0D 0E F8 E7 E0 A9 C3 10 51 A1 3E A4 4F 67 - 5E 75 8C 9D E6 FE 27 B6 3C CF 61 9B 31 D4 D0 22 - B9 2E 4C AF 5F D6 4B 1F F0 4D 06 5F 68 EB 0B 71 - """.trimIndent() - val expectedHashStr = """ - 72 71 D5 83 B4 24 A6 DA 0B 7B 22 24 3B E2 B8 8C - 6E A6 0F 9F 76 11 FD 18 BE 2C E8 8B 21 03 A9 41 - """.trimIndent() - - val expectedHash = expectedHashStr.replace(" ", "").replace("\n", "").toByteArray(Charsets.UTF_8) - - val pub = CryptoUtil.loadRsaPublicKeyFromComponents(decodeHexString(moduloStr), decodeHexString(exponentStr)) - - println("echoed pub exp: ${pub.publicExponent.toUnsignedHexString()}") - println("echoed pub mod: ${pub.modulus.toUnsignedHexString()}") - - val pubHash = CryptoUtil.getEbicsPublicKeyHash(pub) - - println("our pubHash: ${pubHash.toHexString()}") - println("expected pubHash: ${expectedHash.toString(Charsets.UTF_8)}") - - assertEquals(expectedHash.toString(Charsets.UTF_8), pubHash.toHexString()) - } - - @Test - fun checkEddsaPublicKey() { - val givenEnc = "XZH3P6NF9DSG3BH0C082X38N2RVK1RV2H24KF76028QBKDM24BCG" - val non32bytes = "N2RVK1RV2H24KF76028QBKDM24BCG" - assertTrue(CryptoUtil.checkValidEddsaPublicKey(givenEnc)) - assertFalse(CryptoUtil.checkValidEddsaPublicKey(non32bytes)) - } - - @Test - fun base32Test() { - val validKey = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" - val enc = validKey - val obj = Base32Crockford.decode(enc) - assertTrue(obj.size == 32) - val roundTrip = Base32Crockford.encode(obj) - assertEquals(enc, roundTrip) - val invalidShorterKey = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCE" - val shorterBlob = Base32Crockford.decode(invalidShorterKey) - assertTrue(shorterBlob.size < 32) // See #7980 - } - - @Test - fun blobRoundTrip() { - val blob = ByteArray(30) - Random().nextBytes(blob) - val enc = Base32Crockford.encode(blob) - val blobAgain = Base32Crockford.decode(enc) - assertTrue(blob.contentEquals(blobAgain)) - } - - /** - * Manual test: tests that gnunet-base32 and - * libeufin encode to the same string. - */ - @Ignore - fun gnunetEncodeCheck() { - val blob = ByteArray(30) - Random().nextBytes(blob) - val b = File("/tmp/libeufin-blob.bin") - b.writeBytes(blob) - val enc = Base32Crockford.encode(blob) - // The following output needs to match the one from - // "gnunet-base32 /tmp/libeufin-blob.bin" - println(enc) - } - - /** - * Manual test: tests that gnunet-base32 and - * libeufin decode to the same value - */ - @Ignore - fun gnunetDecodeCheck() { - // condition: "gnunet-base32 -d /tmp/blob.enc" needs to decode to /tmp/blob.bin - val blob = File("/tmp/blob.bin").readBytes() - val blobEnc = File("/tmp/blob.enc").readText(Charsets.UTF_8) - val dec = Base32Crockford.decode(blobEnc) - assertTrue(blob.contentEquals(dec)) - } - - @Test - fun emptyBase32Test() { - val enc = Base32Crockford.encode(ByteArray(0)) - assert(enc.isEmpty()) - val blob = Base32Crockford.decode("") - assert(blob.isEmpty()) - } - - @Test - fun passwordHashing() { - val x = CryptoUtil.hashpw("myinsecurepw") - assertTrue(CryptoUtil.checkpw("myinsecurepw", x)) - } -} diff --git a/util/src/test/kotlin/EbicsMessagesTest.kt b/util/src/test/kotlin/EbicsMessagesTest.kt @@ -1,371 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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 junit.framework.TestCase.assertEquals -import org.apache.xml.security.binding.xmldsig.SignatureType -import org.junit.Test -import org.w3c.dom.Element -import tech.libeufin.util.ebics_h004.* -import tech.libeufin.util.ebics_hev.HEVResponse -import tech.libeufin.util.ebics_hev.SystemReturnCodeType -import tech.libeufin.util.ebics_s001.SignatureTypes -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.XMLUtil -import javax.xml.datatype.DatatypeFactory -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class EbicsMessagesTest { - /** - * Tests the JAXB instantiation of non-XmlRootElement documents, - * as notably are the inner XML strings carrying keys in INI/HIA - * messages. - */ - @Test - fun testImportNonRoot() { - val classLoader = ClassLoader.getSystemClassLoader() - val ini = classLoader.getResource("ebics_ini_inner_key.xml") - val jaxb = XMLUtil.convertStringToJaxb<SignatureTypes.SignaturePubKeyOrderData>(ini.readText()) - assertEquals("A006", jaxb.value.signaturePubKeyInfo.signatureVersion) - } - - /** - * Test string -> JAXB - */ - @Test - fun testStringToJaxb() { - val classLoader = ClassLoader.getSystemClassLoader() - val ini = classLoader.getResource("ebics_ini_request_sample.xml") - val jaxb = XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(ini.readText()) - println("jaxb loaded") - assertEquals( - "INI", - jaxb.value.header.static.orderDetails.orderType - ) - } - - /** - * Test JAXB -> string - */ - @Test - fun testJaxbToString() { - val hevResponseJaxb = HEVResponse().apply { - this.systemReturnCode = SystemReturnCodeType().apply { - this.reportText = "[EBICS_OK]" - this.returnCode = "000000" - } - this.versionNumber = listOf(HEVResponse.VersionNumber.create("H004", "02.50")) - } - XMLUtil.convertJaxbToString(hevResponseJaxb) - } - - /** - * Test DOM -> JAXB - */ - @Test - fun testDomToJaxb() { - val classLoader = ClassLoader.getSystemClassLoader() - val ini = classLoader.getResource("ebics_ini_request_sample.xml")!! - val iniDom = XMLUtil.parseStringIntoDom(ini.readText()) - XMLUtil.convertDomToJaxb<EbicsUnsecuredRequest>( - EbicsUnsecuredRequest::class.java, - iniDom - ) - } - - @Test - fun testKeyMgmgResponse() { - val responseXml = EbicsKeyManagementResponse().apply { - header = EbicsKeyManagementResponse.Header().apply { - mutable = EbicsKeyManagementResponse.MutableHeaderType().apply { - reportText = "foo" - returnCode = "bar" - } - _static = EbicsKeyManagementResponse.EmptyStaticHeader() - } - version = "H004" - body = EbicsKeyManagementResponse.Body().apply { - returnCode = EbicsKeyManagementResponse.ReturnCode().apply { - authenticate = true - value = "000000" - } - } - } - val text = XMLUtil.convertJaxbToString(responseXml) - assertTrue(text.isNotEmpty()) - } - - @Test - fun testParseHiaRequestOrderData() { - val classLoader = ClassLoader.getSystemClassLoader() - val hia = classLoader.getResource("hia_request_order_data.xml")!!.readText() - XMLUtil.convertStringToJaxb<HIARequestOrderData>(hia) - } - - @Test - fun testHiaLoad() { - val classLoader = ClassLoader.getSystemClassLoader() - val hia = classLoader.getResource("hia_request.xml")!! - val hiaDom = XMLUtil.parseStringIntoDom(hia.readText()) - val x: Element = hiaDom.getElementsByTagNameNS( - "urn:org:ebics:H004", - "OrderDetails" - )?.item(0) as Element - - x.setAttributeNS( - "http://www.w3.org/2001/XMLSchema-instance", - "type", - "UnsecuredReqOrderDetailsType" - ) - - XMLUtil.convertDomToJaxb<EbicsUnsecuredRequest>( - EbicsUnsecuredRequest::class.java, - hiaDom - ) - } - - @Test - fun testLoadInnerKey() { - val jaxbKey = run { - val classLoader = ClassLoader.getSystemClassLoader() - val file = classLoader.getResource( - "ebics_ini_inner_key.xml" - ) - assertNotNull(file) - XMLUtil.convertStringToJaxb<SignatureTypes.SignaturePubKeyOrderData>(file.readText()) - } - - val modulus = jaxbKey.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.modulus - val exponent = jaxbKey.value.signaturePubKeyInfo.pubKeyValue.rsaKeyValue.exponent - CryptoUtil.loadRsaPublicKeyFromComponents(modulus, exponent) - } - - @Test - fun testLoadIniMessage() { - val classLoader = ClassLoader.getSystemClassLoader() - val text = classLoader.getResource("ebics_ini_request_sample.xml")!!.readText() - XMLUtil.convertStringToJaxb<EbicsUnsecuredRequest>(text) - } - - @Test - fun testLoadResponse() { - val response = EbicsResponse().apply { - version = "H004" - header = EbicsResponse.Header().apply { - _static = EbicsResponse.StaticHeaderType() - mutable = EbicsResponse.MutableHeaderType().apply { - this.reportText = "foo" - this.returnCode = "bar" - this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - } - } - authSignature = SignatureType() - body = EbicsResponse.Body().apply { - returnCode = EbicsResponse.ReturnCode().apply { - authenticate = true - value = "asdf" - } - } - } - print(XMLUtil.convertJaxbToString(response)) - } - - @Test - fun testLoadHpb() { - val classLoader = ClassLoader.getSystemClassLoader() - val text = classLoader.getResource("hpb_request.xml")!!.readText() - XMLUtil.convertStringToJaxb<EbicsNpkdRequest>(text) - } - - @Test - fun testHtd() { - val htd = HTDResponseOrderData().apply { - this.partnerInfo = EbicsTypes.PartnerInfo().apply { - this.accountInfoList = listOf( - EbicsTypes.AccountInfo().apply { - this.id = "acctid1" - this.accountHolder = "Mina Musterfrau" - this.accountNumberList = listOf( - EbicsTypes.GeneralAccountNumber().apply { - this.international = true - this.value = "AT411100000237571500" - } - ) - this.currency = "EUR" - this.description = "some account" - this.bankCodeList = listOf( - EbicsTypes.GeneralBankCode().apply { - this.international = true - this.value = "ABAGATWWXXX" - } - ) - } - ) - this.addressInfo = EbicsTypes.AddressInfo().apply { - this.name = "Foo" - } - this.bankInfo = EbicsTypes.BankInfo().apply { - this.hostID = "MYHOST" - } - this.orderInfoList = listOf( - EbicsTypes.AuthOrderInfoType().apply { - this.description = "foo" - this.orderType = "CCC" - this.orderFormat = "foo" - this.transferType = "Upload" - } - ) - } - this.userInfo = EbicsTypes.UserInfo().apply { - this.name = "Some User" - this.userID = EbicsTypes.UserIDType().apply { - this.status = 2 - this.value = "myuserid" - } - this.permissionList = listOf( - EbicsTypes.UserPermission().apply { - this.orderTypes = "CCC ABC" - } - ) - } - } - - val str = XMLUtil.convertJaxbToString(htd) - println(str) - assert(XMLUtil.validateFromString(str)) - } - - - @Test - fun testHkd() { - val hkd = HKDResponseOrderData().apply { - this.partnerInfo = EbicsTypes.PartnerInfo().apply { - this.accountInfoList = listOf( - EbicsTypes.AccountInfo().apply { - this.id = "acctid1" - this.accountHolder = "Mina Musterfrau" - this.accountNumberList = listOf( - EbicsTypes.GeneralAccountNumber().apply { - this.international = true - this.value = "AT411100000237571500" - } - ) - this.currency = "EUR" - this.description = "some account" - this.bankCodeList = listOf( - EbicsTypes.GeneralBankCode().apply { - this.international = true - this.value = "ABAGATWWXXX" - } - ) - } - ) - this.addressInfo = EbicsTypes.AddressInfo().apply { - this.name = "Foo" - } - this.bankInfo = EbicsTypes.BankInfo().apply { - this.hostID = "MYHOST" - } - this.orderInfoList = listOf( - EbicsTypes.AuthOrderInfoType().apply { - this.description = "foo" - this.orderType = "CCC" - this.orderFormat = "foo" - this.transferType = "Upload" - } - ) - } - this.userInfoList = listOf( - EbicsTypes.UserInfo().apply { - this.name = "Some User" - this.userID = EbicsTypes.UserIDType().apply { - this.status = 2 - this.value = "myuserid" - } - this.permissionList = listOf( - EbicsTypes.UserPermission().apply { - this.orderTypes = "CCC ABC" - } - ) - }) - } - - val str = XMLUtil.convertJaxbToString(hkd) - println(str) - assert(XMLUtil.validateFromString(str)) - } - - @Test - fun testEbicsRequestInitializationPhase() { - val ebicsRequestObj = EbicsRequest().apply { - this.version = "H004" - this.revision = 1 - this.authSignature = SignatureType() - this.header = EbicsRequest.Header().apply { - this.authenticate = true - this.mutable = EbicsRequest.MutableHeader().apply { - this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - } - this.static = EbicsRequest.StaticHeaderType().apply { - this.hostID = "myhost" - this.nonce = ByteArray(16) - this.timestamp = - DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(2019, 5, 5, 5, 5, 5, 0, 0) - this.partnerID = "mypid01" - this.userID = "myusr01" - this.product = EbicsTypes.Product().apply { - this.instituteID = "test" - this.language = "en" - this.value = "test" - } - this.orderDetails = EbicsRequest.OrderDetails().apply { - this.orderAttribute = "DZHNN" - this.orderID = "OR01" - this.orderType = "BLA" - this.orderParams = EbicsRequest.StandardOrderParams() - } - this.bankPubKeyDigests = EbicsRequest.BankPubKeyDigests().apply { - this.authentication = EbicsTypes.PubKeyDigest().apply { - this.algorithm = "foo" - this.value = ByteArray(32) - this.version = "X002" - } - this.encryption = EbicsTypes.PubKeyDigest().apply { - this.algorithm = "foo" - this.value = ByteArray(32) - this.version = "E002" - } - } - this.securityMedium = "0000" - } - } - this.body = EbicsRequest.Body().apply { - } - } - - val str = XMLUtil.convertJaxbToString(ebicsRequestObj) - val doc = XMLUtil.parseStringIntoDom(str) - val pair = CryptoUtil.generateRsaKeyPair(1024) - XMLUtil.signEbicsDocument(doc, pair.private) - val finalStr = XMLUtil.convertDomToString(doc) - assert(XMLUtil.validateFromString(finalStr)) - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/EbicsOrderUtilTest.kt b/util/src/test/kotlin/EbicsOrderUtilTest.kt @@ -1,308 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import org.junit.Test -import tech.libeufin.util.EbicsOrderUtil -import tech.libeufin.util.XMLUtil -import tech.libeufin.util.ebics_h004.HTDResponseOrderData -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)) - } - - @Test - fun testDecodeOrderData() { - val orderDataXml = """ - <?xml version="1.0" encoding="UTF-8"?> - <HTDResponseOrderData xmlns="urn:org:ebics:H004" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:org:ebics:H004 ebics_orders_H004.xsd"> - <PartnerInfo> - <AddressInfo> - <Name>Mr Anybody</Name> - <Street>CENSORED</Street> - <PostCode>12345</PostCode> - <City>Footown</City> - </AddressInfo> - <BankInfo> - <HostID>BLABLUBLA</HostID> - </BankInfo> - <AccountInfo ID="accid000000001" Currency="EUR"> - <AccountNumber international="false">12345667</AccountNumber> - <AccountNumber international="true">DE54430609999999999999</AccountNumber> - <BankCode international="false">43060967</BankCode> - <BankCode international="true">GENODEM1GLS</BankCode> - <AccountHolder>Mr Anybody</AccountHolder> - </AccountInfo> - <OrderInfo> - <OrderType>C52</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>CAMT052</OrderFormat> - <Description>Abholen Vormerkposten</Description> - </OrderInfo> - <OrderInfo> - <OrderType>C53</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>CAMT053</OrderFormat> - <Description>Abholen Kontoauszuege</Description> - </OrderInfo> - <OrderInfo> - <OrderType>C54</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>CAMT054</OrderFormat> - <Description>Abholen Nachricht Sammelbuchungsdatei, Soll-, Haben-Avis</Description> - </OrderInfo> - <OrderInfo> - <OrderType>CDZ</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>XMLBIN</OrderFormat> - <Description>Abholen Payment Status Report for Direct Debit</Description> - </OrderInfo> - <OrderInfo> - <OrderType>CRZ</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>XMLBIN</OrderFormat> - <Description>Abholen Payment Status Report for Credit Transfer</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HAA</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Abrufbare Auftragsarten abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HAC</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>HAC</OrderFormat> - <Description>Kundenprotokoll (XML-Format) abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HKD</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Kunden- und Teilnehmerdaten abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HPB</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Public Keys der Bank abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HPD</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Bankparameter abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HTD</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Kunden- und Teilnehmerdaten abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HVD</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>VEU-Status abrufen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HVT</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>VEU-Transaktionsdetails abrufen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HVU</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>VEU-Uebersicht abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>HVZ</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>VEU-Uebersicht mit Zusatzinformationen abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>PTK</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>PTK</OrderFormat> - <Description>Protokolldatei abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>STA</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MT940</OrderFormat> - <Description>Swift-Tagesauszuege abholen</Description> - </OrderInfo> - <OrderInfo> - <OrderType>VMK</OrderType> - <TransferType>Download</TransferType> - <OrderFormat>MT942</OrderFormat> - <Description>Abholen kurzfristige Vormerkposten</Description> - </OrderInfo> - <OrderInfo> - <OrderType>AZV</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>DTAZVJS</OrderFormat> - <Description>AZV im Diskettenformat senden</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>C1C</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>P8CCOR1</OrderFormat> - <Description>Einreichen von Lastschriften D-1-Option in einem Container</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>C2C</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>PN8CONCS</OrderFormat> - <Description>Einreichen von Firmenlastschriften in einem Container</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>CCC</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>PN1CONCS</OrderFormat> - <Description>Ueberweisungen im SEPA-Container</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>CCT</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>PN1GOCS</OrderFormat> - <Description>Überweisungen im ZKA-Format</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>CCU</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>P1URGCS</OrderFormat> - <Description>Einreichen von Eilueberweisungen</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>CDB</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>PAIN8CS</OrderFormat> - <Description>Einreichen von Firmenlastschriften</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>CDC</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>PN8CONCS</OrderFormat> - <Description>Einreichen von Lastschriften in einem Container</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>CDD</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>PN8GOCS</OrderFormat> - <Description>Einreichen von Lastschriften</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>HCA</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Public Key senden</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>HCS</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Teilnehmerschluessel EU und EBICS aendern</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>HIA</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Initiales Senden Public Keys</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>HVE</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>VEU-Unterschrift hinzufuegen</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>HVS</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>VEU-Storno</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>INI</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Passwort-Initialisierung</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>PUB</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Public-Key senden</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - <OrderInfo> - <OrderType>SPR</OrderType> - <TransferType>Upload</TransferType> - <OrderFormat>MISC</OrderFormat> - <Description>Sperrung der Zugangsberechtigung</Description> - <NumSigRequired>0</NumSigRequired> - </OrderInfo> - </PartnerInfo> - <UserInfo> - <UserID Status="1">ANYBOMR</UserID> - <Name>Mr Anybody</Name> - <Permission> - <OrderTypes>C52 C53 C54 CDZ CRZ HAA HAC HKD HPB HPD HTD HVD HVT HVU HVZ PTK</OrderTypes> - </Permission> - <Permission> - <OrderTypes></OrderTypes> - <AccountID>accid000000001</AccountID> - </Permission> - <Permission AuthorisationLevel="E"> - <OrderTypes>AZV CCC CCT CCU</OrderTypes> - </Permission> - <Permission AuthorisationLevel="T"> - <OrderTypes>HCA HCS HIA HVE HVS INI PUB SPR</OrderTypes> - </Permission> - </UserInfo> - </HTDResponseOrderData> - """.trimIndent() - XMLUtil.convertStringToJaxb<HTDResponseOrderData>(orderDataXml); - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/PaytoTest.kt b/util/src/test/kotlin/PaytoTest.kt @@ -1,51 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import org.junit.Test -import tech.libeufin.util.IbanPayto -import tech.libeufin.util.parsePayto - -class PaytoTest { - - @Test - fun wrongCases() { - assert(parsePayto("http://iban/BIC123/IBAN123?receiver-name=The%20Name") == null) - assert(parsePayto("payto:iban/BIC123/IBAN123?receiver-name=The%20Name&address=house") == null) - assert(parsePayto("payto://wrong/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo") == null) - } - - @Test - fun parsePaytoTest() { - val withBic: IbanPayto = parsePayto("payto://iban/BIC123/IBAN123?receiver-name=The%20Name")!! - assert(withBic.iban == "IBAN123") - assert(withBic.bic == "BIC123") - assert(withBic.receiverName == "The Name") - val complete = parsePayto("payto://iban/BIC123/IBAN123?sender-name=The%20Name&amount=EUR:1&message=donation")!! - assert(withBic.iban == "IBAN123") - assert(withBic.bic == "BIC123") - assert(withBic.receiverName == "The Name") - assert(complete.message == "donation") - assert(complete.amount == "EUR:1") - val withoutOptionals = parsePayto("payto://iban/IBAN123")!! - assert(withoutOptionals.bic == null) - assert(withoutOptionals.message == null) - assert(withoutOptionals.receiverName == null) - assert(withoutOptionals.amount == null) - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/SignatureDataTest.kt b/util/src/test/kotlin/SignatureDataTest.kt @@ -1,96 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import org.apache.xml.security.binding.xmldsig.SignatureType -import org.junit.Test -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.XMLUtil -import tech.libeufin.util.ebics_h004.EbicsRequest -import tech.libeufin.util.ebics_h004.EbicsTypes -import java.math.BigInteger -import java.util.* -import javax.xml.datatype.DatatypeFactory - -class SignatureDataTest { - - @Test - fun makeSignatureData() { - - val pair = CryptoUtil.generateRsaKeyPair(1024) - - val tmp = EbicsRequest().apply { - header = EbicsRequest.Header().apply { - version = "H004" - revision = 1 - authenticate = true - static = EbicsRequest.StaticHeaderType().apply { - hostID = "some host ID" - nonce = "nonce".toByteArray() - timestamp = DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()) - partnerID = "some partner ID" - userID = "some user ID" - orderDetails = EbicsRequest.OrderDetails().apply { - orderType = "TST" - orderAttribute = "OZHNN" - } - bankPubKeyDigests = EbicsRequest.BankPubKeyDigests().apply { - authentication = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "X002" - value = CryptoUtil.getEbicsPublicKeyHash(pair.public) - } - encryption = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = CryptoUtil.getEbicsPublicKeyHash(pair.public) - } - } - securityMedium = "0000" - numSegments = BigInteger.ONE - - authSignature = SignatureType() - } - mutable = EbicsRequest.MutableHeader().apply { - transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - } - body = EbicsRequest.Body().apply { - dataTransfer = EbicsRequest.DataTransfer().apply { - signatureData = EbicsRequest.SignatureData().apply { - authenticate = true - value = "to byte array".toByteArray() - } - dataEncryptionInfo = EbicsTypes.DataEncryptionInfo().apply { - transactionKey = "mock".toByteArray() - authenticate = true - encryptionPubKeyDigest = EbicsTypes.PubKeyDigest().apply { - algorithm = "http://www.w3.org/2001/04/xmlenc#sha256" - version = "E002" - value = - CryptoUtil.getEbicsPublicKeyHash(pair.public) - } - } - hostId = "a host ID" - } - } - } - } - - println(XMLUtil.convertJaxbToString(tmp)) - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/TalerConfigTest.kt b/util/src/test/kotlin/TalerConfigTest.kt @@ -1,62 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. - * - * 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/> - */ - -import org.junit.Test -import kotlin.test.assertEquals - -class TalerConfigTest { - - @Test - fun parsing() { - // We assume that libeufin-bank is installed. We could also try to locate the source tree here. - val conf = TalerConfig(ConfigSource("libeufin", "libeufin-bank", "libeufin-bank")) - conf.loadDefaults() - conf.loadFromString( - """ - - [foo] - - bar = baz - - """.trimIndent() - ) - - println(conf.stringify()) - - assertEquals("baz", conf.lookupString("foo", "bar")) - - println(conf.getInstallPath()) - } - - @Test - fun substitution() { - // We assume that libeufin-bank is installed. We could also try to locate the source tree here. - val conf = TalerConfig(ConfigSource("libeufin", "libeufin-bank", "libeufin-bank")) - conf.putValueString("PATHS", "DATADIR", "mydir") - conf.putValueString("foo", "bar", "baz") - conf.putValueString("foo", "bar2", "baz") - - assertEquals("baz", conf.lookupString("foo", "bar")) - assertEquals("baz", conf.lookupPath("foo", "bar")) - - conf.putValueString("foo", "dir1", "foo/\$DATADIR/bar") - - assertEquals("foo/mydir/bar", conf.lookupPath("foo", "dir1")) - } -} diff --git a/util/src/test/kotlin/TimeTest.kt b/util/src/test/kotlin/TimeTest.kt @@ -1,48 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import org.junit.Test -import tech.libeufin.util.maxTimestamp -import tech.libeufin.util.minTimestamp -import java.time.Instant -import java.time.temporal.ChronoUnit -import kotlin.test.assertEquals -import kotlin.test.assertNull - -class TimeTest { - @Test - fun cmp() { - val now = Instant.now() - val inOneMinute = now.plus(1, ChronoUnit.MINUTES) - - // testing the "min" function - assertNull(minTimestamp(null, null)) - assertEquals(now, minTimestamp(now, inOneMinute)) - assertNull(minTimestamp(now, null)) - assertNull(minTimestamp(null, now)) - assertEquals(inOneMinute, minTimestamp(inOneMinute, inOneMinute)) - - // testing the "max" function - assertNull(maxTimestamp(null, null)) - assertEquals(inOneMinute, maxTimestamp(now, inOneMinute)) - assertEquals(now, maxTimestamp(now, null)) - assertEquals(now, maxTimestamp(null, now)) - assertEquals(now, minTimestamp(now, now)) - } -} -\ No newline at end of file diff --git a/util/src/test/kotlin/XmlCombinatorsTest.kt b/util/src/test/kotlin/XmlCombinatorsTest.kt @@ -1,75 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import org.junit.Test -import tech.libeufin.util.XmlElementBuilder -import tech.libeufin.util.constructXml - -class XmlCombinatorsTest { - - @Test - fun testWithModularity() { - fun module(base: XmlElementBuilder) { - base.element("module") - } - val s = constructXml { - root("root") { - module(this) - } - } - println(s) - } - - @Test - fun testWithIterable() { - val s = constructXml(indent = true) { - namespace("iter", "able") - root("iterable") { - element("endOfDocument") { - for (i in 1..10) - element("$i") { - element("$i$i") { - text("$i$i$i") - } - } - } - } - } - println(s) - } - - @Test - fun testBasicXmlBuilding() { - val s = constructXml(indent = true) { - namespace("ebics", "urn:org:ebics:H004") - root("ebics:ebicsRequest") { - attribute("version", "H004") - element("a/b/c") { - attribute("attribute-of", "c") - element("//d/e/f//") { - attribute("nested", "true") - element("g/h/") - } - } - element("one more") - } - } - println(s) - } -} diff --git a/util/src/test/kotlin/XmlUtilTest.kt b/util/src/test/kotlin/XmlUtilTest.kt @@ -1,195 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. - - * 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/> - */ - -import org.apache.xml.security.binding.xmldsig.SignatureType -import org.junit.Test -import org.junit.Assert.* -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import tech.libeufin.util.ebics_h004.EbicsKeyManagementResponse -import tech.libeufin.util.ebics_h004.EbicsResponse -import tech.libeufin.util.ebics_h004.EbicsTypes -import tech.libeufin.util.ebics_h004.HTDResponseOrderData -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.XMLUtil -import java.security.KeyPairGenerator -import java.util.* -import javax.xml.transform.stream.StreamSource -import tech.libeufin.util.XMLUtil.Companion.signEbicsResponse - -class XmlUtilTest { - - @Test - fun deserializeConsecutiveLists() { - - val tmp = XMLUtil.convertStringToJaxb<HTDResponseOrderData>(""" - <?xml version="1.0" encoding="UTF-8" standalone="yes"?> - <HTDResponseOrderData xmlns="urn:org:ebics:H004"> - <PartnerInfo> - <AddressInfo> - <Name>Foo</Name> - </AddressInfo> - <BankInfo> - <HostID>host01</HostID> - </BankInfo> - <AccountInfo Currency="EUR" Description="ACCT" ID="acctid1"> - <AccountNumber international="true">DE21500105174751659277</AccountNumber> - <BankCode international="true">INGDDEFFXXX</BankCode> - <AccountHolder>Mina Musterfrau</AccountHolder> - </AccountInfo> - <AccountInfo Currency="EUR" Description="glsdemoacct" ID="glsdemo"> - <AccountNumber international="true">DE91430609670123123123</AccountNumber> - <BankCode international="true">GENODEM1GLS</BankCode> - <AccountHolder>Mina Musterfrau</AccountHolder> - </AccountInfo> - <OrderInfo> - <OrderType>C53</OrderType> - <TransferType>Download</TransferType> - <Description>foo</Description> - </OrderInfo> - <OrderInfo> - <OrderType>C52</OrderType> - <TransferType>Download</TransferType> - <Description>foo</Description> - </OrderInfo> - <OrderInfo> - <OrderType>CCC</OrderType> - <TransferType>Upload</TransferType> - <Description>foo</Description> - </OrderInfo> - </PartnerInfo> - <UserInfo> - <UserID Status="5">USER1</UserID> - <Name>Some User</Name> - <Permission> - <OrderTypes>C54 C53 C52 CCC</OrderTypes> - </Permission> - </UserInfo> - </HTDResponseOrderData>""".trimIndent() - ) - - println(tmp.value.partnerInfo.orderInfoList[0].description) - } - - @Test - fun exceptionOnConversion() { - try { - XMLUtil.convertStringToJaxb<EbicsKeyManagementResponse>("<malformed xml>") - } catch (e: javax.xml.bind.UnmarshalException) { - // just ensuring this is the exception - println("caught") - return - } - assertTrue(false) - } - - @Test - fun hevValidation(){ - val classLoader = ClassLoader.getSystemClassLoader() - val hev = classLoader.getResourceAsStream("ebics_hev.xml") - assertTrue(XMLUtil.validate(StreamSource(hev))) - } - - @Test - fun iniValidation(){ - val classLoader = ClassLoader.getSystemClassLoader() - val ini = classLoader.getResourceAsStream("ebics_ini_request_sample.xml") - assertTrue(XMLUtil.validate(StreamSource(ini))) - } - - @Test - fun basicSigningTest() { - val doc = XMLUtil.parseStringIntoDom(""" - <myMessage xmlns:ebics="urn:org:ebics:H004"> - <ebics:AuthSignature /> - <foo authenticate="true">Hello World</foo> - </myMessage> - """.trimIndent()) - val kpg = KeyPairGenerator.getInstance("RSA") - kpg.initialize(2048) - val pair = kpg.genKeyPair() - val otherPair = kpg.genKeyPair() - XMLUtil.signEbicsDocument(doc, pair.private) - kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) - kotlin.test.assertFalse(XMLUtil.verifyEbicsDocument(doc, otherPair.public)) - } - - @Test - fun verifySigningWithConversion() { - - val pair = CryptoUtil.generateRsaKeyPair(2048) - - val response = EbicsResponse().apply { - version = "H004" - header = EbicsResponse.Header().apply { - _static = EbicsResponse.StaticHeaderType() - mutable = EbicsResponse.MutableHeaderType().apply { - this.reportText = "foo" - this.returnCode = "bar" - this.transactionPhase = EbicsTypes.TransactionPhaseType.INITIALISATION - } - } - authSignature = SignatureType() - body = EbicsResponse.Body().apply { - returnCode = EbicsResponse.ReturnCode().apply { - authenticate = true - value = "asdf" - } - } - } - - val signature = signEbicsResponse(response, pair.private) - val signatureJaxb = XMLUtil.convertStringToJaxb<EbicsResponse>(signature) - - assertTrue( - - XMLUtil.verifyEbicsDocument( - XMLUtil.convertJaxbToDocument(signatureJaxb.value), - pair.public - ) - ) - } - - @Test - fun multiAuthSigningTest() { - val doc = XMLUtil.parseStringIntoDom(""" - <myMessage xmlns:ebics="urn:org:ebics:H004"> - <ebics:AuthSignature /> - <foo authenticate="true">Hello World</foo> - <bar authenticate="true">Another one!</bar> - </myMessage> - """.trimIndent()) - val kpg = KeyPairGenerator.getInstance("RSA") - kpg.initialize(2048) - val pair = kpg.genKeyPair() - XMLUtil.signEbicsDocument(doc, pair.private) - kotlin.test.assertTrue(XMLUtil.verifyEbicsDocument(doc, pair.public)) - } - - @Test - fun testRefSignature() { - val classLoader = ClassLoader.getSystemClassLoader() - val docText = classLoader.getResourceAsStream("signature1/doc.xml")!!.readAllBytes().toString(Charsets.UTF_8) - val doc = XMLUtil.parseStringIntoDom(docText) - val keyText = classLoader.getResourceAsStream("signature1/public_key.txt")!!.readAllBytes() - val keyBytes = Base64.getDecoder().decode(keyText) - val key = CryptoUtil.loadRsaPublicKey(keyBytes) - assertTrue(XMLUtil.verifyEbicsDocument(doc, key)) - } -} -\ No newline at end of file