mirror of
https://github.com/bitwarden/android.git
synced 2024-11-26 19:36:18 +03:00
Add 'Have I Been Pwnd' method that returns a breach count (#281)
This commit is contained in:
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 }
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.HaveIBeenPwnedApi
|
|||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -15,14 +16,38 @@ class HaveIBeenPwnedServiceTest : BaseServiceTest() {
|
|||
private val service = HaveIBeenPwnedServiceImpl(haveIBeenPwnedApi)
|
||||
|
||||
@Test
|
||||
fun `when service returns failure should return failure`() = runTest {
|
||||
fun `getPasswordBreachCount should return failure when service returns failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
assertTrue(service.getPasswordBreachCount(PWNED_PASSWORD).isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasswordBreachCount should return breach count when password is in response`() =
|
||||
runTest {
|
||||
val response = MockResponse().setBody(HIBP_RESPONSE)
|
||||
server.enqueue(response)
|
||||
val result = service.getPasswordBreachCount(PWNED_PASSWORD)
|
||||
assertEquals(36865, result.getOrThrow())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPasswordBreachCount should returns 0 when password is not in response`() = runTest {
|
||||
val response = MockResponse().setBody(HIBP_RESPONSE)
|
||||
server.enqueue(response)
|
||||
val result = service.getPasswordBreachCount("testpassword")
|
||||
assertEquals(0, result.getOrThrow())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasPasswordBeenBreached should return failure when service returns failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
assertTrue(service.hasPasswordBeenBreached(PWNED_PASSWORD).isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when given password is in response returns true`() = runTest {
|
||||
fun `hasPasswordBeenBreached should return true when password is in response`() = runTest {
|
||||
val response = MockResponse().setBody(HIBP_RESPONSE)
|
||||
server.enqueue(response)
|
||||
val result = service.hasPasswordBeenBreached(PWNED_PASSWORD)
|
||||
|
@ -30,7 +55,7 @@ class HaveIBeenPwnedServiceTest : BaseServiceTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `when given password is not in response returns false`() = runTest {
|
||||
fun `hasPasswordBeenBreached should return false when password is not in response`() = runTest {
|
||||
val response = MockResponse().setBody(HIBP_RESPONSE)
|
||||
server.enqueue(response)
|
||||
val result = service.hasPasswordBeenBreached("testpassword")
|
||||
|
|
Loading…
Reference in a new issue