Przechowywanie hasła w postaci tablicy char[] zamiast String

0

Cześć,
czytałem, że przechowywanie hasła w postaci Stringa to zły pomysł. Trochę myślałem o tym, jak zaimplementować to w kodzie, napisałem taki przykład.

Request rejestracji ma normalnie pole password jako String.

public record RegisterRequest(String username,  String email, String password) {}

W kontrolerze zamieniam hasło na tablicę char[] i przekazuję ją dalej. Za każdym razem, gdy hasło jest używane, czyszczę tablicę, tak jak w kodzie poniżej. Czy o to właśnie chodzi?.
Kontroller:

@PostMapping("/register")
public ResponseEntity<ApiResponse> register(RegisterRequest request) {
    char[] passwordChars = request.password().toCharArray();

    ApiResponse registrationResponse = registration.registerUser(request.username(), request.email(), passwordChars);

    Arrays.fill(passwordChars, '\0');

    return new ResponseEntity<>(registrationResponse, HttpStatus.CREATED);
}

Przykłądowo klasa odpowiadająca za walidajcę:

public void validate(String username, String email, char[] password) {

String stringPassword = new String(password);

    if (!stringPassword.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$")) {
        Arrays.fill(password, '\0');
        throw new CredentialValidationException("Invalid password format. The password must contain at least " +
                "8 characters, including uppercase letters, lowercase letters, numbers, and special characters.");
    }
    Arrays.fill(password, '\0');
}

Jakaś klasa odpowiadająca za tworzenie usera:

public class UserCreator {

    private final PasswordEncoder passwordEncoder;
    private final RegistrationRoleRepository roleRepository;

    public UserCreator(PasswordEncoder passwordEncoder,
                       RegistrationRoleRepository roleRepository) {
        this.passwordEncoder = passwordEncoder;
        this.roleRepository = roleRepository;
    }

    public User createUser(String username, String email, char[] password) {
        String encodedPassword;
        try {
            encodedPassword = passwordEncoder.encode(new String(password));
        } finally {

            Arrays.fill(password, '\0');
        }
         return new User.Builder()
                .withUsername(username)
                .withEmail(email)
                .withPassword(encodedPassword)
                .withRole(fetchDefaultUserRole())
                .build();
    }
7

Jeden pies.

Poziom zaawansowania aplikacji kiedy to ma znaczenie jest dużo dużo wyżej niż to co piszesz. Nie musisz się tym przejmować teraz. Użyj normalnie String

0
Ornstein napisał(a):

Cześć,
czytałem, że przechowywanie hasła w postaci Stringa to zły pomysł. Trochę myślałem o tym, jak zaimplementować to w kodzie, napisałem taki przykład.

co rozumiesz poprzez przechowywanie? Normalnie kojarzy się z zapisem do bazy

Przykłądowo klasa odpowiadająca za walidajcę:

public void validate(String username, String email, char[] password) {

String stringPassword = new String(password);

    if (!stringPassword.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$")) {
        Arrays.fill(password, '\0');
        throw new CredentialValidationException("Invalid password format. The password must contain at least " +
                "8 characters, including uppercase letters, lowercase letters, numbers, and special characters.");
    }
    Arrays.fill(password, '\0');
}

ja tam Javy z bardzo nie znam, ale czy czyścisz tu na końcu funkcji zmienną lokalną tej funkcji?

0
Miang napisał(a):
Ornstein napisał(a):

Cześć,
czytałem, że przechowywanie hasła w postaci Stringa to zły pomysł. Trochę myślałem o tym, jak zaimplementować to w kodzie, napisałem taki przykład.

co rozumiesz poprzez przechowywanie? Normalnie kojarzy się z zapisem do bazy

Chodziło mi o przechowywanie w pamięci podczas działania aplikacji.

Przykłądowo klasa odpowiadająca za walidajcę:

public void validate(String username, String email, char[] password) {

String stringPassword = new String(password);

    if (!stringPassword.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$")) {
        Arrays.fill(password, '\0');
        throw new CredentialValidationException("Invalid password format. The password must contain at least " +
                "8 characters, including uppercase letters, lowercase letters, numbers, and special characters.");
    }
    Arrays.fill(password, '\0');
}

ja tam Javy z bardzo nie znam, ale czy czyścisz tu na końcu funkcji zmienną lokalną tej funkcji?

Nie. Przyjmuję hasło od użytkownika w postaci Stringa. String w javie jest niezmienny, nie mogę tego zrobić.

2

czytałem, że przechowywanie hasła w postaci Stringa to zły pomysł

Hola hola. Fajnie że coś przeczytałeś, ale czy przeczytałeś dlaczego to się robi? Np. tutaj https://stackoverflow.com/questions/8881291/why-is-char-preferred-over-string-for-passwords

Request rejestracji ma normalnie pole password jako String.

No to masz źle, bo już w tym momencie przechowujesz sekret jako String. Nie ważne, co potem z nim dalej robisz. Spróbuj w rekordzie dać normalnie char[], Spring chyba jest na tyle mądry że załapie.

8
Riddle napisał(a):

Jeden pies.

Poziom zaawansowania aplikacji kiedy to ma znaczenie jest dużo dużo wyżej niż to co piszesz. Nie musisz się tym przejmować teraz. Użyj normalnie String

Często czytam Twoje odpowiedzi na forum (nawet jeśli temat mnie średnio interesuje) bo cenię ja merytorczność (z reguły) ale czasem odnoszę nieodparte wrażenie że do odpowiezi na forum podchodzisz jakby to był use case w pracy, tak z perspektywy konsultanta.

Autor pytał o kwestie przechowywania hasła w postaci char[] z uwagi na security, na fakt, że String jest w javie niezmienny i fakt, że przy pierwszym lepszym heap dumpie hasło było by jawnie dostępne.

tl;dr:
spłycanie problemu bo złożoność aplikacji (choćby sam fakt, że to nie aplikacja produkcyjna która zarabia $) jest niemal zerowa zapominając, że pytanie ma kontekst stricte edukacyjny (podejście do rozwiązania hipotetycznego problemu z którym później łatwo spotkać  się w realnym życiu)

Ty mam wrażenie, że podszedłeś do tematu jak konsultant tzn:

Wchodzisz do firmy X która ma problem z Y, spoglądasz najpierw na zespół developerski potem w IDE, potem znowu na zespół developerski i z powrotem na IDE i... Twoj ą rekomendacją jest: jesteście małą firmą, dopiero walidujecie swój produkt w oparciu o rynek więc zróbcie mvp (minimal value product) a jak się będziecie skalować i okaże się, że problem Y faktycznie wymaga interwencji to wrócimy do tematu.

Na forum jest trochę inaczej: głęboko wierzę, że @Ornstein nie ma żadnego produkcyjnego prblemu ani nie odpowiada za architekturę nowo powstałęgo systemu w pracy a jedynie robi swój pet project i próbuje zwalidować rzeczy o których czyta w internecie (god practices).

Zakładając kontekst stricte edukacyjny a nie z perspektywy konsultanta w firmie gdzie trzeba doradzić tak aby biznes się spinał uważam, że rada powinna iść bardziej w kierunku tego co zaproponował @kelog (wskazanie potencjalnego problemu).

Ja bym dorazucił do tego, że być może ten User to użytkownik w innym niż logowanie bounded kontekście i może nie trzeba tych dwóch bytów (user na potrzeby np. profilu użytkownika w aplikacji vs user w kontekście zestawu danych potrzebnych do autentykacji i autoryzacji w systemie) ze sobą mieszać (to już wybiegnięcie jeszcze dalej w filozoficzne rozważania ale chodzi o nakreślenie kierunku odpowiedzi).

Nie odbierz tego w sposóļ negatywny, ot taka moja luźna obserwacja którą mam już od jakiegoś czasu 😉

4
RequiredNickname napisał(a):

Na forum jest trochę inaczej: głęboko wierzę, że @Ornstein nie ma żadnego produkcyjnego prblemu ani nie odpowiada za architekturę nowo powstałęgo systemu w pracy a jedynie robi swój pet project i próbuje zwalidować rzeczy o których czyta w internecie (god practices).

Zakładając kontekst stricte edukacyjny a nie z perspektywy konsultanta w firmie gdzie trzeba doradzić tak aby biznes się spinał uważam, że rada powinna iść bardziej w kierunku tego co zaproponował @kelog (wskazanie potencjalnego problemu).

Ja bym dorazucił do tego, że być może ten User to użytkownik w innym niż logowanie bounded kontekście i może nie trzeba tych dwóch bytów (user na potrzeby np. profilu użytkownika w aplikacji vs user w kontekście zestawu danych potrzebnych do autentykacji i autoryzacji w systemie) ze sobą mieszać (to już wybiegnięcie jeszcze dalej w filozoficzne rozważania ale chodzi o nakreślenie kierunku odpowiedzi).

No okej, no to jeśli chcesz do tego tak podejść, to mogę napisać jak to zrobić.

@Ornstein

  1. Uruchom swoją aplikację która trzyma hasła w String i zaloguj się hasłem, może to być np. komputer123
  2. Spróbuj wykraść swoje hasło z pamięci albo z dumpa. To jest czasami trudniejsze niż się wydaje.
  3. Kiedy Ci się to udało masz próbkę kontrolną.
  4. Teraz zamień w swoim programie hasło ze String na char[], i spróbuj wykraść swoje hasło jeszcze raz:
    • Jeśli Ci się udało wykraśc hasło, to znaczy że jeszcze gdzieś został String z hasłem
    • Jeśli tym razem Ci się nie udało, to znak że hasła już najpewniej nie ma w stringpoolu.

I tym samym masz zabezpieczenie Twojej aplikacji przed wykradnięciem haseł ze string poola.

Aczkolwiek - żeby to zagrożenie faktycznie było realne, to ktoś niepowołany musiałby mieć dostęp do maszyny na której stoi Twoja aplikacja żeby zrobić takiego dumpa. Więc moim zdaniem to jest rozwiązywanie problemu który nie istnieje (jeszcze).

0

a majac dostęp nie wystarczyłoby po prostu logować wszystkich danych co idą do tej aplikacji? ;)

3
Miang napisał(a):

a majac dostęp nie wystarczyłoby po prostu logować wszystkich danych co idą do tej aplikacji? ;)

Jeśli ktoś zrobi coś takiego, to wtedy wszystko jedno czy @Ornstein to zapisze jako String, char[] czy cokolwiek innego. Hasło będzie można znaleźć.

Aczkolwiek wtedy haslo trzeba by "wydobyć" z żądań; w stringpoolu są hasła "gotowe" w pewnym sensie.

3

a majac dostęp nie wystarczyłoby po prostu logować wszystkich danych co idą do tej aplikacji?

Bezpieczeństwo aplikacji nigdy nie jest czarno-białe. Mając dostęp - ale jaki? Mogę odpalić tcpdump i przesłać sobie plik .pcap z całego dnia - może tak, może nie. Mogę dokleić .jar-kę co wstrzykuje komponent do Springa co wysyła mi hasła na telegram - może tak, może nie.

Ale czy mogę zrobić heapdump wirtualnej maszyny, szybko go ściągnąć i posprzątać po sobie - też nie wiadomo, ale z tego co kojarzę to mogę to zrobić bez roota, na prawach procesu javy, potem zostaje tylko wyciągnięcie pliku po sieci.

Ta sztuczka z char[] rozwiązuje tylko i wyłącznie ten jeden aspekt bezpieczeństwa - żeby nie trzymać wrażliwych danych w RAM-ie, kiedy już nie są potrzebne. To odnośnie kontekstu edukacyjnego. W kontekście konsultanctwa, cóż, niech każdy robi wedle swojego uznania.

1

Odpowiadając na twoje pytanie zasadniczo to musiałbyś od samego początku operować na tablicach, np przyjąć hasło w request w postaci listy znaków, oraz w swoim systemie operować na tym typie postaci danych, wystarczy żeby choć w jednym miejscu było plain password -> string (nawet niejawnie) i twoje starania idą w piach, tak jak masz tu passwordEncoder.encode(new String(password));. Nie wiem czy nie łatwiej byłoby zrobienie forka gc i dopisanie własnej metody do czyszczenia stringa z poolstringa. Tak jak zauważyli inni, to co chcesz osiągnąć nie ma sensu, bo przed tym zabezpiecza się gdzie indziej w inny sposób, po przez szyfrowane połączenie, ograniczony dostęp do maszyny etc.

4

Ale archeologia :D

@Riddle miał rację pisząc, że jeden pies. Powód jest prosty - to trzymanie hasła w char[] zamiast w Stringu rozwiązuje problem z czasów, kiedy StringPool trzymany był w permgenie.

Szybkie Google wskazuje, że StringPool siedzi w heapie od Javy 7 (rok 2011) i stamtąd mogą być wyłapywane przez GC. W skrócie - jeśli ktoś potrafi się dobrać do heapa to nie ma znaczenia, czy to będzie w char[] czy String, bo i tak sobie podejrzy zawartość.

Tyle teorii. W praktyce można trochę się pobawić, tj. zrobić coś takiego:


public void doSomething() {
  char[] password = getPassword();
  String hash = hash(password);

  for (int i=0; i<password.length; i++) {
    password[i] = '0';
  }

  storePassword(username, hash);  
}

I tutaj - to ciągle nie jest stuprocentowo bezpieczne (bo JVM sobie zrobi lokalne kopie password, bo można podejrzeć "w dobrym momencie" itp.) pewnie najlepiej byłoby, żeby to trzymać w jakimś dedykowanym obiekcie, który by miał volatile czy jakiegoś innego AtomicReference do wyciągania char[].

4

Jedna uwaga.
String jest immutable, ok, ale to nie znaczy, ze GC go nie wywali, gdy referencja już nie będzie potrzebna.

Sama analiza "internowania" stringów (wrzucanie do string poola) zaczyna się już na etapie compile-time, gdzie kompilator szuka "literałów"
np. String pies = "pies"
i to zostanie wrzucone do string poola po uruchomieniu.

Twoj przypadek dzieje się w runtime. Framework mapujący request pod spodem zrobi new String("password") i nie użyje metody .intern() służącej do wrzucenia do string poola. Wtedy masz zwykły obiekt, który rzeczywiście jest immutable, nic więcej. Możesz tworzyć przecież niezmienne obiekty i nie zostaną z Tobą na zawsze przez cały okres trwania programu.

Tak więc, GC będzie decydował za Ciebie kiedy hasło zostanie usunięte.

1
Ornstein napisał(a):

Cześć,
czytałem, że przechowywanie hasła w postaci Stringa to zły pomysł. Trochę myślałem o tym, jak zaimplementować to w kodzie, napisałem taki przykład.

Request rejestracji ma normalnie pole password jako String.

TLDR;

Odpowiem Tobie tak jak kiedyś - na podobne pytanie i mi odpowiedziano.

Nie powinno się haseł trzymać w aplikacji w formie zmiennych ze zwykłych klas. Bo są do takich kwestii specjalnie przystosowane klasy.
A wykraść można próbować na wiele sposobów. Albo podczas transmisji danych, albo za pomocą dump'a albo za pomocą uruchomionego profilera. I każde z tych miejsc się odpowiednio profilaktycznie zabezpiecza. A inne kwestie trzeba na pewnym etapie w końcu zostawić zabezpieczeniom JVM, systemowi operacyjnemu itp. - bo inaczej człowiek się zapętli i oszaleje, a na końcu zabetonuje komputer w piwnicy.

3

Jeżeli request z hasłem przychodzi jako HTTPs to hasło i tak będzie w pamięci.
U ciebie to będzie request.password() - więc ten extra string niewiele Ci da.

Niemniej jak już używać czegoś to najlepiej gotowca z OWASP: https://github.com/OWASP/passfault/blob/master/core/src/main/java/org/owasp/passfault/impl/SecureString.java

Tutaj nowsza wersja:
https://github.com/nulab/zxcvbn4j/blob/main/src/main/java/com/nulabinc/zxcvbn/WipeableString.java#L7

3

Odpowiedź trochę nie na temat, bo w temacie, to bez większego znaczenia, czy to Strong, czy char[]. Ale jedno mnie niepokoi...
Co robi encode? Jeżeli tak jak to wygląda jest to jedynie zakodowanenp Base64 hasło, albo nawet nieposolony hash, to bardzo, ale to bardzo niefajnie. Tak niefajnie, że podobnie jak morele.pl jedynie kwestią czasu jest, aż będzie trzeba wysłać maila aktywnego żalu. https://plikcenter.pl/Producent_IT/hasla-350-tysiecy-uzytkownikow-morele-juz-zlamane-co-gorsza-wyciekly-dane-takze-tych-ktorzy-konta-w-morele-skasowali-nawet-przed-wlamaniem/

Przechowywanie haseł w formie odwracalnej, to błąd. Hash bez salt, to trochę lepiej, ale nadal 90% da się odczytać w sekundy. (rainbow tables)

Czyli podsumowując - jak masz pisać uwierzytelnianie, to zapytaj, czy nie lepiej kupić to jako usługę (Firebase, Auth0). Jeżeli odpowiedź jest odmowna, to warto rozważyć np. KeyCloak. Jak i tutaj się nie da, to trzeba zadbać o jednostronne szyfrowanie z odpowiednio dużą złożonością obliczeniową/pamięciową), oraz koniecznie, unikalnością hashy (2 różnych użytkowników z identycznymi hasłami powinno mieć różne pwd hash w bazie.
Np. kilka tysięcy rund hmac z nazwą użytkownika jako kluczem. Albo Bcrypt2 - o tyle prościej, że solenie jest już wbudowane w algorytm, w dodatku prościej można sterować złożonością obliczeniową.

3
piotrpo napisał(a):

Co robi encode?

Hashuje hasło przy użyciu BCryptPasswordEncoder.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.