summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt206
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt9
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt80
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt16
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Tan.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt4
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt190
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt14
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt13
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/helpers.kt20
-rw-r--r--bank/src/test/kotlin/AmountTest.kt5
-rw-r--r--bank/src/test/kotlin/BankIntegrationApiTest.kt1
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt1
-rw-r--r--bank/src/test/kotlin/DatabaseTest.kt2
-rw-r--r--bank/src/test/kotlin/helpers.kt3
-rw-r--r--build.gradle5
m---------contrib/wallet-core0
-rw-r--r--integration/test/IntegrationTest.kt2
-rw-r--r--util/src/main/kotlin/Encoding.kt92
-rw-r--r--util/src/main/kotlin/HTTP.kt42
-rw-r--r--util/src/main/kotlin/strings.kt5
-rw-r--r--util/src/test/kotlin/CryptoUtilTest.kt73
34 files changed, 419 insertions, 406 deletions
diff --git a/Makefile b/Makefile
index ced6ab17..94d79851 100644
--- a/Makefile
+++ b/Makefile
@@ -94,6 +94,11 @@ install:
assemble:
./gradlew assemble
+.PHONY: doc
+doc:
+ ./gradlew dokkaHtmlMultiModule
+ open build/dokka/htmlMultiModule/index.html
+
.PHONY: check
check: install-nobuild-bank-files
./gradlew check
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
deleted file mode 100644
index 4a83e94e..00000000
--- a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
+++ /dev/null
@@ -1,206 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2023 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.bank
-
-import io.ktor.http.*
-import io.ktor.server.application.*
-import io.ktor.server.routing.Route
-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 org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import tech.libeufin.bank.AccountDAO.*
-import tech.libeufin.util.*
-
-private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Authentication")
-
-private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin");
-
-/** Restrict route access to admin */
-fun Route.authAdmin(db: Database, scope: TokenScope, enforce: Boolean = true, callback: Route.() -> Unit): Route =
- intercept(callback) {
- if (enforce) {
- val login = context.authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
- if (login != "admin") {
- throw unauthorized("Only administrator allowed")
- }
- context.attributes.put(AUTH_IS_ADMIN, true)
- } else {
- val login = try {
- context.authenticateBankRequest(db, scope)
- } catch (e: Exception) {
- null
- }
- context.attributes.put(AUTH_IS_ADMIN, login == "admin")
- }
- }
-
-
-/** Authenticate and check access rights */
-fun Route.auth(db: Database, scope: TokenScope, allowAdmin: Boolean = false, requireAdmin: Boolean = false, callback: Route.() -> Unit): Route =
- intercept(callback) {
- val authLogin = context.authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
- if (requireAdmin && authLogin != "admin") {
- if (authLogin != "admin") {
- throw unauthorized("Only administrator allowed")
- }
- } else {
- val hasRight = authLogin == username || (allowAdmin && authLogin == "admin");
- if (!hasRight) {
- throw unauthorized("Customer $authLogin have no right on $username account")
- }
- }
- context.attributes.put(AUTH_IS_ADMIN, authLogin == "admin")
- }
-
-val PipelineContext<Unit, ApplicationCall>.username: String get() = call.username
-val PipelineContext<Unit, ApplicationCall>.isAdmin: Boolean get() = call.isAdmin
-val ApplicationCall.username: String get() = expectUriComponent("USERNAME")
-val ApplicationCall.isAdmin: Boolean get() = attributes.getOrNull(AUTH_IS_ADMIN) ?: false
-
-/**
- * This function tries to authenticate the call according
- * to the scheme that is mentioned in the Authorization header.
- * The allowed schemes are either 'HTTP basic auth' or 'bearer token'.
- *
- * requiredScope can be either "readonly" or "readwrite".
- *
- * Returns the authenticated customer login, or null if they failed.
- */
-private suspend fun ApplicationCall.authenticateBankRequest(db: Database, requiredScope: TokenScope): String? {
- // Extracting the Authorization header.
- val header = getAuthorizationRawHeader(this.request)
- if (header == null) {
- // Basic auth challenge
- response.header(HttpHeaders.WWWAuthenticate, "Basic")
- throw unauthorized(
- "Authorization header not found.",
- TalerErrorCode.GENERIC_PARAMETER_MISSING
- )
- }
-
- val authDetails = getAuthorizationDetails(header) ?: throw badRequest(
- "Authorization is invalid.",
- TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
- )
- return when (authDetails.scheme) {
- "Basic" -> doBasicAuth(db, authDetails.content)
- "Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
- else -> throw unauthorized("Authorization method wrong or not supported.")
- }
-}
-
-// Get the auth token (stripped of the bearer-token:-prefix)
-// IF the call was authenticated with it.
-fun ApplicationCall.getAuthToken(): String? {
- val h = getAuthorizationRawHeader(this.request) ?: return null
- val authDetails = getAuthorizationDetails(h) ?: throw badRequest(
- "Authorization header is malformed.", TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
- )
- if (authDetails.scheme == "Bearer") return splitBearerToken(authDetails.content) ?: throw throw badRequest(
- "Authorization header is malformed (could not strip the prefix from Bearer token).",
- TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
- )
- return null // Not a Bearer token case.
-}
-
-
-/**
- * Performs the HTTP basic authentication. Returns the
- * authenticated customer login on success, or null otherwise.
- */
-private suspend fun doBasicAuth(db: Database, encodedCredentials: String): String? {
- val plainUserAndPass = String(base64ToBytes(encodedCredentials), Charsets.UTF_8) // :-separated
- val userAndPassSplit = plainUserAndPass.split(
- ":",
- /**
- * this parameter allows colons to occur in passwords.
- * Without this, passwords that have colons would be split
- * and become meaningless.
- */
- limit = 2
- )
- if (userAndPassSplit.size != 2) throw badRequest(
- "Malformed Basic auth credentials found in the Authorization header.",
- TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
- )
- val (login, plainPassword) = userAndPassSplit
- val passwordHash = db.account.passwordHash(login) ?: throw unauthorized("Bad password")
- if (!CryptoUtil.checkpw(plainPassword, passwordHash)) return null
- return login
-}
-
-/**
- * This function takes a prefixed Bearer token, removes the
- * secret-token:-prefix and returns it. Returns null, if the
- * input is invalid.
- */
-private fun splitBearerToken(tok: String): String? {
- val tokenSplit = tok.split(":", limit = 2)
- if (tokenSplit.size != 2) return null
- if (tokenSplit[0] != "secret-token") return null
- return tokenSplit[1]
-}
-
-/* Performs the secret-token authentication. Returns the
- * authenticated customer login on success, null otherwise. */
-private suspend fun doTokenAuth(
- db: Database,
- token: String,
- requiredScope: TokenScope,
-): String? {
- val bareToken = splitBearerToken(token) ?: throw badRequest(
- "Bearer token malformed",
- TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
- )
- val tokenBytes = try {
- Base32Crockford.decode(bareToken)
- } catch (e: Exception) {
- throw badRequest(
- e.message, TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
- )
- }
- val maybeToken: BearerToken? = db.token.get(tokenBytes)
- if (maybeToken == null) {
- logger.error("Auth token not found")
- return null
- }
- if (maybeToken.expirationTime.isBefore(Instant.now())) {
- logger.error("Auth token is expired")
- return null
- }
- if (maybeToken.scope == TokenScope.readonly && requiredScope == TokenScope.readwrite) {
- logger.error("Auth token has insufficient scope")
- return null
- }
- if (!maybeToken.isRefreshable && requiredScope == TokenScope.refreshable) {
- logger.error("Could not refresh unrefreshable token")
- return null
- }
- // Getting the related username.
- return db.account.login(maybeToken.bankCustomer) ?: throw libeufinError(
- HttpStatusCode.InternalServerError,
- "Customer not found, despite token mentions it.",
- TalerErrorCode.GENERIC_INTERNAL_INVARIANT_FAILURE
- )
-} \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
index 22c573ae..11f791aa 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
@@ -27,7 +27,8 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import net.taler.common.errorcodes.TalerErrorCode
-import tech.libeufin.bank.WithdrawalDAO.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.db.WithdrawalDAO.*
import java.lang.AssertionError
fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
@@ -40,7 +41,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
// Note: wopid acts as an authentication token.
get("/taler-integration/withdrawal-operation/{wopid}") {
- val uuid = call.uuidUriComponent("wopid")
+ val uuid = call.uuidParameter("wopid")
val params = StatusParams.extract(call.request.queryParameters)
val op = db.withdrawal.pollStatus(uuid, params) ?: throw notFound(
"Withdrawal operation '$uuid' not found",
@@ -57,7 +58,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
))
}
post("/taler-integration/withdrawal-operation/{wopid}") {
- val opId = call.uuidUriComponent("wopid")
+ val opId = call.uuidParameter("wopid")
val req = call.receive<BankWithdrawalOperationPostRequest>()
val res = db.withdrawal.setDetails(
@@ -100,7 +101,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
}
}
post("/taler-integration/withdrawal-operation/{wopid}/abort") {
- val opId = call.uuidUriComponent("wopid")
+ val opId = call.uuidParameter("wopid")
when (db.withdrawal.abort(opId)) {
AbortResult.UnknownOperation -> throw notFound(
"Withdrawal operation $opId not found",
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt
index ba6748b4..d9e88942 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt
@@ -25,7 +25,9 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
import tech.libeufin.util.*
-import tech.libeufin.bank.ConversionDAO.*
+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) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
index 69df2ab3..a7d0f2c6 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -36,12 +36,15 @@ import net.taler.common.errorcodes.TalerErrorCode
import net.taler.wallet.crypto.Base32Crockford
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import tech.libeufin.bank.AccountDAO.*
-import tech.libeufin.bank.CashoutDAO.*
-import tech.libeufin.bank.ExchangeDAO.*
-import tech.libeufin.bank.TransactionDAO.*
-import tech.libeufin.bank.WithdrawalDAO.*
-import tech.libeufin.bank.TanDAO.*
+import tech.libeufin.bank.*
+import tech.libeufin.bank.auth.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.db.TanDAO.*
+import tech.libeufin.bank.db.AccountDAO.*
+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.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers")
@@ -80,41 +83,36 @@ private fun Routing.coreBankTokenApi(db: Database) {
val TOKEN_DEFAULT_DURATION: Duration = Duration.ofDays(1L)
auth(db, TokenScope.refreshable) {
post("/accounts/{USERNAME}/token") {
- val maybeAuthToken = call.getAuthToken()
+ val existingToken = call.authToken
val req = call.receive<TokenRequest>()
- /**
- * This block checks permissions ONLY IF the call was authenticated with a token. Basic auth
- * gets always granted.
- */
- if (maybeAuthToken != null) {
- val tokenBytes = Base32Crockford.decode(maybeAuthToken)
- val refreshingToken =
- db.token.get(tokenBytes)
- ?: throw internalServerError(
- "Token used to auth not found in the database!"
- )
+
+ if (existingToken != null) {
+ // This block checks permissions ONLY IF the call was authenticated with a token
+ val refreshingToken = db.token.get(existingToken) ?: throw internalServerError(
+ "Token used to auth not found in the database!"
+ )
if (refreshingToken.scope == TokenScope.readonly && req.scope == TokenScope.readwrite)
- throw forbidden(
- "Cannot generate RW token from RO",
- TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT
- )
+ throw forbidden(
+ "Cannot generate RW token from RO",
+ TalerErrorCode.GENERIC_TOKEN_PERMISSION_INSUFFICIENT
+ )
}
val tokenBytes = ByteArray(32).apply { Random.nextBytes(this) }
val tokenDuration: Duration = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION
val creationTime = Instant.now()
- val expirationTimestamp =
- if (tokenDuration == ChronoUnit.FOREVER.duration) {
- logger.debug("Creating 'forever' token.")
- Instant.MAX
- } else {
- try {
- logger.debug("Creating token with days duration: ${tokenDuration.toDays()}")
- creationTime.plus(tokenDuration)
- } catch (e: Exception) {
- throw badRequest("Bad token duration: ${e.message}")
- }
+ val expirationTimestamp =
+ if (tokenDuration == ChronoUnit.FOREVER.duration) {
+ logger.debug("Creating 'forever' token.")
+ Instant.MAX
+ } else {
+ try {
+ logger.debug("Creating token with days duration: ${tokenDuration.toDays()}")
+ creationTime.plus(tokenDuration)
+ } catch (e: Exception) {
+ throw badRequest("Bad token duration: ${e.message}")
}
+ }
if (!db.token.create(
login = username,
content = tokenBytes,
@@ -135,8 +133,8 @@ private fun Routing.coreBankTokenApi(db: Database) {
}
auth(db, TokenScope.readonly) {
delete("/accounts/{USERNAME}/token") {
- val token = call.getAuthToken() ?: throw badRequest("Basic auth not supported here.")
- db.token.delete(Base32Crockford.decode(token))
+ val token = call.authToken ?: throw badRequest("Basic auth not supported here.")
+ db.token.delete(token)
call.respond(HttpStatusCode.NoContent)
}
}
@@ -398,7 +396,7 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) {
}
}
get("/accounts/{USERNAME}/transactions/{T_ID}") {
- val tId = call.longUriComponent("T_ID")
+ val tId = call.longParameter("T_ID")
val tx = db.transaction.get(tId, username) ?: throw notFound(
"Bank transaction '$tId' not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
@@ -479,7 +477,7 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
}
}
post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
- val id = call.uuidUriComponent("withdrawal_id")
+ val id = call.uuidParameter("withdrawal_id")
val challenge = call.challenge(db, Operation.withdrawal)
when (db.withdrawal.confirm(username, id, Instant.now(), challenge != null)) {
WithdrawalConfirmationResult.UnknownOperation -> throw notFound(
@@ -510,7 +508,7 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) {
}
}
get("/withdrawals/{withdrawal_id}") {
- val uuid = call.uuidUriComponent("withdrawal_id")
+ val uuid = call.uuidParameter("withdrawal_id")
val params = StatusParams.extract(call.request.queryParameters)
val op = db.withdrawal.pollInfo(uuid, params) ?: throw notFound(
"Withdrawal operation '$uuid' not found",
@@ -568,7 +566,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio
}
auth(db, TokenScope.readonly) {
get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") {
- val id = call.longUriComponent("CASHOUT_ID")
+ val id = call.longParameter("CASHOUT_ID")
val cashout = db.cashout.get(id, username) ?: throw notFound(
"Cashout operation $id not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
@@ -601,7 +599,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio
private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) {
auth(db, TokenScope.readwrite) {
post("/accounts/{USERNAME}/challenge/{CHALLENGE_ID}") {
- val id = call.longUriComponent("CHALLENGE_ID")
+ val id = call.longParameter("CHALLENGE_ID")
val res = db.tan.send(
id = id,
login = username,
@@ -646,7 +644,7 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) {
}
}
post("/accounts/{USERNAME}/challenge/{CHALLENGE_ID}/confirm") {
- val id = call.longUriComponent("CHALLENGE_ID")
+ val id = call.longParameter("CHALLENGE_ID")
val req = call.receive<ChallengeSolve>()
val res = db.tan.solve(
id = id,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 7198be5a..042ef833 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -54,7 +54,8 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import org.postgresql.util.PSQLState
-import tech.libeufin.bank.AccountDAO.*
+import tech.libeufin.bank.db.AccountDAO.*
+import tech.libeufin.bank.db.*
import tech.libeufin.util.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Main")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt
index 6a5f7238..4575b106 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt
@@ -25,6 +25,8 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
import tech.libeufin.util.*
+import tech.libeufin.bank.auth.*
+import tech.libeufin.bank.db.*
fun Routing.revenueApi(db: Database) {
auth(db, TokenScope.readonly) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index 569a31f3..c94fa09e 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -264,24 +264,12 @@ enum class TokenScope {
refreshable // Not spec'd as a scope!
}
-/**
- * Convenience type to set/get authentication tokens to/from
- * the database.
- */
data class BearerToken(
- val content: ByteArray,
val scope: TokenScope,
- val isRefreshable: Boolean = false,
+ val isRefreshable: Boolean,
val creationTime: Instant,
val expirationTime: Instant,
- /**
- * Serial ID of the database row that hosts the bank customer
- * that is associated with this token. NOTE: if the token is
- * refreshed by a client that doesn't have a user+password login
- * in the system, the creator remains always the original bank
- * customer that created the very first token.
- */
- val bankCustomer: Long
+ val login: String
)
@Serializable
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt b/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt
index 797320f9..5dddd807 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt
@@ -27,7 +27,9 @@ import io.ktor.http.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.application.*
-import tech.libeufin.bank.TanDAO.Challenge
+import tech.libeufin.bank.db.TanDAO.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.auth.*
import io.ktor.util.pipeline.PipelineContext
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
index 67de9f06..4da9a3ab 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -31,7 +31,9 @@ import java.time.Instant
import net.taler.common.errorcodes.TalerErrorCode
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import tech.libeufin.bank.ExchangeDAO.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.db.ExchangeDAO.*
+import tech.libeufin.bank.auth.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
new file mode 100644
index 00000000..edb8e305
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
@@ -0,0 +1,190 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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.bank.auth
+
+import io.ktor.http.*
+import io.ktor.server.application.*
+import io.ktor.server.routing.Route
+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 org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import tech.libeufin.bank.db.AccountDAO.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.*
+import tech.libeufin.util.*
+
+private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Authentication")
+
+/** Used to store if the currenly authenticated user is admin */
+private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin");
+/** Used to store used auth token */
+private val AUTH_TOKEN = AttributeKey<ByteArray>("auth_token");
+
+/** Get username of the request account */
+val ApplicationCall.username: String get() = expectParameter("USERNAME")
+/** Get username of the request account */
+val PipelineContext<Unit, ApplicationCall>.username: String get() = call.username
+
+/** Check if current auth account is admin */
+val ApplicationCall.isAdmin: Boolean get() = attributes.getOrNull(AUTH_IS_ADMIN) ?: false
+/** Check if current auth account is admin */
+val PipelineContext<Unit, ApplicationCall>.isAdmin: Boolean get() = call.isAdmin
+
+/** Check auth token used for authentification */
+val ApplicationCall.authToken: ByteArray? get() = attributes.getOrNull(AUTH_TOKEN)
+
+/**
+ * Create an admin authenticated route for [scope].
+ *
+ * If [enforce], only admin can access this route.
+ *
+ * You can check is the currently authenticated user is admin using [isAdmin].
+ **/
+fun Route.authAdmin(db: Database, scope: TokenScope, enforce: Boolean = true, callback: Route.() -> Unit): Route =
+ intercept(callback) {
+ if (enforce) {
+ val login = context.authenticateBankRequest(db, scope)
+ if (login != "admin") {
+ throw unauthorized("Only administrator allowed")
+ }
+ context.attributes.put(AUTH_IS_ADMIN, true)
+ } else {
+ val login = try {
+ context.authenticateBankRequest(db, scope)
+ } catch (e: Exception) {
+ null
+ }
+ context.attributes.put(AUTH_IS_ADMIN, login == "admin")
+ }
+ }
+
+
+/**
+ * Create an authenticated route for [scope].
+ *
+ * If [allowAdmin], admin is allowed to auth for any user.
+ * If [requireAdmin], only admin can access this route.
+ *
+ * You can check is the currently authenticated user is admin using [isAdmin].
+ **/
+fun Route.auth(db: Database, scope: TokenScope, allowAdmin: Boolean = false, requireAdmin: Boolean = false, callback: Route.() -> Unit): Route =
+ intercept(callback) {
+ val authLogin = context.authenticateBankRequest(db, scope)
+ if (requireAdmin && authLogin != "admin") {
+ if (authLogin != "admin") {
+ throw unauthorized("Only administrator allowed")
+ }
+ } else {
+ val hasRight = authLogin == username || (allowAdmin && authLogin == "admin");
+ if (!hasRight) {
+ throw unauthorized("Customer $authLogin have no right on $username account")
+ }
+ }
+ context.attributes.put(AUTH_IS_ADMIN, authLogin == "admin")
+ }
+
+/**
+ * Authenticate an HTTP request for [requiredScope] according to the scheme that is mentioned
+ * in the Authorization header.
+ * The allowed schemes are either 'Basic' or 'Bearer'.
+ *
+ * Returns the authenticated customer login.
+ */
+private suspend fun ApplicationCall.authenticateBankRequest(db: Database, requiredScope: TokenScope): String {
+ val header = request.headers["Authorization"]
+
+ // Basic auth challenge
+ if (header == null) {
+ response.header(HttpHeaders.WWWAuthenticate, "Basic")
+ throw unauthorized(
+ "Authorization header not found",
+ TalerErrorCode.GENERIC_PARAMETER_MISSING
+ )
+ }
+
+ // Parse header
+ val (scheme, content) = header.splitOnce(" ") ?: throw badRequest(
+ "Authorization is invalid",
+ TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ return when (scheme) {
+ "Basic" -> doBasicAuth(db, content)
+ "Bearer" -> doTokenAuth(db, content, requiredScope)
+ else -> throw unauthorized("Authorization method wrong or not supported")
+ }
+}
+
+/**
+ * Performs the HTTP Basic Authentication.
+ *
+ * Returns the authenticated customer login
+ */
+private suspend fun doBasicAuth(db: Database, encoded: String): String {
+ val decoded = String(base64ToBytes(encoded), Charsets.UTF_8)
+ val (login, plainPassword) = decoded.splitOnce(":") ?: throw badRequest(
+ "Malformed Basic auth credentials found in the Authorization header",
+ TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ val hash = db.account.passwordHash(login) ?: throw unauthorized("Unknown account")
+ if (!CryptoUtil.checkpw(plainPassword, hash)) throw unauthorized("Bad password")
+ return login
+}
+
+/**
+ * Performs the secret-token HTTP Bearer Authentication.
+ *
+ * Returns the authenticated customer login
+ */
+private suspend fun ApplicationCall.doTokenAuth(
+ db: Database,
+ bearer: String,
+ requiredScope: TokenScope,
+): String {
+ if (!bearer.startsWith("secret-token:")) throw badRequest(
+ "Bearer token malformed",
+ TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ val decoded = try {
+ Base32Crockford.decode(bearer.slice(13..bearer.length-1))
+ } catch (e: Exception) {
+ throw badRequest(
+ e.message, TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ }
+ val token: BearerToken = db.token.get(decoded) ?: throw unauthorized("Unknown token")
+ when {
+ token.expirationTime.isBefore(Instant.now())
+ -> throw unauthorized("Expired auth token")
+
+ token.scope == TokenScope.readonly && requiredScope == TokenScope.readwrite
+ -> throw unauthorized("Auth token has insufficient scope")
+
+ !token.isRefreshable && requiredScope == TokenScope.refreshable
+ -> throw unauthorized("Unrefreshable token")
+ }
+
+ attributes.put(AUTH_TOKEN, decoded)
+
+ return token.login
+} \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
index 9694d357..70b9bd1f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -17,11 +17,12 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import tech.libeufin.util.*
import java.time.*
import java.sql.Types
+import tech.libeufin.bank.*
/** Data access logic for accounts */
class AccountDAO(private val db: Database) {
@@ -411,17 +412,6 @@ class AccountDAO(private val db: Database) {
}
}
- /** Get login of account [id] */
- suspend fun login(id: Long): String? = db.conn { conn ->
- val stmt = conn.prepareStatement("""
- SELECT login FROM customers WHERE customer_id=?
- """)
- stmt.setLong(1, id)
- stmt.oneOrNull {
- it.getString(1)
- }
- }
-
/** Get bank info of account [login] */
suspend fun bankInfo(login: String): BankInfo? = db.conn { conn ->
val stmt = conn.prepareStatement("""
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
index 2c70a144..a7950aa2 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
@@ -17,12 +17,13 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+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.bank.*
/** Data access logic for cashout operations */
class CashoutDAO(private val db: Database) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
index 7cd43dd4..b2d521c7 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -17,10 +17,11 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import tech.libeufin.util.*
import tech.libeufin.bank.*
+import tech.libeufin.bank.*
/** Data access logic for conversion */
class ConversionDAO(private val db: Database) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
index 454c03b9..e4effe00 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
@@ -17,7 +17,7 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import org.postgresql.jdbc.PgConnection
import org.postgresql.ds.PGSimpleDataSource
@@ -36,6 +36,7 @@ import com.zaxxer.hikari.*
import tech.libeufin.util.*
import io.ktor.http.HttpStatusCode
import net.taler.common.errorcodes.TalerErrorCode
+import tech.libeufin.bank.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Database")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
index 236c60c3..44cb272c 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -17,13 +17,14 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import java.util.UUID
import java.time.Instant
import java.time.Duration
import java.util.concurrent.TimeUnit
import tech.libeufin.util.*
+import tech.libeufin.bank.*
/** Data access logic for exchange specific logic */
class ExchangeDAO(private val db: Database) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt
index 65776f52..c96a1e5d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/NotificationWatcher.kt
@@ -17,7 +17,7 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
@@ -27,6 +27,7 @@ import org.postgresql.ds.PGSimpleDataSource
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.*
+import tech.libeufin.bank.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.NotificationWatcher")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
index e15707b6..457d1216 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
@@ -17,10 +17,11 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import tech.libeufin.util.*
import tech.libeufin.bank.*
+import tech.libeufin.bank.db.*
import java.util.concurrent.TimeUnit
import java.time.Duration
import java.time.Instant
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
index 2791d92f..d3754938 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
@@ -17,10 +17,11 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import tech.libeufin.util.*
import java.time.Instant
+import tech.libeufin.bank.*
/** Data access logic for auth tokens */
class TokenDAO(private val db: Database) {
@@ -63,21 +64,21 @@ class TokenDAO(private val db: Database) {
suspend fun get(token: ByteArray): BearerToken? = db.conn { conn ->
val stmt = conn.prepareStatement("""
SELECT
- expiration_time,
creation_time,
- bank_customer,
+ expiration_time,
+ login,
scope,
is_refreshable
FROM bearer_tokens
+ JOIN customers ON bank_customer=customer_id
WHERE content=?;
""")
stmt.setBytes(1, token)
stmt.oneOrNull {
BearerToken(
- content = token,
- creationTime = it.getLong("creation_time").microsToJavaInstant() ?: throw faultyTimestampByBank(),
+ creationTime = it.getLong("creation_time").microsToJavaInstant() ?: throw faultyDurationByClient(),
expirationTime = it.getLong("expiration_time").microsToJavaInstant() ?: throw faultyDurationByClient(),
- bankCustomer = it.getLong("bank_customer"),
+ login = it.getString("login"),
scope = TokenScope.valueOf(it.getString("scope")),
isRefreshable = it.getBoolean("is_refreshable")
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
index 4d734946..a72f9743 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -17,13 +17,14 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.*
import java.time.*
import java.sql.Types
+import tech.libeufin.bank.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util.TransactionDAO")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
index f6b912cc..380263b4 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -17,7 +17,7 @@
* <http://www.gnu.org/licenses/>
*/
-package tech.libeufin.bank
+package tech.libeufin.bank.db
import java.util.UUID
import java.time.Instant
@@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit
import tech.libeufin.util.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
+import tech.libeufin.bank.*
/** Data access logic for withdrawal operations */
class WithdrawalDAO(private val db: Database) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 268cef66..84dc96a6 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -39,14 +39,16 @@ import java.util.*
import net.taler.common.errorcodes.TalerErrorCode
import org.slf4j.Logger
import org.slf4j.LoggerFactory
-import tech.libeufin.bank.AccountDAO.*
import tech.libeufin.util.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.db.AccountDAO.*
+import tech.libeufin.bank.auth.*
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.helpers")
-fun ApplicationCall.expectUriComponent(componentName: String) =
- maybeUriComponent(componentName) ?: throw badRequest(
- "No username found in the URI",
+fun ApplicationCall.expectParameter(name: String) =
+ parameters[name] ?: throw badRequest(
+ "Missing '$name' param",
TalerErrorCode.GENERIC_PARAMETER_MISSING
)
@@ -93,17 +95,17 @@ fun getWithdrawalConfirmUrl(
return baseUrl.replace("{woid}", wopId.toString())
}
-fun ApplicationCall.uuidUriComponent(name: String): UUID {
+fun ApplicationCall.uuidParameter(name: String): UUID {
try {
- return UUID.fromString(expectUriComponent(name))
+ return UUID.fromString(expectParameter(name))
} catch (e: Exception) {
throw badRequest("UUID uri component malformed: ${e.message}")
}
}
-fun ApplicationCall.longUriComponent(name: String): Long {
+fun ApplicationCall.longParameter(name: String): Long {
try {
- return expectUriComponent(name).toLong()
+ return expectParameter(name).toLong()
} catch (e: Exception) {
throw badRequest("Long uri component malformed: ${e.message}")
}
@@ -177,4 +179,4 @@ data class StoredUUID(val value: UUID) {
return StoredUUID(UUID.fromString(string))
}
}
-} \ No newline at end of file
+}
diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt
index 32e51197..43b6fbef 100644
--- a/bank/src/test/kotlin/AmountTest.kt
+++ b/bank/src/test/kotlin/AmountTest.kt
@@ -23,8 +23,9 @@ import kotlin.test.*
import org.junit.Test
import org.postgresql.jdbc.PgConnection
import tech.libeufin.bank.*
-import tech.libeufin.bank.TransactionDAO.*
-import tech.libeufin.bank.WithdrawalDAO.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.db.TransactionDAO.*
+import tech.libeufin.bank.db.WithdrawalDAO.*
import tech.libeufin.util.*
class AmountTest {
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
index 941aa422..ce6b46c7 100644
--- a/bank/src/test/kotlin/BankIntegrationApiTest.kt
+++ b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -29,6 +29,7 @@ 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.*
class BankIntegrationApiTest {
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
index 7e56f0b8..86115e78 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -34,6 +34,7 @@ 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.*
class CoreBankConfigTest {
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
index 19d552ac..fc19d5a8 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -29,7 +29,7 @@ import kotlin.test.*
import kotlinx.coroutines.*
import org.junit.Test
import tech.libeufin.bank.*
-import tech.libeufin.bank.AccountDAO.*
+import tech.libeufin.bank.db.AccountDAO.*
import tech.libeufin.util.*
class DatabaseTest {
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 978333cb..951d24f5 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -32,7 +32,8 @@ import kotlinx.serialization.json.*
import net.taler.common.errorcodes.TalerErrorCode
import net.taler.wallet.crypto.Base32Crockford
import tech.libeufin.bank.*
-import tech.libeufin.bank.AccountDAO.*
+import tech.libeufin.bank.db.*
+import tech.libeufin.bank.db.AccountDAO.*
import tech.libeufin.util.*
/* ----- Setup ----- */
diff --git a/build.gradle b/build.gradle
index 632f7c52..01941c05 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,6 +4,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
plugins {
id("org.jetbrains.kotlin.jvm") version "1.9.20"
+ id("org.jetbrains.dokka") version "1.9.10"
id("idea")
id("java-library")
id("maven-publish")
@@ -33,6 +34,10 @@ allprojects {
}
}
+subprojects {
+ apply plugin: 'org.jetbrains.dokka'
+}
+
idea {
module {
excludeDirs += file("frontend")
diff --git a/contrib/wallet-core b/contrib/wallet-core
-Subproject ec95723a68d36620aa66109c329437612383830
+Subproject 330edf879ab8f6fa4dbaf96de8ac84365c584e1
diff --git a/integration/test/IntegrationTest.kt b/integration/test/IntegrationTest.kt
index ec2ea642..13b665c0 100644
--- a/integration/test/IntegrationTest.kt
+++ b/integration/test/IntegrationTest.kt
@@ -23,7 +23,7 @@ import tech.libeufin.bank.TalerAmount as BankAmount
import tech.libeufin.nexus.*
import tech.libeufin.nexus.Database as NexusDb
import tech.libeufin.nexus.TalerAmount as NexusAmount
-import tech.libeufin.bank.AccountDAO.*
+import tech.libeufin.bank.db.AccountDAO.*
import tech.libeufin.util.*
import java.io.File
import java.time.Instant
diff --git a/util/src/main/kotlin/Encoding.kt b/util/src/main/kotlin/Encoding.kt
index 4f3dcabf..26a523f6 100644
--- a/util/src/main/kotlin/Encoding.kt
+++ b/util/src/main/kotlin/Encoding.kt
@@ -31,61 +31,73 @@ object Base32Crockford {
fun encode(data: ByteArray): String {
val sb = StringBuilder()
- val size = data.size
- var bitBuf = 0
- var numBits = 0
- var pos = 0
- while (pos < size || numBits > 0) {
- if (pos < size && numBits < 5) {
- val d = data.getIntAt(pos++)
- bitBuf = (bitBuf shl 8) or d
- numBits += 8
- }
- if (numBits < 5) {
- // zero-padding
- bitBuf = bitBuf shl (5 - numBits)
- numBits = 5
+ 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
}
- val v = bitBuf.ushr(numBits - 5) and 31
- sb.append(encTable[v])
- numBits -= 5
}
- return sb.toString()
+ 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) {
- val size = encoded.length
- var bitpos = 0
- var bitbuf = 0
- var readPosition = 0
-
- while (readPosition < size || bitpos > 0) {
- //println("at position $readPosition with bitpos $bitpos")
- if (readPosition < size) {
- val v = getValue(encoded[readPosition++])
- bitbuf = (bitbuf shl 5) or v
- bitpos += 5
- }
- while (bitpos >= 8) {
- val d = (bitbuf ushr (bitpos - 8)) and 0xFF
- out.write(d)
- bitpos -= 8
- }
- if (readPosition == size && bitpos > 0) {
- bitbuf = (bitbuf shl (8 - bitpos)) and 0xFF
- bitpos = if (bitbuf == 0) 0 else 8
+ 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)
- return out.toByteArray()
+ val blob = out.toByteArray()
+ return blob
}
private fun getValue(chr: Char): Int {
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
index 20ec3817..cedd07b6 100644
--- a/util/src/main/kotlin/HTTP.kt
+++ b/util/src/main/kotlin/HTTP.kt
@@ -45,46 +45,4 @@ fun ApplicationRequest.getBaseUrl(): String? {
encodedPath = "/"
}
}
-}
-
-fun ApplicationCall.maybeUriComponent(name: String): String? {
- val ret: String? = this.parameters[name]
- if (ret == null) {
- logger.error("Component $name not found in URI")
- return null
- }
- return ret
-}
-
-// Extracts the Authorization:-header line, or returns null if not found.
-fun getAuthorizationRawHeader(request: ApplicationRequest): String? {
- return request.headers["Authorization"] ?: run {
- logger.error("Authorization header not found")
- return null
- }
-}
-
-/**
- * Holds the details contained in an Authorization header.
- * The content is held as it was found in the header and supposed
- * to be processed according to the scheme.
- */
-data class AuthorizationDetails(
- val scheme: String,
- val content: String
-)
-
-// Returns the authorization scheme mentioned in the Auth header,
-// or null if that could not be found.
-fun getAuthorizationDetails(authorizationHeader: String): AuthorizationDetails? {
- val split = authorizationHeader.split(" ")
- if (split.isEmpty()) {
- logger.error("malformed Authorization header: contains no space")
- return null
- }
- if (split.size != 2) {
- logger.error("malformed Authorization header: contains more than one space")
- return null
- }
- return AuthorizationDetails(scheme = split[0], content = split[1])
} \ No newline at end of file
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
index 54d6d5a3..1d7b49a4 100644
--- a/util/src/main/kotlin/strings.kt
+++ b/util/src/main/kotlin/strings.kt
@@ -83,3 +83,8 @@ fun getQueryParam(uriQueryString: String, param: String): String? {
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/test/kotlin/CryptoUtilTest.kt b/util/src/test/kotlin/CryptoUtilTest.kt
index 866866a2..82448c45 100644
--- a/util/src/test/kotlin/CryptoUtilTest.kt
+++ b/util/src/test/kotlin/CryptoUtilTest.kt
@@ -18,17 +18,13 @@
*/
import net.taler.wallet.crypto.Base32Crockford
-import net.taler.wallet.crypto.EncodingException
-import org.bouncycastle.asn1.edec.EdECObjectIdentifiers
-import org.bouncycastle.asn1.x509.AlgorithmIdentifier
-import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
+import org.junit.Ignore
import org.junit.Test
import tech.libeufin.util.*
-import java.security.KeyFactory
+import java.io.File
import java.security.KeyPairGenerator
import java.security.interfaces.RSAPrivateCrtKey
-import java.security.spec.KeySpec
-import java.security.spec.X509EncodedKeySpec
+import java.util.*
import javax.crypto.EncryptedPrivateKeyInfo
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -150,19 +146,67 @@ class CryptoUtilTest {
fun checkEddsaPublicKey() {
val givenEnc = "XZH3P6NF9DSG3BH0C082X38N2RVK1RV2H24KF76028QBKDM24BCG"
val non32bytes = "N2RVK1RV2H24KF76028QBKDM24BCG"
-
assertTrue(CryptoUtil.checkValidEddsaPublicKey(givenEnc))
assertFalse(CryptoUtil.checkValidEddsaPublicKey(non32bytes))
}
- @Test(expected = EncodingException::class)
- fun base32ToBytesTest() {
- val expectedEncoding = "C9P6YRG" // decodes to 'blob'
- assert(Base32Crockford.decode(expectedEncoding).toString(Charsets.UTF_8) == "blob")
+ @Test
+ fun base32Test() {
val validKey = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
- val obj = Base32Crockford.decode(validKey)
+ val enc = validKey
+ val obj = Base32Crockford.decode(enc)
+ assertTrue(obj.size == 32)
val roundTrip = Base32Crockford.encode(obj)
- assertEquals(validKey, roundTrip)
+ 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
@@ -171,4 +215,3 @@ class CryptoUtilTest {
assertTrue(CryptoUtil.checkpw("myinsecurepw", x))
}
}
-