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 }