libeufin

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

PwCrypto.kt (4060B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2024 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 package tech.libeufin.common.crypto
     21 
     22 import kotlinx.serialization.Serializable
     23 import tech.libeufin.common.*
     24 
     25 @JvmInline
     26 value class Password(val pw: String)
     27 
     28 // NIST Password Guidelines 2024
     29 private const val PASSWORD_MIN_LEN = 8
     30 private const val PASSWORD_MAX_LEN = 64
     31 
     32 /** Check if a string is a valid password */
     33 fun String.checkPw(checkQuality: Boolean): Password {
     34     if (!checkQuality) return Password(this)
     35     val len = this.length
     36     return when {
     37         len < PASSWORD_MIN_LEN -> throw conflict(
     38             "Password is too short, expect at least ${PASSWORD_MIN_LEN} characters got ${len}",
     39             TalerErrorCode.BANK_PASSWORD_TOO_SHORT
     40         )
     41         len > PASSWORD_MAX_LEN -> throw conflict(
     42             "Password is too long, expect at most ${PASSWORD_MAX_LEN} characters got ${len}",
     43             TalerErrorCode.BANK_PASSWORD_TOO_LONG
     44         )
     45         else -> Password(this)
     46     }
     47 }
     48 
     49 data class PasswordHashCheck(
     50     val match: Boolean,
     51     val outdated: Boolean
     52 )
     53 
     54 /** Cryptographic operations for secure password storage and verification */
     55 sealed interface PwCrypto {
     56     @Serializable
     57     data class Bcrypt(val cost: Int = 8): PwCrypto
     58 
     59     /** Hash [pw] using [cfg] hashing method */
     60     fun hashpw(pw: String): String {
     61         when (this) {
     62             is Bcrypt -> {
     63                 val salt = ByteArray(16).secureRand()
     64                 val pwh = CryptoUtil.bcrypt(pw, salt, cost)
     65                 return "bcrypt\$$cost\$${salt.encodeBase64()}\$${pwh.encodeBase64()}"
     66             }
     67             /* TODO Argon2id 
     68             "argon2id" -> {
     69                 require(components.size == 3) { "bad password hash format" }
     70                 val salt = components[1].decodeBase64()
     71                 val hash = components[2]
     72                 val pwh = CryptoUtil.hashArgon2id(pw, salt).encodeBase64()
     73                 PasswordHashCheck(pwh == hash, false)
     74             } */
     75         }
     76     }
     77 
     78     /** Check whether [pw] match hashed [storedPwHash] and if it should be rehashed */
     79     fun checkpw(pw: String, storedPwHash: String): PasswordHashCheck {
     80         val components = storedPwHash.split('$', limit = 5)
     81         return when (val algo = components[0]) {
     82             "sha256" -> {
     83                 require(components.size == 2) { "bad password hash format" }
     84                 val hash = components[1]
     85                 val pwh = CryptoUtil.hashStringSHA256(pw).encodeBase64()
     86                 PasswordHashCheck(pwh == hash, true)
     87             }
     88             "sha256-salted" -> {
     89                 require(components.size == 3) { "bad password hash format" }
     90                 val salt = components[1]
     91                 val hash = components[2]
     92                 val pwh = CryptoUtil.hashStringSHA256("$salt|$pw").encodeBase64()
     93                 PasswordHashCheck(pwh == hash, true)
     94             }
     95             "bcrypt" -> {
     96                 require(components.size == 4) { "bad password hash format" }
     97                 val cost = components[1].toInt()
     98                 val salt = components[2].decodeBase64()
     99                 val hash = components[3]
    100                 val pwh = CryptoUtil.bcrypt(pw, salt, cost).encodeBase64()
    101                 PasswordHashCheck(pwh == hash, !(this is Bcrypt && this.cost == cost))
    102             }
    103             else -> throw Exception("unsupported hash algo: '$algo'")
    104         }
    105     }
    106 }