Cipher, szyfrowanie RSA i problem z SonarQube

0

Cześć, potrzebuję hashować i odhashowywać token za pomocą RSA, napisałem sobie metody:

byte[] createHash(byte[] tokenBytes, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        return cipher.doFinal(tokenBytes);
}

byte[] decryptHash(byte[] hash, PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        return cipher.doFinal(hash);
}

i wszystko spoko, ale SonarQube, woła, że ten sposób tworzenia Ciphera jest niebezpieczny i że powinno być:

Cipher cipher = Cipher.getInstance("rsa/ecb/oaepwithsha-256andmgf1padding");

niestety to już się nawet nie uruchamia, bo dostaję błąd:

InvalidKeyException, OAEP cannot be used to sign or verify signatures

Ktoś ma jakieś pomysły? Co źlę robię?

7

RSA nie służy do hashowania i w ogóle nie da się czegoś odhashować - z natury funkcje hashujące są jednokierunkowe!

Co źlę robię?

Dotykasz rzeczy których zupełnie nie rozumiesz. Więcej będzie z tego szkody niż pożytku.

i wszystko spoko, ale SonarQube, woła, że ten sposób tworzenia Ciphera jest niebezpieczny i że powinno być:

Texbook RSA to sie łamie na kartce w przerwie na kawę, więc dobrze że Sonar krzyczy że coś jest nie tak. Zresztą w ogóle ten kod nie ma sensu, bo co jak token jest dłuższy niż modulus? Jeśli w ogóle to podpisuje się skrót wiadomości (czyli najpierw jest jakieś SHA a potem dopiero wynik jest podpisywany RSA).

W ogóle robisz tu jakiś cyrk, bo JWT definiuje w jaki sposób podpisuje się tokeny i nie bardzo rozumiem czemu próbujesz wymyślić jakiś własny sposób, który będzie niekompatybilny ze wszystkim innym?

https://www.rfc-editor.org/rfc/rfc7515.html
https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/JWSAlgorithm.html

Weź też pod uwagę że powinieneś PODPISYWAĆ a nie SZYFROWAĆ zawartość tokena. Teraz to w ogóle każdy może zrobić token który twój system łyknie ja pelikan, bo RSA encrypt robi się kluczem publicznym (do którego każdy ma dostęp). Trik polega na tym że powinieneś token podpisać (kluczem prywatnym) a potem każdy z kluczem publicznym moze ten podpis zweryfikować.

Podsumowując: zrobiłeś źle wszystko co się tylko dało zrobić źle.

0

@Shalom:

Może od początku.
Architekci na samym początku u nas tworzyli JWT w dwóch krokach:

  1. Dane użytkownika w JSON konwertowali do base64
  2. Tworzyli sygnaturę hashując za pomocą HMAC base64 z punktu pierwszego i po prostu łączyli jest kropką jak to w JWT

Przy walidacji tokena sprawdzali czy sygnatura z tokena równa się zahashowanemu payloadowi.

Teraz chcą oni zamiast HMAC użyć RSA.
Zasada ma być ta sama tylko zamiast HMACA do tworzenia sygnatury chcemy użyć RSA.

I kto powiedział, że ja tworzę sygnaturę za pomocą klucz publicznego?

byte[] createHash(byte[] tokenBytes, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        return cipher.doFinal(tokenBytes);
}

Może ta metoda się źle nazywa, ale do niej chcę przekazać payload tokena w base64 i stworzyć sygnaturę po prostu.

A w tej metodzie niżej chcę z sygnatury wyciągnąć payload w base64 i tyle:

byte[] decryptHash(byte[] hash, PublicKey publicKey) throws NoSuchPaddingException, NoSuchAlgorithmException, BadPaddingException, IllegalBlockSizeException, InvalidKeyException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        return cipher.doFinal(hash);
}

Poczytałem i do tworzenia sygnatury można użyć HMAC albo RSA.

Sugerujesz zatem po prostu uzyć jakiejś biblioteki do JWT?

Nie czaję trochę tego, że nie powinienem szyfrować tylko podpisywać zawartość tokena. Przecież wszędzie tą sygnaturę tworzą za pomocą hashowania payloadu albo użycia RSA do zaszyfrowania payloada. Trochę sprzeczność.

Architekci woleli po prostu skorzystać z klas javy zamiast dociągać biblioteki.

2

Nadal nic nie rozumiesz.

  1. Szyfrowanie w RSA odbywa się za pomocą klucza publicznego, bo idea jest taka że każdy może szyfrować, a tylko posiadacz klucza prywatnego może to odszyfrować
  2. Podpisywanie odbywa się odwrotnie, tylko posiadacz klucza prywatnego może podpisać, ale każdy może kluczem publicznym to zweryfikować.
  3. RSA nie da się używać jak młotka. Ten algorytm ma swoje ograniczenia na rozmiar tego co można podpisać albo zaszyfrować i dlatego generalnie podpisuje się hashe a nie cały payload. Dodatkowo RSA bardzo łatwo zepsuć jeśli nie umie się go używać, bo wiele zależy np od paddingu, którego ty w ogóle nie używasz. Taka wersja bez paddingu jesteś się textbook RSA i zupełnie nie jest bezpieczna.
  4. JWT ma swoje standardy i jeśli te wasze tokeny mają współpracować z jakąkolwiek biblioteką to musicie je respektować!
  5. W ogóle nie bardzo rozumiem jak to ma działać, bo przecież infrastruktura pod JWT wymaga też odpowiednich meta-danych wystawianych pod ściśle zdefiniowanymi endpointami, bo inaczej znów żadna istniejącą biblioteka nie będzie umiała się z tym skomunikować.

Moja rada: postawcie sobie jakiś CAS albo Keycloaka albo coś podobnego i absolutnie nie próbujcie tego napisać sami. Kryptografia jest trudna i bardzo łatwo zrobić ją źle.
Nawet jakbyś użył jakiejś biblioteki to nadal możesz tej biblioteki użyć źle i nie ufałbym czemuś takiemu do czegokolwiek więcej niż tylko do testów.

0

@Shalom:
Dzięki. Czyli tak:

Szyfrowanie od podpisywania różni się tym, że szyfruje się kluczem publicznym, a podpisuje prywatnym. Tak? To jest jedyna różnica? No to tak mam zrobione.

Najpierw payload muszę zahashować jakimś HMACiem, aby utworzyć skrót, aby nie podpisywać całego payloadu. Tu też jest ok tak?

W tym momencie mam już hash, mam klucz prywatny i chcę podpisać ten hash. Zmieniam zatem nazwy w mojej metodzie, aby nie myliło:

byte[] sign(byte[] hash, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        return cipher.doFinal(hash);
}

W jaki sposób bezpiecznie to podpisać? Zakładam, że inaczej utworzyć instancje Ciphera?

1

Szyfrowanie od podpisywania różni się tym, że szyfruje się kluczem publicznym, a podpisuje prywatnym. Tak? To jest jedyna różnica? No to tak mam zrobione.

Nie. Tzn wyjątkowo zupełnie przypadkiem dla RSA tak jest, bo to akurat taki algorytm który jednocześnie pozwala na szyfrowanie i na podpisywanie. Ale nie jest to żadna reguła. Takie (EC)(Ed)DSA nie ma odpowiednika "szyfrowania", a jedynie możliwość podpisywania. W kontekście JWT w ogóle możesz zapomnieć o tym, że RSA wspiera coś takiego jak szyfrowanie.

Najpierw payload muszę zahashować jakimś HMACiem, aby utworzyć skrót, aby nie podpisywać całego payloadu. Tu też jest ok tak?

Nie. HMAC a Hash (SHA, MD5) to są zupełnie dwie różne rzeczy. Aż boję się pytać czy aby czasem ten "HMAC" którego teraz używacie też nie jest pisany "ręcznie" i np. to nie jest po prostu SHA z wiadomośći... :D
Jeszcze raz: JWT ma RFC z opisanymi standardami i powinieneś się ich trzymać. RFC opisuje konkretnie jakich algorytmów użyć!

Ale w ogólności tak, musisz hashować wiadomość zanim ją podpiszesz, bo RSA nie pozwala podpisać czegoś dłuższego niż modulus, czyli w zalezności od twojego klucza np. 1024 czy 2048 bitów.

W jaki sposób bezpiecznie to podpisać? Zakładam, że inaczej utworzyć instancje Ciphera?

Ja bym sugerował użyć jakiegoś gotowca, bo Java w standardzie ma bardzo niewiele. Np. taka libka:

        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
        </dependency>

Ona służy dokładnie do tego. Coś w tym stylu:

RSAKey rsaJWK = new RSAKeyGenerator(2048)
                    .keyID(someId)
                    .keyUse(KeyUse.SIGNATURE)
                    .generate();
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
		.subject(subject)
		.jwtID(someid)
		.audience(someAudience)
		.issuer(someIssuer)
		.expirationTime(someTimestamp)
		.claim("claim_name", claim)
		.claim("claim_name2", claim2)
		.build();
SignedJWT signedJWT = new SignedJWT(
		new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(),
		claimsSet);
signedJWT.sign(signer);
String finalJWT = signedJWT.serialize() + "=";

wygeneruje poprawnie podpisane JWT które jak wystawisz klucz (rsaJWK.toPublicJWK().toJSONObject()) to dowolny inny klient JWT (np. jakaś angularowa apka) będzie umiał ten podpis zweryfikować. Ale NIE RÓB TAK chyba że piszesz coś do testów. Absolutnie nie używaj takiego kodu na produkcji!

Jesli już z jakiegoś powodu musisz bez żadnych zewnętrznych zależności, to java ma klasy do podpisów, ale bardzo biedne:
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/security/Signature.html

Niemniej nadal apeluje: NIE PISZ własnego identity providera, jeszcze robiąc jakieś krypto na kolanie. Albo pisz, ale daj nam potem link do tej aplikacji, żebyśmy mogli zostać milionerami za bug bounty ( ͡° ͜ʖ ͡°) Serio, są otwarte darmowe rozwiązania i należy ich używać.

0

Ale po co robisz z ręki JWT? Przecież bibliotek masz pod dostatkiem: https://jwt.io/libraries Z tego co pamiętam używałem jose4j, z ręki musiałem dorobić jedynie mechanizm rotowania kluczy + wystawianie ich przez JWKS.

0

@Shalom: @piotrpo

Udało mi się chyba prawie przekonać, aby użyć czegoś gotowego.

Znajdę coś lepszego niż to? Czy to będzie ok?
https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt/0.9.1

I jeszcze pytanie - czy w innych ekosystemach pisanych w c#, pythonie, ruby itd bez problemu znajdę bibliotekę, aby ten token zwalidować?

0

Nie wiem czy znajdziesz coś lepszego, jak ja to robiłem, to wszedłem na stronę, którą podlinkowałem wyżej, poszukałem czegoś dla Java, gdzie wszystkie cechy były na zielono. Wspólną cechą tych bibliotek jest (powinno być?) trzymanie się standardu, więc jeżeli wygenerujesz sobie token jednym narzędziem, to bez problemu powinno się dać go walidować drugim, niezależnie od języka programowania. Z natury JWT masz mieć możliwość wystawienia wystawienia i sprawdzenia go na różnych maszynach. Biblioteki dla innych języków masz na tej samej stronie.

1 użytkowników online, w tym zalogowanych: 0, gości: 1