package com.peachpasswords.vault import com.peachpasswords.shared.extractDomain import java.security.MessageDigest object VaultHealth { private val SENSITIVE_PATTERN = Regex( "bank|pay|finance|invest|mail|gmail|outlook|yahoo|icloud|instagram|twitter|facebook|linkedin", RegexOption.IGNORE_CASE, ) private const val SENSITIVE_AGE_MS = 90L * 24 * 60 * 60 * 1000 private const val STANDARD_AGE_MS = 180L * 24 * 60 * 60 * 1000 data class EntryHealthScore( val entryId: String, val score: Int, val isBreach: Boolean, val isReused: Boolean, val isAged: Boolean, val isWeak: Boolean, val missingTotp: Boolean, val isSensitive: Boolean, ) data class VaultHealthReport( val vaultScore: Int, val entries: List, val breachCount: Int, val reusedCount: Int, val agedCount: Int, val weakCount: Int, ) fun buildReport(entries: List): VaultHealthReport { val loginEntries = entries.filter { it.isLogin() && !it.isDeleted() } val passwordHashes = loginEntries.mapNotNull { entry -> entry.password?.let { pw -> sha256Hex(pw) to entry.id } } val hashCounts = passwordHashes.groupBy { it.first }.mapValues { it.value.size } val now = System.currentTimeMillis() val scores = loginEntries.map { entry -> val isSensitive = entry.url?.let { domain -> val extracted = extractDomain(domain) (extracted ?: domain).let { SENSITIVE_PATTERN.containsMatchIn(it) } } ?: false val ageThreshold = if (isSensitive) SENSITIVE_AGE_MS else STANDARD_AGE_MS val isBreach = (entry.breach ?: 0) > 0 val isReused = entry.password?.let { pw -> (hashCounts[sha256Hex(pw)] ?: 0) > 1 } ?: false val isAged = entry.passwordChangedAt?.let { (now - it) > ageThreshold } ?: false val isWeak = entry.password?.let { pw -> pw.length < 10 || countCharClasses(pw) < 3 } ?: false val missingTotp = isSensitive && entry.totp.isNullOrBlank() var score = 0 if (isBreach) score += 60 if (isReused) score += 25 if (isAged) score += 15 if (isWeak) score += 20 if (missingTotp) score += 10 score = minOf(score, 100) EntryHealthScore( entryId = entry.id, score = score, isBreach = isBreach, isReused = isReused, isAged = isAged, isWeak = isWeak, missingTotp = missingTotp, isSensitive = isSensitive, ) } val totalWeight = scores.sumOf { if (it.isSensitive) 2L else 1L }.toFloat() val weightedSum = scores.sumOf { (it.score * (if (it.isSensitive) 2 else 1)).toLong() }.toFloat() val vaultScore = if (totalWeight > 0f) (weightedSum / totalWeight).toInt() else 0 return VaultHealthReport( vaultScore = vaultScore, entries = scores, breachCount = scores.count { it.isBreach }, reusedCount = scores.count { it.isReused }, agedCount = scores.count { it.isAged }, weakCount = scores.count { it.isWeak }, ) } private fun countCharClasses(password: String): Int { var classes = 0 if (password.any(Char::isLowerCase)) classes++ if (password.any(Char::isUpperCase)) classes++ if (password.any(Char::isDigit)) classes++ if (password.any { !it.isLetterOrDigit() }) classes++ return classes } private fun sha256Hex(input: String): String { val digest = MessageDigest.getInstance("SHA-256") val hash = digest.digest(input.toByteArray(Charsets.UTF_8)) return hash.joinToString("") { "%02x".format(it) } } }