Add 'Have I Been Pwnd' method that returns a breach count (#281)

This commit is contained in:
David Perez
2024-06-20 17:08:07 +01:00
committed by Álison Fernandes
parent dfc653b72e
commit 912d6b62fe
3 changed files with 59 additions and 15 deletions
@@ -5,6 +5,13 @@ package com.x8bit.bitwarden.data.auth.datasource.network.service
*/
interface HaveIBeenPwnedService {
/**
* Check to see if the given password has been breached. Returns the number of breaches.
*/
suspend fun getPasswordBreachCount(
password: String,
): Result<Int>
/**
* Check to see if the given password has been breached. Returns true if breached.
*/
@@ -6,7 +6,7 @@ import java.security.MessageDigest
class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenPwnedService {
@Suppress("MagicNumber")
override suspend fun hasPasswordBeenBreached(password: String): Result<Boolean> {
override suspend fun getPasswordBreachCount(password: String): Result<Int> {
// Hash the password:
val hashedPassword = MessageDigest
.getInstance("SHA-1")
@@ -18,19 +18,31 @@ class HaveIBeenPwnedServiceImpl(private val api: HaveIBeenPwnedApi) : HaveIBeenP
return api
.fetchBreachedPasswords(hashPrefix = hashPrefix)
.mapCatching { responseBody ->
val allPwnedPasswords = responseBody.string()
responseBody.string()
// First split the response by newline: each hashed password is on a new line.
.split("\r\n")
.map { pwnedSuffix ->
// Then remove everything after the ":", since we only want the pwned hash:
// Before: 20d61603aba324bf08799896110561f05e1ad3be:12
// After: 20d61603aba324bf08799896110561f05e1ad3be
pwnedSuffix.substring(0, endIndex = pwnedSuffix.indexOf(":"))
.split("\r\n", "\r", "\n")
.associate { pwnedSuffix ->
// Then split everything on the ":", since we want to compare the pwned
// hash but we also want to return the count:
// Pattern: <pwnd_hash_suffix>:<breach_count>
// Example: 20d61603aba324bf08799896110561f05e1ad3be:12
val split = pwnedSuffix.split(":")
split[0] to split[1]
}
// Then see if any of those passwords match our full password hash:
allPwnedPasswords.any { pwnedSuffix ->
(hashPrefix + pwnedSuffix).equals(hashedPassword, ignoreCase = true)
}
.entries
.find { (pwnedSuffix, _) ->
// Then see if any of those passwords match our full password hash:
(hashPrefix + pwnedSuffix).equals(hashedPassword, ignoreCase = true)
}
// If we found a match, return the value as this is the number of breaches.
?.value
?.toIntOrNull()
?: 0
}
}
override suspend fun hasPasswordBeenBreached(
password: String,
): Result<Boolean> = getPasswordBreachCount(password)
.map { numberOfBreaches -> numberOfBreaches > 0 }
}