diff options
34 files changed, 419 insertions, 406 deletions
@@ -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)) } } - |