6 Commits

Author SHA1 Message Date
22f58ff478 literowka 2025-11-21 18:30:24 +01:00
cf37859078 Merge branch 'feature/check-password-leaks'
# Conflicts:
#	src/main/java/iz/_11a/passmetric/PasswordController.java
#	src/main/resources/templates/password.html
2025-11-21 17:52:08 +01:00
409e4ae98f css migration 2025-11-21 17:25:30 +01:00
978974f012 Merge remote-tracking branch 'origin/password-feedback' 2025-11-21 10:54:45 +01:00
a23222a026 ADD: add password leaks check 2025-11-20 22:13:17 +01:00
a1fdfa0a41 added readme 2025-11-16 09:01:18 +01:00
6 changed files with 181 additions and 51 deletions

25
README.md Normal file
View File

@@ -0,0 +1,25 @@
Temat: Analizator haseł
Założenia:
- Sprawdza ile hasło będzie łamane przy pomocy metody Bruteforce (Andrii Solianyk)
- Sprawdza czy hasło nie wyciekło (Patryk Kania)
- Sprawdza jego złożoność (Hubert Salwa)
- Proponuje zmiany hasła w celu poprawy jego złożoności (Hubert Salwa)
- Statystyki złożoności (Andrii Solianyk)
Skład zespołu:
- Andrii Solianyk
- Patryk Kania
- Hubert Salwa
Weryfikator Siły Hasła (Password Validator) to narzędzie służące do kompleksowej oceny bezpieczeństwa wprowadzonych haseł. Aplikacja natychmiastowo analizuje hasło pod kątem siły, sprawdzając takie kryteria jak długość, różnorodność znaków (duże/małe litery, cyfry, symbole) i brak typowych słów słownikowych. Ponadto, kluczową funkcjonalnością jest weryfikacja, czy hasło nie wyciekło wcześniej w wyniku naruszeń danych. Ostatecznym celem aplikacji jest dostarczenie użytkownikowi natychmiastowej informacji zwrotnej i wskazówek, które pomogą mu stworzyć silne i unikalne zabezpieczenie konta.
Do statystyk:
Wypisywanie dodatkowych informacji na temat statystyk liter w tym haslie.
Raport przygotowany w formie dokumentu, rozbudowane sprawozdanie
- wstęp teoretyczny
- implementacja

View File

@@ -1,5 +1,7 @@
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.*;
@@ -8,6 +10,11 @@ import java.util.*;
@Controller
public class PasswordController {
private final PasswordLeakService service;
public PasswordController(PasswordLeakService service) {
this.service = service;
}
@GetMapping("/")
public String home() {
return "password";
@@ -22,10 +29,12 @@ public class PasswordController {
int score = calculateScore(password);
String strengthText = strengthText(score);
List<String> tips = generateTips(password);
PasswordLeakResult leakResult = service.checkLeakWithCount(password);
response.put("strengthText", strengthText);
response.put("progress", score * 20); // pasek postępu 0100%
response.put("tips", tips);
response.put("leaked", leakResult.isLeaked() ? "Hasło wyciekło " + leakResult.getCount() +" razy" : "Hasło nie występuje w wyciekach");
return response;
}
@@ -73,7 +82,7 @@ public class PasswordController {
if (!password.matches(".*[0-9].*"))
tips.add("Dodaj co najmniej jedną cyfre.");
if (!password.matches(".*[@$!%*?&#].*"))
tips.add("Dodaj co najmniej jeden znnak specjalny (np. ! @ # $).");
tips.add("Dodaj co najmniej jeden znak specjalny (np. ! @ # $).");
if (password.length() < 12)
tips.add("Wydłuż hasło do co najmniej 12 znaków.");

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -117,7 +117,7 @@ input[type="password"]:focus {
}
}
#liveMessage {
.message {
width: min(92vw, 480px);
margin: 12px auto 0;
padding: 12px 14px;
@@ -129,55 +129,80 @@ 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;
}
}
#strengthBarContainer {
width: min(92vw, 480px);
margin: 10px auto 0;
height: 10px;
background: #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
#strengthBarFill {
height: 100%;
width: 0;
background: var(--bad);
transition: width 200ms ease, background-color 200ms ease;
}
ul#tipsList {
width: min(92vw, 480px);
margin: 10px auto 0;
padding-left: 20px;
font-size: 14px;
color: var(--text);
}

View File

@@ -5,32 +5,6 @@
<title>PassMetric</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" th:href="@{/css/password.css}">
<style>
/* Pasek siły hasła dopasowany do Twojego stylu */
#strengthBarContainer {
width: min(92vw, 480px);
margin: 10px auto 0;
height: 10px;
background: #e2e8f0;
border-radius: 8px;
overflow: hidden;
}
#strengthBarFill {
height: 100%;
width: 0%;
background: var(--bad);
transition: width 200ms ease, background-color 200ms ease;
}
ul#tipsList {
width: min(92vw, 480px);
margin: 10px auto 0;
padding-left: 20px;
font-size: 14px;
color: var(--text);
}
</style>
</head>
<body>
<h1>Sprawdź siłę hasła</h1>
@@ -45,8 +19,10 @@
<div id="strengthBarFill"></div>
</div>
<!-- Główny komunikat (słabe, średnie, silne itd.) -->
<p id="liveMessage" aria-live="polite"></p>
<div class="message-wrapper">
<p id="liveMessage" class="message" aria-live="polite"></p>
<p id="leakMessage" class="message" aria-live="polite"></p>
</div>
<!-- Podpowiedzi krok po kroku -->
<ul id="tipsList"></ul>
@@ -56,6 +32,7 @@
const out = document.getElementById('liveMessage');
const bar = document.getElementById('strengthBarFill');
const tipsList = document.getElementById('tipsList');
const leakOut = document.getElementById('leakMessage');
let t;
input.addEventListener('input', () => {
@@ -64,7 +41,9 @@
if (!val) {
out.textContent = '';
out.className = '';
out.className = 'message';
leakOut.textContent = '';
leakOut.className = 'message';
bar.style.width = "0%";
tipsList.innerHTML = '';
return;
@@ -77,17 +56,14 @@
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ password: val })
});
if (!resp.ok) {
out.textContent = 'Błąd sprawdzania';
leakOut.textContent = '';
return;
}
const data = await resp.json();
// Ustawiamy tekst siły hasła
out.textContent = data.strengthText || '';
out.className = '';
out.className = 'message';
switch (data.strengthText) {
case "Bardzo słabe hasło":
@@ -95,23 +71,30 @@
bar.style.background = "var(--bad)";
break;
case "Słabe hasło":
out.classList.add("warn");
bar.style.background = "var(--warn)";
break;
case "Średnie hasło":
out.classList.add("warn");
bar.style.background = "var(--warn)";
break;
case "Silne hasło":
out.classList.add("ok");
bar.style.background = "var(--ok)";
break;
case "Bardzo silne hasło":
out.classList.add("ok");
bar.style.background = "var(--ok)";
break;
}
if (data.leaked) {
leakOut.textContent = data.leaked || '';
leakOut.className = 'message';
switch (data.leaked) {
case "Hasło wyciekło!":
leakOut.classList.add("bad");
break;
case "Hasło nie występuje w wyciekach":
leakOut.classList.add("ok");
break;
}
}
// Pasek postępu (0100%)
bar.style.width = (data.progress || 0) + "%";
@@ -125,6 +108,7 @@
} catch {
out.textContent = 'Błąd sieci';
leakOut.textContent = '';
}
}, 150);
});