2 Commits

6 changed files with 137 additions and 174 deletions

View File

@@ -1,25 +0,0 @@
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,25 +1,13 @@
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;
import java.util.*;
@Controller
public class PasswordController {
private final PasswordLeakService service;
public PasswordController(PasswordLeakService service) {
this.service = service;
}
@GetMapping("/")
public String home() {
return "password";
@@ -27,23 +15,38 @@ public class PasswordController {
@PostMapping(path = "/api/password/strength")
@ResponseBody
public Map<String, String> checkPasswordLive(@RequestParam("password") String 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"
);
public Map<String, Object> checkPasswordLive(@RequestParam("password") String password) {
Map<String, Object> response = new HashMap<>();
int score = calculateScore(password);
String strengthText = strengthText(score);
List<String> tips = generateTips(password);
response.put("strengthText", strengthText);
response.put("progress", score * 20); // pasek postępu 0100%
response.put("tips", tips);
return response;
}
private String analyze(String password) {
// ------------------------------------------
// Ocena siły hasła
// ------------------------------------------
private int calculateScore(String password) {
int score = 0;
if (password != null) {
if (password.matches(".*[a-z].*")) score++;
if (password.matches(".*[A-Z].*")) score++;
if (password.matches(".*[0-9].*")) score++;
if (password.matches(".*[@$!%*?&#].*")) score++;
if (password.length() >= 8) score++;
}
if (password == null || password.isEmpty()) return 0;
if (password.matches(".*[a-z].*")) score++;
if (password.matches(".*[A-Z].*")) score++;
if (password.matches(".*[0-9].*")) score++;
if (password.matches(".*[@$!%*?&#].*")) score++;
if (password.length() >= 12) score++;
return score;
}
private String strengthText(int score) {
return switch (score) {
case 5 -> "Bardzo silne hasło";
case 4 -> "Silne hasło";
@@ -52,4 +55,31 @@ public class PasswordController {
default -> "Bardzo słabe hasło";
};
}
// ------------------------------------------
// Stopniowe podpowiedzi
// ------------------------------------------
private List<String> generateTips(String password) {
List<String> tips = new ArrayList<>();
if (password == null || password.isEmpty()) {
tips.add("Wpisz hasło, aby rozpocząć analizę.");
return tips;
}
if (!password.matches(".*[A-Z].*"))
tips.add("Dodaj co najmniej jedną dużą litere.");
if (!password.matches(".*[0-9].*"))
tips.add("Dodaj co najmniej jedną cyfre.");
if (!password.matches(".*[@$!%*?&#].*"))
tips.add("Dodaj co najmniej jeden znnak specjalny (np. ! @ # $).");
if (password.length() < 12)
tips.add("Wydłuż hasło do co najmniej 12 znaków.");
if (tips.isEmpty())
tips.add("Świetnie! Twoje hasło jest bardzo silne.");
return tips;
}
}

View File

@@ -1,16 +0,0 @@
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

@@ -1,71 +0,0 @@
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 {
}
}
.message {
#liveMessage {
width: min(92vw, 480px);
margin: 12px auto 0;
padding: 12px 14px;
@@ -129,55 +129,54 @@ input[type="password"]:focus {
font-size: 14px;
transition: all 200ms ease;
opacity: 1;
min-height: 47px;
}
@media (prefers-color-scheme: dark) {
.message {
#liveMessage {
background: rgba(30, 41, 59, 0.75);
border-color: rgba(203, 213, 225, 0.2);
}
}
/* Hide when empty */
.message:empty {
visibility: hidden;
#liveMessage:empty {
display: none;
}
.message.ok {
#liveMessage.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) {
.message.ok {
#liveMessage.ok {
background: color-mix(in oklab, var(--ok) 18%, rgba(30, 41, 59, 0.75));
color: #86efac;
}
}
.message.warn {
#liveMessage.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) {
.message.warn {
#liveMessage.warn {
background: color-mix(in oklab, var(--warn) 18%, rgba(30, 41, 59, 0.75));
color: #fdba74;
}
}
.message.bad {
#liveMessage.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) {
.message.bad {
#liveMessage.bad {
background: color-mix(in oklab, var(--bad) 18%, rgba(30, 41, 59, 0.75));
color: #fca5a5;
}

View File

@@ -5,6 +5,32 @@
<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>
@@ -14,24 +40,33 @@
<input type="password" id="password" autocomplete="off">
</form>
<div class="message-wrapper">
<p id="liveMessage" class="message" aria-live="polite"></p>
<p id="leakMessage" class="message" aria-live="polite"></p>
<!-- Pasek siły hasła -->
<div id="strengthBarContainer">
<div id="strengthBarFill"></div>
</div>
<!-- Główny komunikat (słabe, średnie, silne itd.) -->
<p id="liveMessage" aria-live="polite"></p>
<!-- Podpowiedzi krok po kroku -->
<ul id="tipsList"></ul>
<script>
const input = document.getElementById('password');
const out = document.getElementById('liveMessage');
const leakOut = document.getElementById('leakMessage');
const bar = document.getElementById('strengthBarFill');
const tipsList = document.getElementById('tipsList');
let t;
input.addEventListener('input', () => {
clearTimeout(t);
const val = input.value;
if (!val) {
out.textContent = '';
leakOut.textContent = '';
out.className = 'message';
leakOut.className = 'message';
out.className = '';
bar.style.width = "0%";
tipsList.innerHTML = '';
return;
}
@@ -42,43 +77,54 @@
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();
out.textContent = data.message || '';
out.className = 'message';
switch (data.message) {
// Ustawiamy tekst siły hasła
out.textContent = data.strengthText || '';
out.className = '';
switch (data.strengthText) {
case "Bardzo słabe hasło":
out.classList.add("bad");
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) + "%";
// Podpowiedzi
tipsList.innerHTML = "";
(data.tips || []).forEach(tip => {
const li = document.createElement("li");
li.textContent = tip;
tipsList.appendChild(li);
});
} catch {
out.textContent = 'Błąd sieci';
leakOut.textContent = '';
}
}, 150);
});