diff --git a/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/AuthenticationService.ios.kt b/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/AuthenticationService.ios.kt index f39e2d0..b09cdc9 100644 --- a/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/AuthenticationService.ios.kt +++ b/composeApp/src/iosMain/kotlin/net/codinux/banking/ui/service/AuthenticationService.ios.kt @@ -1,19 +1,37 @@ package net.codinux.banking.ui.service +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.refTo import net.codinux.banking.ui.model.AuthenticationResult +import platform.CoreCrypto.CCCalibratePBKDF +import platform.CoreCrypto.CCKeyDerivationPBKDF +import platform.CoreCrypto.CC_SHA512 +import platform.CoreCrypto.CC_SHA512_DIGEST_LENGTH +import platform.CoreCrypto.kCCPBKDF2 +import platform.CoreCrypto.kCCPRFHmacAlgSHA256 +import platform.Security.SecRandomCopyBytes +import platform.Security.kSecRandomDefault +@OptIn(ExperimentalForeignApi::class, ExperimentalStdlibApi::class) actual object AuthenticationService { - // for iOS see e.g. - // https://medium.com/@mohamed.ma872/strengthening-mobile-app-security-pbkdf2-bcrypt-and-scrypt-for-android-and-ios-8089b0edbf76 - // https://github.com/felipeflorencio/BCryptSwift + private const val SaltLength = 16 + + private const val SaltAndHashSeparator = '$' actual fun hashPassword(password: String): String { - return password // TODO + val salt = generateRandomSalt(SaltLength) + + val derivedKey = hashPassword(password, salt) + + return toString(salt) + SaltAndHashSeparator + derivedKey } actual fun checkPassword(password: String, hashedPassword: String): Boolean { - return password == hashedPassword // TODO + val (salt, hash) = hashedPassword.split(SaltAndHashSeparator) + + return hashPassword(password, salt.hexToUByteArray()) == hash } @@ -23,4 +41,67 @@ actual object AuthenticationService { authenticationResult(AuthenticationResult(false, "Biometrics is not implemented yet")) } + + private fun generateRandomSalt(length: Int = 16): UByteArray { + val salt = UByteArray(length) + memScoped { + val status = SecRandomCopyBytes(kSecRandomDefault, length.toULong(), salt.refTo(0)) + if (status != 0) { + throw IllegalStateException("Failed to generate random salt, status: $status") + } + } + return salt + } + + private fun hashPassword(password: String, salt: UByteArray): String { + val derivedKey = deriveKeyPBKDF2(password, salt) + + return toString(derivedKey) + } + + private fun deriveKeyPBKDF2(password: String, salt: UByteArray, iterations: Int = 500_000, keyLength: Int = 32): UByteArray { + val derivedKey = UByteArray(keyLength) + + memScoped { + val status = CCKeyDerivationPBKDF( + kCCPBKDF2, + password, + password.length.toULong(), + salt.refTo(0), + salt.size.toULong(), + kCCPRFHmacAlgSHA256, + iterations.toUInt(), + derivedKey.refTo(0), + keyLength.toULong() + ) + + // Check if the derivation succeeded + if (status != 0) { + throw IllegalStateException("Key derivation failed with status $status") + } + } + + return derivedKey + } + + private fun generateSha512(password: String): UByteArray { + val data = password.encodeToByteArray() + + val hash = UByteArray(CC_SHA512_DIGEST_LENGTH) + + memScoped { + val result = CC_SHA512( + data.refTo(0), + data.size.toUInt(), + hash.refTo(0) + ) + + checkNotNull(result) { "Hash computation failed" } + } + + return hash + } + + private fun toString(bytes: UByteArray): String = bytes.toHexString() + } \ No newline at end of file