2 Commits

Author SHA1 Message Date
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 172 additions and 20 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,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<String, String> 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) {

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

View File

@@ -14,17 +14,26 @@
<input type="password" id="password" autocomplete="off">
</form>
<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>
<script>
const input = document.getElementById('password');
const out = document.getElementById('liveMessage');
const leakOut = document.getElementById('leakMessage');
let t;
input.addEventListener('input', () => {
clearTimeout(t);
const val = input.value;
if (!val) { out.textContent = ''; return; }
if (!val) {
out.textContent = '';
leakOut.textContent = '';
out.className = 'message';
leakOut.className = 'message';
return;
}
t = setTimeout(async () => {
try {
@@ -33,29 +42,43 @@
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ password: val })
});
if (!resp.ok) { out.textContent = 'Błąd sprawdzania'; return; }
if (!resp.ok) {
out.textContent = 'Błąd sprawdzania';
leakOut.textContent = '';
return;
}
const data = await resp.json();
out.textContent = data.message || '';
out.className = '';
out.className = 'message';
switch (data.message) {
case "Bardzo słabe hasło":
out.classList.add("bad");
break;
case "Słabe hasło":
out.classList.add("warn");
break;
case "Średnie hasło":
out.classList.add("warn");
break;
case "Silne hasło":
out.classList.add("ok");
break;
case "Bardzo silne hasło":
out.classList.add("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;
}
}
} catch {
out.textContent = 'Błąd sieci';
leakOut.textContent = '';
}
}, 150);
});