From a23222a026722058b149234e0953e9302e2870a5 Mon Sep 17 00:00:00 2001 From: PatryKania Date: Thu, 20 Nov 2025 22:13:17 +0100 Subject: [PATCH] ADD: add password leaks check --- .../_11a/passmetric/PasswordController.java | 18 ++++- .../passmetric/model/PasswordLeakResult.java | 16 +++++ .../service/PasswordLeakService.java | 71 +++++++++++++++++++ src/main/resources/static/css/password.css | 21 +++--- src/main/resources/templates/password.html | 41 ++++++++--- 5 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 src/main/java/iz/_11a/passmetric/model/PasswordLeakResult.java create mode 100644 src/main/java/iz/_11a/passmetric/service/PasswordLeakService.java diff --git a/src/main/java/iz/_11a/passmetric/PasswordController.java b/src/main/java/iz/_11a/passmetric/PasswordController.java index 87f8974..353de2a 100644 --- a/src/main/java/iz/_11a/passmetric/PasswordController.java +++ b/src/main/java/iz/_11a/passmetric/PasswordController.java @@ -1,13 +1,25 @@ package iz._11a.passmetric; +import iz._11a.passmetric.model.PasswordLeakResult; +import iz._11a.passmetric.service.PasswordLeakService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.Map; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.nio.charset.StandardCharsets; +import org.springframework.web.client.RestTemplate; + @Controller public class PasswordController { + private final PasswordLeakService service; + + public PasswordController(PasswordLeakService service) { + this.service = service; + } @GetMapping("/") public String home() { return "password"; @@ -16,7 +28,11 @@ public class PasswordController { @PostMapping(path = "/api/password/strength") @ResponseBody public Map checkPasswordLive(@RequestParam("password") String password) { - return Map.of("message", analyze(password)); + PasswordLeakResult leakResult = service.checkLeakWithCount(password); + return Map.of( + "message", analyze(password), + "leaked", leakResult.isLeaked() ? "Hasło wyciekło " + String.valueOf(leakResult.getCount()) +" razy" : "Hasło nie występuje w wyciekach" + ); } private String analyze(String password) { diff --git a/src/main/java/iz/_11a/passmetric/model/PasswordLeakResult.java b/src/main/java/iz/_11a/passmetric/model/PasswordLeakResult.java new file mode 100644 index 0000000..2c7dc42 --- /dev/null +++ b/src/main/java/iz/_11a/passmetric/model/PasswordLeakResult.java @@ -0,0 +1,16 @@ +package iz._11a.passmetric.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PasswordLeakResult { + public final boolean leaked; + public final int count; + + public PasswordLeakResult(boolean leaked, int count) { + this.leaked = leaked; + this.count = count; + } +} diff --git a/src/main/java/iz/_11a/passmetric/service/PasswordLeakService.java b/src/main/java/iz/_11a/passmetric/service/PasswordLeakService.java new file mode 100644 index 0000000..feb5ded --- /dev/null +++ b/src/main/java/iz/_11a/passmetric/service/PasswordLeakService.java @@ -0,0 +1,71 @@ +package iz._11a.passmetric.service; + +import iz._11a.passmetric.model.PasswordLeakResult; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientException; + +import java.util.OptionalInt; + +@Service +public class PasswordLeakService { + + private final RestClient restClient; + + public PasswordLeakService(RestClient.Builder builder) { + this.restClient = builder + .baseUrl("https://api.pwnedpasswords.com") + .defaultHeader(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE) + .defaultHeader(HttpHeaders.USER_AGENT, "passmetric/1.0") + .build(); + } + + public PasswordLeakResult checkLeakWithCount(String password) { + if (password == null || password.isEmpty()) { + return new PasswordLeakResult(false, 0); + } + String sha1 = sha1Hex(password).toUpperCase(); + String prefix = sha1.substring(0, 5); + String suffix = sha1.substring(5); + + try { + String body = restClient.get() + .uri("/range/{prefix}", prefix) + .retrieve() + .body(String.class); + + if (body == null || body.isEmpty()) { + return new PasswordLeakResult(false, 0); + } + + for (String line : body.split("\n")) { + String[] parts = line.split(":"); + if (parts.length == 2 && parts[0].equalsIgnoreCase(suffix)) { + int count = Integer.parseInt(parts[1].trim()); + return new PasswordLeakResult(true, count); + } + } + return new PasswordLeakResult(false, 0); + } catch (RestClientException ex) { + return new PasswordLeakResult(false, 0); + } + } + + public boolean isLeaked(String password) { + return checkLeakWithCount(password).isLeaked(); + } + + private String sha1Hex(String input) { + try { + var md = java.security.MessageDigest.getInstance("SHA-1"); + byte[] digest = md.digest(input.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(digest.length * 2); + for (byte b : digest) sb.append(String.format("%02x", b)); + return sb.toString(); + } catch (java.security.NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-1 not available", e); + } + } +} diff --git a/src/main/resources/static/css/password.css b/src/main/resources/static/css/password.css index e48f06a..91e9993 100644 --- a/src/main/resources/static/css/password.css +++ b/src/main/resources/static/css/password.css @@ -117,7 +117,7 @@ input[type="password"]:focus { } } -#liveMessage { +.message { width: min(92vw, 480px); margin: 12px auto 0; padding: 12px 14px; @@ -129,54 +129,55 @@ input[type="password"]:focus { font-size: 14px; transition: all 200ms ease; opacity: 1; + min-height: 47px; } @media (prefers-color-scheme: dark) { - #liveMessage { + .message { background: rgba(30, 41, 59, 0.75); border-color: rgba(203, 213, 225, 0.2); } } /* Hide when empty */ -#liveMessage:empty { - display: none; +.message:empty { + visibility: hidden; } -#liveMessage.ok { +.message.ok { border-color: var(--ok); background: color-mix(in oklab, var(--ok) 20%, rgba(241, 245, 249, 0.85)); color: #15803d; } @media (prefers-color-scheme: dark) { - #liveMessage.ok { + .message.ok { background: color-mix(in oklab, var(--ok) 18%, rgba(30, 41, 59, 0.75)); color: #86efac; } } -#liveMessage.warn { +.message.warn { border-color: var(--warn); background: color-mix(in oklab, var(--warn) 20%, rgba(241, 245, 249, 0.85)); color: #c2410c; } @media (prefers-color-scheme: dark) { - #liveMessage.warn { + .message.warn { background: color-mix(in oklab, var(--warn) 18%, rgba(30, 41, 59, 0.75)); color: #fdba74; } } -#liveMessage.bad { +.message.bad { border-color: var(--bad); background: color-mix(in oklab, var(--bad) 20%, rgba(241, 245, 249, 0.85)); color: #b91c1c; } @media (prefers-color-scheme: dark) { - #liveMessage.bad { + .message.bad { background: color-mix(in oklab, var(--bad) 18%, rgba(30, 41, 59, 0.75)); color: #fca5a5; } diff --git a/src/main/resources/templates/password.html b/src/main/resources/templates/password.html index b4a08b8..23fdaaf 100644 --- a/src/main/resources/templates/password.html +++ b/src/main/resources/templates/password.html @@ -14,17 +14,26 @@ -

- +
+

+

+