libeufin

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

commit 4dbd3ae0898c120666329b9b90edea7dc73e777d
parent 77b6f70ceb5f811237c5ccde6e6b6b0cc6038136
Author: Florian Dold <florian.dold@gmail.com>
Date:   Tue, 19 May 2020 14:16:05 +0530

CLI for superuser management, abstract PW hashing algo

Diffstat:
Mnexus/build.gradle | 6++++++
Mnexus/src/main/kotlin/tech/libeufin/nexus/DB.kt | 5+++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt | 24+++++++++++++++---------
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 45+++++++++++++++++++++++++++++++++++++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt | 2+-
Mnexus/src/test/kotlin/authentication.kt | 24+++---------------------
Mutil/src/main/kotlin/CryptoUtil.kt | 19+++++++++++++++++++
Mutil/src/test/kotlin/CryptoUtilTest.kt | 6++++++
8 files changed, 96 insertions(+), 35 deletions(-)

diff --git a/nexus/build.gradle b/nexus/build.gradle @@ -68,6 +68,8 @@ dependencies { implementation 'org.apache.santuario:xmlsec:2.1.4' implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.20' + implementation("com.github.ajalt:clikt:2.7.0") + testImplementation group: 'junit', name: 'junit', version: '4.12' } @@ -85,4 +87,8 @@ jar { manifest { attributes "Main-Class": "tech.libeufin.nexus.MainKt" } +} + +run { + standardInput = System.in } \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -140,6 +140,7 @@ object PreparedPaymentsTable : IdTable<String>() { /** never really used, but it makes sure the user always exists */ val nexusUser = reference("nexusUser", NexusUsersTable) } + class PreparedPaymentEntity(id: EntityID<String>) : Entity<String>(id) { companion object : EntityClass<String, PreparedPaymentEntity>(PreparedPaymentsTable) var paymentId by PreparedPaymentsTable.paymentId @@ -209,12 +210,12 @@ class EbicsSubscriberEntity(id: EntityID<String>) : Entity<String>(id) { object NexusUsersTable : IdTable<String>() { override val id = varchar("id", ID_MAX_LENGTH).entityId().primaryKey() - val password = blob("password").nullable() + val passwordHash = text("password") } class NexusUserEntity(id: EntityID<String>) : Entity<String>(id) { companion object : EntityClass<String, NexusUserEntity>(NexusUsersTable) - var password by NexusUsersTable.password + var passwordHash by NexusUsersTable.passwordHash } object BankAccountMapsTable : IntIdTable() { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt @@ -11,7 +11,6 @@ import tech.libeufin.util.Amount import tech.libeufin.util.CryptoUtil import tech.libeufin.util.EbicsClientSubscriberDetails import tech.libeufin.util.base64ToBytes -import javax.sql.rowset.serial.SerialBlob import java.util.Random import tech.libeufin.util.ebics_h004.EbicsTypes import java.security.interfaces.RSAPublicKey @@ -440,7 +439,7 @@ fun extractNexusUser(param: String?): NexusUserEntity { * and returns a pair made of username and hashed (sha256) password. The hashed value * will then be compared with the one kept into the database. */ -fun extractUserAndHashedPassword(authorizationHeader: String): Pair<String, ByteArray> { +fun extractUserAndHashedPassword(authorizationHeader: String): Pair<String, String> { logger.debug("Authenticating: $authorizationHeader") val (username, password) = try { val split = authorizationHeader.split(" ") @@ -452,7 +451,7 @@ fun extractUserAndHashedPassword(authorizationHeader: String): Pair<String, Byte "invalid Authorization:-header received" ) } - return Pair(username, CryptoUtil.hashStringSHA256(password)) + return Pair(username, password) } /** @@ -466,13 +465,20 @@ fun authenticateRequest(authorization: String?): String { val headerLine = if (authorization == null) throw NexusError( HttpStatusCode.BadRequest, "Authentication:-header line not found" ) else authorization - val subscriber = transaction { - val (user, pass) = extractUserAndHashedPassword(headerLine) - NexusUserEntity.find { - NexusUsersTable.id eq user and (NexusUsersTable.password eq SerialBlob(pass)) + val nexusUserId = transaction { + val (username, password) = extractUserAndHashedPassword(headerLine) + val user = NexusUserEntity.find { + NexusUsersTable.id eq username }.firstOrNull() - } ?: throw NexusError(HttpStatusCode.Forbidden, "Wrong password") - return subscriber.id.value + if (user == null) { + throw NexusError(HttpStatusCode.Unauthorized, "Unknown user") + } + if (!CryptoUtil.checkpw(password, user.passwordHash)) { + throw NexusError(HttpStatusCode.Forbidden, "Wrong password") + } + return@transaction user.id.value + } + return nexusUserId } fun authenticateAdminRequest(authorization: String?): String { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -19,6 +19,11 @@ package tech.libeufin.nexus +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.prompt import com.google.gson.Gson import com.google.gson.JsonObject import io.ktor.application.ApplicationCallPipeline @@ -52,6 +57,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.event.Level import tech.libeufin.util.* +import tech.libeufin.util.CryptoUtil.hashpw import tech.libeufin.util.ebics_h004.HTDResponseOrderData import java.text.DateFormat import java.util.zip.InflaterInputStream @@ -157,9 +163,44 @@ suspend fun handleEbicsSendMSG( return response } +class NexusCommand: CliktCommand() { + override fun run() = Unit +} + +class Serve: CliktCommand("Run nexus HTTP server") { + override fun run() { + serverMain() + } +} + +class Superuser: CliktCommand("Add superuser or change pw") { + val username by argument() + val password by option().prompt(requireConfirmation = true, hideInput = true) + override fun run() { + dbCreateTables() + transaction { + val hashedPw = hashpw(password) + val user = NexusUserEntity.findById(username) + if (user == null) { + NexusUserEntity.new(username) { + this.passwordHash = hashedPw + } + } else { + user.passwordHash = hashedPw + } + } + } +} + +fun main(args: Array<String>) { + NexusCommand() + .subcommands(Serve(), Superuser()) + .main(args) +} + @ExperimentalIoApi @KtorExperimentalAPI -fun main() { +fun serverMain() { dbCreateTables() val client = HttpClient() { expectSuccess = false // this way, it does not throw exceptions on != 200 responses. @@ -249,7 +290,7 @@ fun main() { ) transaction { NexusUserEntity.new(body.username) { - password = SerialBlob(CryptoUtil.hashStringSHA256(body.password)) + passwordHash = hashpw(body.password) } } call.respondText( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/MainDeprecated.kt @@ -206,7 +206,7 @@ fun main() { val body = call.receive<NexusUserRequest>() transaction { NexusUserEntity.new(id = newUserId) { - password = if (body.password != null) { + passwordHash = if (body.password != null) { SerialBlob(CryptoUtil.hashStringSHA256(body.password)) } else { logger.debug("No password set for $newUserId") diff --git a/nexus/src/test/kotlin/authentication.kt b/nexus/src/test/kotlin/authentication.kt @@ -1,37 +1,19 @@ package tech.libeufin.nexus -import org.apache.commons.compress.utils.IOUtils import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.SchemaUtils -import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test +import junit.framework.TestCase.assertEquals import tech.libeufin.util.CryptoUtil import javax.sql.rowset.serial.SerialBlob class AuthenticationTest { @Test - fun dbInvolvingTest() { - Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1", driver = "org.h2.Driver") - transaction { - SchemaUtils.create(NexusUsersTable) - NexusUserEntity.new(id = "username") { - password = SerialBlob(CryptoUtil.hashStringSHA256("password")) - } - // base64 of "username:password" == "dXNlcm5hbWU6cGFzc3dvcmQ=" - val hashedPass= extractUserAndHashedPassword( - "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" - ).second - val row = NexusUserEntity.findById("username") - assert(row?.password == SerialBlob(hashedPass)) - } - } - - @Test fun basicAuthHeaderTest() { - val hashedPass = extractUserAndHashedPassword( + val pass = extractUserAndHashedPassword( "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" ).second - assert(CryptoUtil.hashStringSHA256("password").contentEquals(hashedPass)) + assertEquals("password", pass); } } \ No newline at end of file diff --git a/util/src/main/kotlin/CryptoUtil.kt b/util/src/main/kotlin/CryptoUtil.kt @@ -300,4 +300,23 @@ object CryptoUtil { fun hashStringSHA256(input: String): ByteArray { return MessageDigest.getInstance("SHA-256").digest(input.toByteArray(Charsets.UTF_8)) } + + fun hashpw(pw: String): String { + val pwh = bytesToBase64(CryptoUtil.hashStringSHA256(pw)) + return "sha256\$$pwh" + } + + fun checkpw(pw: String, storedPwHash: String): Boolean { + val idx = storedPwHash.indexOf("\$") + if (idx <= 0) { + throw Exception("bad password hash") + } + val algo = storedPwHash.substring(0, idx) + if (algo != "sha256") { + throw Exception("unsupported hash algo") + } + val rest = storedPwHash.substring(idx + 1) + val pwh = bytesToBase64(CryptoUtil.hashStringSHA256(pw)) + return pwh == rest + } } diff --git a/util/src/test/kotlin/CryptoUtilTest.kt b/util/src/test/kotlin/CryptoUtilTest.kt @@ -160,5 +160,11 @@ class CryptoUtilTest { val expectedEncoding = "C9P6YRG" assert(Base32Crockford.decode(expectedEncoding).toString(Charsets.UTF_8) == "blob") } + + @Test + fun passwordHashing() { + val x = CryptoUtil.hashpw("myinsecurepw") + assertTrue(CryptoUtil.checkpw("myinsecurepw", x)) + } }