diff options
author | Antoine A <> | 2024-03-06 09:55:03 +0100 |
---|---|---|
committer | Antoine A <> | 2024-03-06 09:55:03 +0100 |
commit | 9af4fd9bcb338f20e75254f1aaad9ee7b1c0df47 (patch) | |
tree | 871fac85827e18e240e38dc3bb15b57a0ba3fda0 | |
parent | 73c011ab2f5679bd1fb966318c53790097241540 (diff) | |
download | libeufin-9af4fd9bcb338f20e75254f1aaad9ee7b1c0df47.tar.gz libeufin-9af4fd9bcb338f20e75254f1aaad9ee7b1c0df47.tar.bz2 libeufin-9af4fd9bcb338f20e75254f1aaad9ee7b1c0df47.zip |
Improve password crypto and TAN documentation
22 files changed, 126 insertions, 59 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt index 2bda3a43..4b6a6e53 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -318,7 +318,7 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) { requireAdmin = !ctx.allowAccountDeletion ) { delete("/accounts/{USERNAME}") { - val challenge = call.challenge(db, Operation.account_delete) + val challenge = call.checkChallenge(db, Operation.account_delete) // Not deleting reserved names. if (RESERVED_ACCOUNTS.contains(username)) @@ -511,7 +511,7 @@ private fun Routing.coreBankWithdrawalApi(db: Database, ctx: BankConfig) { } post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") { val id = call.uuidParameter("withdrawal_id") - val challenge = call.challenge(db, Operation.withdrawal) + val challenge = call.checkChallenge(db, Operation.withdrawal) when (db.withdrawal.confirm(username, id, Instant.now(), challenge != null)) { WithdrawalConfirmationResult.UnknownOperation -> throw notFound( "Withdrawal operation $id not found", diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/Tan.kt index eadddc29..f0b346ca 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Tan.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/auth/Tan.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Stanisci and Dold. + * Copyright (C) 2023-2024 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 @@ -30,7 +30,13 @@ import java.security.SecureRandom import java.text.DecimalFormat import java.time.Instant - +/** + * Generate a TAN challenge for an [op] request with [body] and + * respond to the HTTP request with a TAN challenge. + * + * If [channel] and [info] are present, they will be used + * to send the TAN code, otherwise defaults will be used. + */ suspend inline fun <reified B> ApplicationCall.respondChallenge( db: Database, op: Operation, @@ -57,6 +63,10 @@ suspend inline fun <reified B> ApplicationCall.respondChallenge( ) } +/** + * Retrieve a confirmed challenge and its body for [op] from the database + * if the challenge header is defined, otherwise extract the HTTP body. + */ suspend inline fun <reified B> ApplicationCall.receiveChallenge( db: Database, op: Operation @@ -70,7 +80,10 @@ suspend inline fun <reified B> ApplicationCall.receiveChallenge( } } -suspend fun ApplicationCall.challenge( +/** + * Retrieve a confirmed challenge body for [op] if the challenge header is defined + */ +suspend fun ApplicationCall.checkChallenge( db: Database, op: Operation ): Challenge? { @@ -86,6 +99,7 @@ object Tan { private val CODE_FORMAT = DecimalFormat("00000000") private val SECURE_RNG = SecureRandom() + /** Generate a secure random TAN code */ fun genCode(): String { val rand = SECURE_RNG.nextInt(100000000) val code = CODE_FORMAT.format(rand) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt index c70627e7..7e8f4aac 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt @@ -28,6 +28,7 @@ import io.ktor.util.pipeline.* import tech.libeufin.bank.* import tech.libeufin.bank.db.Database import tech.libeufin.common.* +import tech.libeufin.common.crypto.* import java.time.Instant /** Used to store if the currently authenticated user is admin */ @@ -134,13 +135,13 @@ private suspend fun ApplicationCall.authenticateBankRequest(db: Database, requir * Returns the authenticated customer login */ private suspend fun doBasicAuth(db: Database, encoded: String): String { - val decoded = String(base64ToBytes(encoded), Charsets.UTF_8) + val decoded = String(encoded.decodeBase64(), 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") + if (!PwCrypto.checkpw(plainPassword, hash)) throw unauthorized("Bad password") return login } 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 a23d34d5..c4c5054c 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -21,6 +21,7 @@ package tech.libeufin.bank.db import tech.libeufin.bank.* import tech.libeufin.common.* +import tech.libeufin.common.crypto.* import java.time.Instant /** Data access logic for accounts */ @@ -78,7 +79,7 @@ class AccountDAO(private val db: Database) { setString(9, tanChannel?.name) setString(10, login) oneOrNull { - CryptoUtil.checkpw(password, it.getString(1)) && it.getBoolean(2) + PwCrypto.checkpw(password, it.getString(1)) && it.getBoolean(2) } } @@ -118,7 +119,7 @@ class AccountDAO(private val db: Database) { """ ).run { setString(1, login) - setString(2, CryptoUtil.hashpw(password)) + setString(2, PwCrypto.hashpw(password)) setString(3, name) setString(4, email) setString(5, phone) @@ -392,13 +393,13 @@ class AccountDAO(private val db: Database) { } if (tanRequired) { AccountPatchAuthResult.TanRequired - } else if (oldPw != null && !CryptoUtil.checkpw(oldPw, currentPwh)) { + } else if (oldPw != null && !PwCrypto.checkpw(oldPw, currentPwh)) { AccountPatchAuthResult.OldPasswordMismatch } else { val stmt = conn.prepareStatement(""" UPDATE customers SET password_hash=? where login=? """) - stmt.setString(1, CryptoUtil.hashpw(newPw)) + stmt.setString(1, PwCrypto.hashpw(newPw)) stmt.setString(2, login) stmt.executeUpdateCheck() AccountPatchAuthResult.Success diff --git a/common/src/main/kotlin/crypto/password.kt b/common/src/main/kotlin/crypto/password.kt new file mode 100644 index 00000000..2dd8af07 --- /dev/null +++ b/common/src/main/kotlin/crypto/password.kt @@ -0,0 +1,73 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.common.crypto + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.math.BigInteger +import java.security.* +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.* +import javax.crypto.* +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.PBEParameterSpec +import javax.crypto.spec.SecretKeySpec +import java.util.Base64 +import tech.libeufin.common.* + +/** Cryptographic operations for secure password storage and verification */ +object PwCrypto { + // TODO Use a real password hashing method to store passwords + + private val SECURE_RNG = SecureRandom() + + /** Hash [pw] using the strongest supported hashing method */ + fun hashpw(pw: String): String { + val saltBytes = ByteArray(8) + SECURE_RNG.nextBytes(saltBytes) + val salt = saltBytes.encodeBase64() + val pwh = CryptoUtil.hashStringSHA256("$salt|$pw").encodeBase64() + return "sha256-salted\$$salt\$$pwh" + } + + /** Check whether [pw] match hashed [storedPwHash] */ + fun checkpw(pw: String, storedPwHash: String): Boolean { + val components = storedPwHash.split('$') + when (val algo = components[0]) { + "sha256" -> { // Support legacy unsalted passwords + if (components.size != 2) throw Exception("bad password hash") + val hash = components[1] + val pwh = CryptoUtil.hashStringSHA256(pw).encodeBase64() + return pwh == hash + } + "sha256-salted" -> { + if (components.size != 3) throw Exception("bad password hash") + val salt = components[1] + val hash = components[2] + val pwh = CryptoUtil.hashStringSHA256("$salt|$pw").encodeBase64() + return pwh == hash + } + else -> throw Exception("unsupported hash algo: '$algo'") + } + } +} diff --git a/common/src/main/kotlin/CryptoUtil.kt b/common/src/main/kotlin/crypto/utils.kt index a4560c29..6bad9741 100644 --- a/common/src/main/kotlin/CryptoUtil.kt +++ b/common/src/main/kotlin/crypto/utils.kt @@ -17,7 +17,7 @@ * <http://www.gnu.org/licenses/> */ -package tech.libeufin.common +package tech.libeufin.common.crypto import org.bouncycastle.jce.provider.BouncyCastleProvider import java.io.ByteArrayOutputStream @@ -32,11 +32,13 @@ import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.PBEKeySpec import javax.crypto.spec.PBEParameterSpec import javax.crypto.spec.SecretKeySpec +import tech.libeufin.common.* /** * Helpers for dealing with cryptographic operations in EBICS / LibEuFin. */ object CryptoUtil { + // TODO split common and ebics crypto /** * RSA key pair. @@ -286,32 +288,4 @@ object CryptoUtil { fun hashStringSHA256(input: String): ByteArray { return MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8)) } - - fun hashpw(pw: String): String { - val saltBytes = ByteArray(8) - SecureRandom().nextBytes(saltBytes) - val salt = bytesToBase64(saltBytes) - val pwh = bytesToBase64(hashStringSHA256("$salt|$pw")) - return "sha256-salted\$$salt\$$pwh" - } - - fun checkpw(pw: String, storedPwHash: String): Boolean { - val components = storedPwHash.split('$') - when (val algo = components[0]) { - "sha256" -> { // Support legacy unsalted passwords - if (components.size != 2) throw Exception("bad password hash") - val hash = components[1] - val pwh = bytesToBase64(hashStringSHA256(pw)) - return pwh == hash - } - "sha256-salted" -> { - if (components.size != 3) throw Exception("bad password hash") - val salt = components[1] - val hash = components[2] - val pwh = bytesToBase64(hashStringSHA256("$salt|$pw")) - return pwh == hash - } - else -> throw Exception("unsupported hash algo: '$algo'") - } - } -} +}
\ No newline at end of file diff --git a/common/src/main/kotlin/strings.kt b/common/src/main/kotlin/strings.kt index b3608ec2..3fa5f564 100644 --- a/common/src/main/kotlin/strings.kt +++ b/common/src/main/kotlin/strings.kt @@ -52,12 +52,13 @@ fun decodeHexString(hexString: String): ByteArray { return bytes } -fun bytesToBase64(bytes: ByteArray): String { - return Base64.getEncoder().encodeToString(bytes) + +fun ByteArray.encodeBase64(): String { + return Base64.getEncoder().encodeToString(this) } -fun base64ToBytes(encoding: String): ByteArray { - return Base64.getDecoder().decode(encoding) +fun String.decodeBase64(): ByteArray { + return Base64.getDecoder().decode(this) } // used mostly in RSA math, never as amount. diff --git a/common/src/test/kotlin/CryptoUtilTest.kt b/common/src/test/kotlin/CryptoUtilTest.kt index a4778369..11d87055 100644 --- a/common/src/test/kotlin/CryptoUtilTest.kt +++ b/common/src/test/kotlin/CryptoUtilTest.kt @@ -21,6 +21,7 @@ import org.junit.Ignore import org.junit.Test import kotlin.io.path.* import tech.libeufin.common.* +import tech.libeufin.common.crypto.* import java.security.KeyPairGenerator import java.security.interfaces.RSAPrivateCrtKey import java.util.* @@ -202,7 +203,7 @@ class CryptoUtilTest { @Test fun passwordHashing() { - val x = CryptoUtil.hashpw("myinsecurepw") - assertTrue(CryptoUtil.checkpw("myinsecurepw", x)) + val x = PwCrypto.hashpw("myinsecurepw") + assertTrue(PwCrypto.checkpw("myinsecurepw", x)) } } diff --git a/ebics/src/main/kotlin/Ebics.kt b/ebics/src/main/kotlin/Ebics.kt index ab591b61..e335e502 100644 --- a/ebics/src/main/kotlin/Ebics.kt +++ b/ebics/src/main/kotlin/Ebics.kt @@ -26,7 +26,7 @@ package tech.libeufin.ebics import io.ktor.http.* import org.w3c.dom.Document -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.ebics.ebics_h004.EbicsRequest import tech.libeufin.ebics.ebics_h004.EbicsResponse import tech.libeufin.ebics.ebics_h004.EbicsTypes diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt b/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt index 8282e8b4..7ca7c6b9 100644 --- a/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt +++ b/ebics/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -1,7 +1,7 @@ package tech.libeufin.ebics.ebics_h004 import org.apache.xml.security.binding.xmldsig.SignatureType -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import java.math.BigInteger import java.security.interfaces.RSAPublicKey import java.util.* diff --git a/ebics/src/main/kotlin/ebics_h004/EbicsResponse.kt b/ebics/src/main/kotlin/ebics_h004/EbicsResponse.kt index 709e4569..373bb8f3 100644 --- a/ebics/src/main/kotlin/ebics_h004/EbicsResponse.kt +++ b/ebics/src/main/kotlin/ebics_h004/EbicsResponse.kt @@ -1,7 +1,7 @@ package tech.libeufin.ebics.ebics_h004 import org.apache.xml.security.binding.xmldsig.SignatureType -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import java.math.BigInteger import javax.xml.bind.annotation.* import javax.xml.bind.annotation.adapters.CollapsedStringAdapter diff --git a/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt b/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt index b6ca5df3..c8370d4c 100644 --- a/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt +++ b/ebics/src/main/kotlin/ebics_h005/Ebics3Request.kt @@ -1,7 +1,7 @@ package tech.libeufin.ebics.ebics_h005 import org.apache.xml.security.binding.xmldsig.SignatureType -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import java.math.BigInteger import java.security.interfaces.RSAPublicKey import java.util.* diff --git a/ebics/src/main/kotlin/ebics_h005/Ebics3Response.kt b/ebics/src/main/kotlin/ebics_h005/Ebics3Response.kt index d648f541..dd4b18a2 100644 --- a/ebics/src/main/kotlin/ebics_h005/Ebics3Response.kt +++ b/ebics/src/main/kotlin/ebics_h005/Ebics3Response.kt @@ -1,7 +1,7 @@ package tech.libeufin.ebics.ebics_h005 import org.apache.xml.security.binding.xmldsig.SignatureType -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.ebics.ebics_h004.EbicsTypes import java.math.BigInteger import javax.xml.bind.annotation.* diff --git a/ebics/src/test/kotlin/EbicsMessagesTest.kt b/ebics/src/test/kotlin/EbicsMessagesTest.kt index bf55b392..28820374 100644 --- a/ebics/src/test/kotlin/EbicsMessagesTest.kt +++ b/ebics/src/test/kotlin/EbicsMessagesTest.kt @@ -21,7 +21,7 @@ import junit.framework.TestCase.assertEquals import org.apache.xml.security.binding.xmldsig.SignatureType import org.junit.Test import org.w3c.dom.Element -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.ebics.XMLUtil import tech.libeufin.ebics.ebics_h004.* import tech.libeufin.ebics.ebics_hev.HEVResponse diff --git a/ebics/src/test/kotlin/SignatureDataTest.kt b/ebics/src/test/kotlin/SignatureDataTest.kt index 435962c3..9c5e4da6 100644 --- a/ebics/src/test/kotlin/SignatureDataTest.kt +++ b/ebics/src/test/kotlin/SignatureDataTest.kt @@ -19,7 +19,7 @@ import org.apache.xml.security.binding.xmldsig.SignatureType import org.junit.Test -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.ebics.XMLUtil import tech.libeufin.ebics.ebics_h004.EbicsRequest import tech.libeufin.ebics.ebics_h004.EbicsTypes diff --git a/ebics/src/test/kotlin/XmlUtilTest.kt b/ebics/src/test/kotlin/XmlUtilTest.kt index 078037d2..8b8f340f 100644 --- a/ebics/src/test/kotlin/XmlUtilTest.kt +++ b/ebics/src/test/kotlin/XmlUtilTest.kt @@ -20,7 +20,7 @@ import org.apache.xml.security.binding.xmldsig.SignatureType import org.junit.Assert.assertTrue import org.junit.Test -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.common.decodeBase64 import tech.libeufin.ebics.XMLUtil import tech.libeufin.ebics.XMLUtil.Companion.signEbicsResponse diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt index df2f8f15..685ad772 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt @@ -25,6 +25,7 @@ import com.github.ajalt.clikt.parameters.options.* import io.ktor.client.* import kotlinx.coroutines.runBlocking import tech.libeufin.common.* +import tech.libeufin.common.crypto.* import tech.libeufin.ebics.* import tech.libeufin.nexus.ebics.* import java.nio.file.* diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt index cf981a60..fd91cfd4 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/KeyFiles.kt @@ -31,7 +31,7 @@ import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import tech.libeufin.common.Base32Crockford -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import java.nio.file.* import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt index 7eebcea8..929246af 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -44,6 +44,7 @@ import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.utils.io.jvm.javaio.* import tech.libeufin.common.* +import tech.libeufin.common.crypto.* import tech.libeufin.ebics.* import tech.libeufin.ebics.ebics_h005.Ebics3Request import tech.libeufin.nexus.* diff --git a/nexus/src/test/kotlin/CliTest.kt b/nexus/src/test/kotlin/CliTest.kt index 6776e7f2..7e03053d 100644 --- a/nexus/src/test/kotlin/CliTest.kt +++ b/nexus/src/test/kotlin/CliTest.kt @@ -19,7 +19,7 @@ import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.testing.test -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.nexus.* import java.io.ByteArrayOutputStream import java.io.PrintStream diff --git a/nexus/src/test/kotlin/Keys.kt b/nexus/src/test/kotlin/Keys.kt index 765afcc1..826ec77e 100644 --- a/nexus/src/test/kotlin/Keys.kt +++ b/nexus/src/test/kotlin/Keys.kt @@ -18,7 +18,7 @@ */ import org.junit.Test -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.nexus.* import kotlin.io.path.Path import kotlin.io.path.deleteIfExists diff --git a/nexus/src/test/kotlin/MySerializers.kt b/nexus/src/test/kotlin/MySerializers.kt index 3aa2c803..98707948 100644 --- a/nexus/src/test/kotlin/MySerializers.kt +++ b/nexus/src/test/kotlin/MySerializers.kt @@ -19,7 +19,7 @@ import org.junit.Test import tech.libeufin.common.Base32Crockford -import tech.libeufin.common.CryptoUtil +import tech.libeufin.common.crypto.CryptoUtil import tech.libeufin.nexus.ClientPrivateKeysFile import tech.libeufin.nexus.JSON import kotlin.test.assertEquals |