11 Commits

Author SHA1 Message Date
515745451b Update README.md 2025-11-21 20:32:09 +01:00
a21693fd0b Password stats implemented
password now passed in body instead of parametr
2025-11-21 20:31:22 +01:00
12aad793c6 bruteforce implemented 2025-11-21 19:57:57 +01:00
b5de9852ca md 2025-11-21 18:34:44 +01:00
143be4a33a readme adjustment 2025-11-21 18:34:07 +01:00
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
3588e67aff Pasek siły hasła oraz podpowiedzi do stworzenia silnego hasła 2025-11-19 21:10:56 +01:00
5b719d6e33 Pasek siły hasła oraz podpowiedzi do stworzenia silnego hasła 2025-11-11 21:18:38 +01:00
6 changed files with 489 additions and 45 deletions

View File

@@ -1,22 +1,33 @@
Temat: Analizator haseł
# PassMetric
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)
### Temat projektu: Analizator haseł
Skład zespołu:
### Opis:
PassMetric 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.
### Założenia:
- [x] Sprawdza ile hasło będzie łamane przy pomocy metody Bruteforce (Andrii Solianyk)
- [x] Sprawdza czy hasło nie wyciekło (Patryk Kania)
- [x] Sprawdza jego złożoność (Hubert Salwa)
- [x] Proponuje zmiany hasła w celu poprawy jego złożoności (Hubert Salwa)
- [x] 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:
### Do statystyk:
Wypisywanie dodatkowych informacji na temat statystyk liter w tym haslie.

View File

@@ -1,25 +1,23 @@
package iz._11a.passmetric;
import iz._11a.passmetric.model.PasswordLeakResult;
import iz._11a.passmetric.service.BruteForceService;
import iz._11a.passmetric.service.PasswordLeakService;
import iz._11a.passmetric.service.PasswordStatisticsService;
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;
private final PasswordLeakService passwordLeakService;
public PasswordController(PasswordLeakService service) {
this.service = service;
public PasswordController(PasswordLeakService passwordLeakService) {
this.passwordLeakService = passwordLeakService;
}
@GetMapping("/")
public String home() {
return "password";
@@ -27,23 +25,42 @@ 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(@RequestBody String password) {
Map<String, Object> response = new HashMap<>();
int score = calculateScore(password);
String strengthText = strengthText(score);
List<String> tips = generateTips(password);
PasswordLeakResult leakResult = passwordLeakService.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");
response.put("timetohack", BruteForceService.estimateTimeToHackFormatted(password));
response.put("stats", PasswordStatisticsService.getPasswordStatistics(password));
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 == 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() >= 8) 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 +69,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 znak 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

@@ -0,0 +1,131 @@
package iz._11a.passmetric.service;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BruteForceService {
private static final long ATTEMPTS_PER_SECOND = 1_000_000_000L;
private static final int LOWERCASE_SIZE = 26; // a-z
private static final int UPPERCASE_SIZE = 26; // A-Z
private static final int DIGITS_SIZE = 10; // 0-9
private static final int SPECIAL_CHARS_SIZE = 32; // !@#$%^&*() и т.д.
private static final BigDecimal SECONDS_IN_MINUTE = BigDecimal.valueOf(60);
private static final BigDecimal SECONDS_IN_HOUR = SECONDS_IN_MINUTE.multiply(BigDecimal.valueOf(60));
private static final BigDecimal SECONDS_IN_DAY = SECONDS_IN_HOUR.multiply(BigDecimal.valueOf(24));
private static final BigDecimal SECONDS_IN_YEAR = SECONDS_IN_DAY.multiply(new BigDecimal("365.25"));
public static String estimateTimeToHackFormatted(String password) {
int charsetSize = calculateCharsetSize(password);
if (charsetSize == 0) {
return "nie można oszacować czasu dla pustego hasła";
}
int passwordLength = password.length();
BigDecimal totalCombinations = BigDecimal.valueOf(charsetSize).pow(passwordLength);
BigDecimal averageAttempts = totalCombinations.divide(BigDecimal.valueOf(2.0), RoundingMode.HALF_UP);
return formatTime(averageAttempts.divide(BigDecimal.valueOf(ATTEMPTS_PER_SECOND), 10, RoundingMode.HALF_UP).doubleValue());
}
public static String formatTime(double seconds) {
if (seconds < 1.0) {
return "mniej niż sekundę";
}
BigDecimal totalSeconds = BigDecimal.valueOf(seconds);
BigDecimal[] yearsAndRemainder = totalSeconds.divideAndRemainder(SECONDS_IN_YEAR);
long years = yearsAndRemainder[0].longValue();
if (years > 1_000_000_000L) {
return "∞ (nieskończenie długo)";
}
StringBuilder result = new StringBuilder();
if (years > 0) {
result.append(years).append(" ").append(getYearsForm(years)).append(" ");
}
BigDecimal[] daysAndRemainder = yearsAndRemainder[1].divideAndRemainder(SECONDS_IN_DAY);
long days = daysAndRemainder[0].longValue();
if (days > 0) {
result.append(days).append(" ").append(getDaysForm(days)).append(" ");
}
BigDecimal[] hoursAndRemainder = daysAndRemainder[1].divideAndRemainder(SECONDS_IN_HOUR);
long hours = hoursAndRemainder[0].longValue();
if (hours > 0) {
result.append(hours).append(" ").append(getHoursForm(hours)).append(" ");
}
BigDecimal[] minutesAndRemainder = hoursAndRemainder[1].divideAndRemainder(SECONDS_IN_MINUTE);
long minutes = minutesAndRemainder[0].longValue();
if (minutes > 0) {
result.append(minutes).append(" ").append(getMinutesForm(minutes)).append(" ");
}
long remainingSeconds = minutesAndRemainder[1].setScale(0, RoundingMode.HALF_UP).longValue();
if (remainingSeconds > 0) {
result.append(remainingSeconds).append(" ").append(getSecondsForm(remainingSeconds));
}
return result.toString().trim();
}
private static String getYearsForm(long years) {
if (years == 1) return "rok";
if (years % 10 >= 2 && years % 10 <= 4 && (years % 100 < 10 || years % 100 >= 20)) return "lata";
return "lat";
}
private static String getDaysForm(long days) {
if (days == 1) return "dzień";
return "dni";
}
private static String getHoursForm(long hours) {
if (hours == 1) return "godzina";
if (hours % 10 >= 2 && hours % 10 <= 4 && (hours % 100 < 10 || hours % 100 >= 20)) return "godziny";
return "godzin";
}
private static String getMinutesForm(long minutes) {
if (minutes == 1) return "minuta";
if (minutes % 10 >= 2 && minutes % 10 <= 4 && (minutes % 100 < 10 || minutes % 100 >= 20)) return "minuty";
return "minut";
}
private static String getSecondsForm(long seconds) {
if (seconds == 1) return "sekunda";
if (seconds % 10 >= 2 && seconds % 10 <= 4 && (seconds % 100 < 10 || seconds % 100 >= 20)) return "sekundy";
return "sekund";
}
private static int calculateCharsetSize(String password) {
int charsetSize = 0;
boolean hasLowercase = false;
boolean hasUppercase = false;
boolean hasDigits = false;
boolean hasSpecial = false;
for (char c : password.toCharArray()) {
if (Character.isLowerCase(c)) hasLowercase = true;
else if (Character.isUpperCase(c)) hasUppercase = true;
else if (Character.isDigit(c)) hasDigits = true;
else hasSpecial = true;
}
if (hasLowercase) charsetSize += LOWERCASE_SIZE;
if (hasUppercase) charsetSize += UPPERCASE_SIZE;
if (hasDigits) charsetSize += DIGITS_SIZE;
if (hasSpecial) charsetSize += SPECIAL_CHARS_SIZE;
return charsetSize;
}
}

View File

@@ -0,0 +1,45 @@
package iz._11a.passmetric.service;
import java.util.HashMap;
import java.util.Map;
public class PasswordStatisticsService {
public static Map<String, Object> getPasswordStatistics(String password) {
Map<String, Object> stats = new HashMap<>();
if (password == null || password.isEmpty()) {
stats.put("length", 0);
stats.put("lowercaseCount", 0);
stats.put("uppercaseCount", 0);
stats.put("digitCount", 0);
stats.put("specialCount", 0);
return stats;
}
int lowercaseCount = 0;
int uppercaseCount = 0;
int digitCount = 0;
int specialCount = 0;
for (char c : password.toCharArray()) {
if (Character.isLowerCase(c)) {
lowercaseCount++;
} else if (Character.isUpperCase(c)) {
uppercaseCount++;
} else if (Character.isDigit(c)) {
digitCount++;
} else {
specialCount++;
}
}
stats.put("length", password.length());
stats.put("lowercaseCount", lowercaseCount);
stats.put("uppercaseCount", uppercaseCount);
stats.put("digitCount", digitCount);
stats.put("specialCount", specialCount);
return stats;
}
}

View File

@@ -47,6 +47,7 @@ body {
display: grid;
place-items: center;
padding: 24px;
align-content: space-evenly;
}
h1 {
@@ -59,7 +60,7 @@ h1 {
}
form {
width: min(92vw, 480px);
width: min(92vw, 540px);
background: var(--panel);
border: 1px solid var(--border);
padding: 20px 18px;
@@ -118,7 +119,7 @@ input[type="password"]:focus {
}
.message {
width: min(92vw, 480px);
width: min(92vw, 540px);
margin: 12px auto 0;
padding: 12px 14px;
border-radius: 12px;
@@ -182,3 +183,104 @@ input[type="password"]:focus {
color: #fca5a5;
}
}
#strengthBarContainer {
width: min(92vw, 540px);
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, 540px);
margin: 10px auto 0;
padding-left: 20px;
font-size: 14px;
color: var(--text);
list-style: disc;
}
ul#tipsList.ok {
padding-left: 14px;
list-style: none;
color: #86efac;
}
ul#tipsList li {
margin: 0.3rem 0.5rem;
}
/*stats*/
.stats-container {
width: min(92vw, 540px);
margin: 16px auto 0;
padding: 16px;
border-radius: 12px;
border: 1.5px solid var(--border);
background: var(--panel);
backdrop-filter: blur(12px);
display: none;
}
.stats-title {
margin: 0 0 12px;
font-size: 15px;
font-weight: 600;
color: var(--text);
text-align: center;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 10px;
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(148, 163, 184, 0.1);
border-radius: 8px;
transition: background 160ms ease;
}
@media (prefers-color-scheme: dark) {
.stat-item {
background: rgba(51, 65, 85, 0.4);
}
}
.stat-item:hover {
background: rgba(148, 163, 184, 0.15);
}
@media (prefers-color-scheme: dark) {
.stat-item:hover {
background: rgba(51, 65, 85, 0.6);
}
}
.stat-label {
font-size: 13px;
color: var(--muted);
font-weight: 500;
}
.stat-value {
font-size: 14px;
font-weight: 700;
color: var(--primary);
}

View File

@@ -14,24 +14,77 @@
<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>
<div id="statsContainer" class="stats-container">
<h3 class="stats-title">Statystyki hasła</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Długość:</span>
<span class="stat-value" id="statLength">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Małe litery:</span>
<span class="stat-value" id="statLowercase">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Wielkie litery:</span>
<span class="stat-value" id="statUppercase">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Cyfry:</span>
<span class="stat-value" id="statDigits">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Znaki specjalne:</span>
<span class="stat-value" id="statSpecial">-</span>
</div>
</div>
</div>
<div class="message-wrapper">
<p id="liveMessage" class="message" aria-live="polite"></p>
<p id="leakMessage" class="message" aria-live="polite"></p>
<p id="timeToHackMessage" class="message" aria-live="polite"></p>
<ul id="tipsList" class="message" aria-live="polite">
</ul>
</div>
<script>
const input = document.getElementById('password');
const out = document.getElementById('liveMessage');
const bar = document.getElementById('strengthBarFill');
const barContainer = document.getElementById('strengthBarContainer');
const tipsList = document.getElementById('tipsList');
const leakOut = document.getElementById('leakMessage');
const timeToHackOut = document.getElementById('timeToHackMessage');
const statsContainer = document.getElementById('statsContainer');
// ukryj przy pierwszym załadowaniu strony
tipsList.style.display = 'none';
barContainer.style.display = 'none';
let t;
input.addEventListener('input', () => {
clearTimeout(t);
const val = input.value;
if (!val) {
out.textContent = '';
leakOut.textContent = '';
out.className = 'message';
leakOut.textContent = '';
leakOut.className = 'message';
bar.style.width = "0%";
barContainer.style.display = 'none';
tipsList.innerHTML = "";
tipsList.style.display = 'none';
timeToHackOut.textContent = '';
statsContainer.style.display = 'none';
return;
}
@@ -39,8 +92,8 @@
try {
const resp = await fetch('/api/password/strength', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ password: val })
headers: {'Content-Type': 'text/plain'},
body: val,
});
if (!resp.ok) {
out.textContent = 'Błąd sprawdzania';
@@ -48,19 +101,23 @@
return;
}
const data = await resp.json();
out.textContent = data.message || '';
out.textContent = data.strengthText || '';
out.className = 'message';
switch (data.message) {
switch (data.strengthText) {
case "Bardzo słabe hasło":
out.classList.add("bad");
bar.style.background = "var(--bad)";
break;
case "Słabe hasło":
case "Średnie hasło":
out.classList.add("warn");
bar.style.background = "var(--warn)";
break;
case "Silne hasło":
case "Bardzo silne hasło":
out.classList.add("ok");
bar.style.background = "var(--ok)";
break;
}
@@ -76,12 +133,66 @@
break;
}
}
bar.style.width = (data.progress || 0) + "%";
barContainer.style.display = 'block';
// Podpowiedzi
tipsList.innerHTML = "";
if (data.tips && data.tips.length > 0) {
if (data.tips.length === 1 && data.tips[0] === "Świetnie! Twoje hasło jest bardzo silne.") {
tipsList.textContent = data.tips[0];
tipsList.classList.add("ok");
tipsList.style.display = 'block';
} else {
tipsList.classList.remove("ok");
data.tips.forEach(tip => {
const li = document.createElement("li");
li.textContent = tip;
tipsList.appendChild(li);
});
tipsList.style.display = 'block';
}
} else {
tipsList.style.display = 'none';
}
// TimeToHack
timeToHackOut.textContent = "Zostanie złamane w: " + (data.timetohack || '');
timeToHackOut.className = 'message';
if (data.timetohack && data.timetohack.includes("nieskończenie długo")) {
timeToHackOut.classList.add("ok");
} else if (data.timetohack && data.timetohack.includes("mniej")) {
timeToHackOut.classList.add("bad")
} else {
timeToHackOut.classList.remove("ok");
}
// stats
if (data.stats) {
document.getElementById('statLength').textContent = data.stats.length || 0;
document.getElementById('statLowercase').textContent =
data.stats.lowercaseCount || 0;
document.getElementById('statUppercase').textContent =
data.stats.uppercaseCount || 0;
document.getElementById('statDigits').textContent =
data.stats.digitCount || 0;
document.getElementById('statSpecial').textContent =
data.stats.specialCount || 0;
statsContainer.style.display = 'block';
} else {
statsContainer.style.display = 'none';
}
} catch {
out.textContent = 'Błąd sieci';
leakOut.textContent = '';
}
}, 150);
});
</script>
</body>
</html>