5 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
6 changed files with 386 additions and 35 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,7 +1,9 @@
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.*;
@@ -10,11 +12,12 @@ 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";
@@ -22,19 +25,21 @@ public class PasswordController {
@PostMapping(path = "/api/password/strength")
@ResponseBody
public Map<String, Object> checkPasswordLive(@RequestParam("password") String password) {
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 = service.checkLeakWithCount(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;
}

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;
@@ -184,7 +185,7 @@ input[type="password"]:focus {
}
#strengthBarContainer {
width: min(92vw, 480px);
width: min(92vw, 540px);
margin: 10px auto 0;
height: 10px;
background: #e2e8f0;
@@ -200,9 +201,86 @@ input[type="password"]:focus {
}
ul#tipsList {
width: min(92vw, 480px);
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

@@ -19,20 +19,55 @@
<div id="strengthBarFill"></div>
</div>
<div class="message-wrapper">
<p id="liveMessage" class="message" aria-live="polite"></p>
<p id="leakMessage" class="message" aria-live="polite"></p>
<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>
<!-- Podpowiedzi krok po kroku -->
<ul id="tipsList"></ul>
<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', () => {
@@ -45,7 +80,11 @@
leakOut.textContent = '';
leakOut.className = 'message';
bar.style.width = "0%";
tipsList.innerHTML = '';
barContainer.style.display = 'none';
tipsList.innerHTML = "";
tipsList.style.display = 'none';
timeToHackOut.textContent = '';
statsContainer.style.display = 'none';
return;
}
@@ -53,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';
@@ -95,16 +134,57 @@
}
}
// Pasek postępu (0100%)
bar.style.width = (data.progress || 0) + "%";
barContainer.style.display = 'block';
// Podpowiedzi
tipsList.innerHTML = "";
(data.tips || []).forEach(tip => {
const li = document.createElement("li");
li.textContent = tip;
tipsList.appendChild(li);
});
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';
@@ -112,6 +192,7 @@
}
}, 150);
});
</script>
</body>
</html>