Przejdź do treści

Szyfrowanie i odszyfrowanie przez AES-GCM

  • Przydatne narzędzia
  • poziom: zaawansowany
  • aktualizacja: 16 sierpnia 2026

AES-GCM naraz ukrywa treść i dokłada znacznik uwierzytelniający, dzięki któremu wykryjesz zmianę choćby jednego bitu. To bezpieczniejszy wybór niż samo AES/CBC/PKCS5Padding, przy którym bez osobnego MAC ktoś może majstrować przy szyfrogramie, a Ty się o tym nie dowiesz. Najważniejsza zasada GCM brzmi: dla jednego klucza nie wolno powtórzyć wartości IV. Przykład losuje 12-bajtowy IV przez SecureRandom przy każdym szyfrowaniu, dołącza go jawnie do szyfrogramu i pozwala objąć tagiem dodatkowy kontekst jako AAD.

JAVA SzyfrowanieAesGcm.java 109 linii Pobierz .java
import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

/**
 * Uwierzytelnione szyfrowanie tekstu algorytmem AES-GCM — skryptyJAVA.pl
 * Zapisz jako: SzyfrowanieAesGcm.java
 */
public class SzyfrowanieAesGcm {

    private static final String TRANSFORMACJA = "AES/GCM/NoPadding";
    private static final String WERSJA = "AESGCM1";
    private static final int BAJTY_IV = 12;
    private static final int BITY_TAGU = 128;
    private static final SecureRandom LOSOWE = new SecureRandom();
    private static final Base64.Encoder KODER = Base64.getUrlEncoder().withoutPadding();
    private static final Base64.Decoder DEKODER = Base64.getUrlDecoder();

    /** Generuje nowy 256-bitowy klucz AES. Klucza nie wolno trzymać obok szyfrogramu. */
    public static SecretKey generujKlucz() throws GeneralSecurityException {
        KeyGenerator generator = KeyGenerator.getInstance("AES");
        generator.init(256, LOSOWE);
        return generator.generateKey();
    }

    /** Eksportuje klucz do Base64 URL-safe, np. do chronionego magazynu sekretów. */
    public static String zapiszKlucz(SecretKey klucz) {
        return KODER.encodeToString(klucz.getEncoded());
    }

    /** Odtwarza klucz 128-, 192- albo 256-bitowy z zapisu Base64 URL-safe. */
    public static SecretKey wczytajKlucz(String zapis) {
        byte[] bajty = DEKODER.decode(zapis);
        try {
            if (bajty.length != 16 && bajty.length != 24 && bajty.length != 32) {
                throw new IllegalArgumentException("Nieprawidłowa długość klucza AES.");
            }
            return new SecretKeySpec(bajty, "AES");
        } finally {
            Arrays.fill(bajty, (byte) 0);
        }
    }

    /** Zwraca zapis WERSJA.IV.SZYFROGRAM; 16-bajtowy tag GCM jest częścią szyfrogramu. */
    public static String szyfruj(String tekst, SecretKey klucz, byte[] aad)
            throws GeneralSecurityException {
        byte[] iv = new byte[BAJTY_IV];
        LOSOWE.nextBytes(iv);
        Cipher szyfr = Cipher.getInstance(TRANSFORMACJA);
        szyfr.init(Cipher.ENCRYPT_MODE, klucz, new GCMParameterSpec(BITY_TAGU, iv));
        if (aad != null && aad.length > 0) { szyfr.updateAAD(aad); }
        byte[] szyfrogram = szyfr.doFinal(tekst.getBytes(StandardCharsets.UTF_8));
        return WERSJA + "." + KODER.encodeToString(iv) + "." + KODER.encodeToString(szyfrogram);
    }

    /**
     * Odszyfrowuje i weryfikuje tag. Zły klucz, inne AAD albo zmieniony bajt danych kończą się
     * wyjątkiem AEADBadTagException, a brakujący lub uszkodzony zapis — IllegalArgumentException.
     */
    public static String odszyfruj(String zapis, SecretKey klucz, byte[] aad)
            throws GeneralSecurityException {
        if (zapis == null) {
            throw new IllegalArgumentException("Brak zapisu do odszyfrowania.");
        }
        String[] czesci = zapis.split("\\.", -1);
        if (czesci.length != 3 || !czesci[0].equals(WERSJA)) {
            throw new IllegalArgumentException("Nieobsługiwany format szyfrogramu.");
        }
        byte[] iv = DEKODER.decode(czesci[1]);
        byte[] szyfrogram = DEKODER.decode(czesci[2]);
        if (iv.length != BAJTY_IV || szyfrogram.length < BITY_TAGU / 8) {
            throw new IllegalArgumentException("Uszkodzony szyfrogram.");
        }

        Cipher szyfr = Cipher.getInstance(TRANSFORMACJA);
        szyfr.init(Cipher.DECRYPT_MODE, klucz, new GCMParameterSpec(BITY_TAGU, iv));
        if (aad != null && aad.length > 0) { szyfr.updateAAD(aad); }
        byte[] tekst = szyfr.doFinal(szyfrogram);
        return new String(tekst, StandardCharsets.UTF_8);
    }

    // ── PRZYKŁADY ───────────────────────────────────────────
    public static void main(String[] args) throws GeneralSecurityException {
        SecretKey klucz = generujKlucz();
        String zapisKlucza = zapiszKlucz(klucz);
        SecretKey odtworzonyKlucz = wczytajKlucz(zapisKlucza);
        byte[] aad = "uzytkownik:42".getBytes(StandardCharsets.UTF_8);

        String szyfrogram = szyfruj("Poufna wiadomość: ąęść", odtworzonyKlucz, aad);
        System.out.println("Szyfrogram: " + szyfrogram);
        System.out.println("Odszyfrowano: " + odszyfruj(szyfrogram, odtworzonyKlucz, aad));
        System.out.println("Klucz Base64 ma " + zapisKlucza.length() + " znaki.");

        try {
            byte[] zleAad = "uzytkownik:43".getBytes(StandardCharsets.UTF_8);
            odszyfruj(szyfrogram, odtworzonyKlucz, zleAad);
        } catch (AEADBadTagException blad) {
            System.out.println("Zmienione AAD zostało wykryte — tag GCM jest niepoprawny.");
        }
    }
}

Jak użyć tego skryptu

  1. Zapisz kod w pliku SzyfrowanieAesGcm.java — kompilator wymaga, żeby plik nazywał się jak klasa publiczna — i uruchom przez java SzyfrowanieAesGcm.java. Program wygeneruje klucz, zaszyfruje polski tekst, poprawnie go odtworzy i pokaże, że zmienione AAD zostaje odrzucone.
  2. Klucz utwórz raz przez generujKlucz(), a następnie zapisz wynik zapiszKlucz(...) w menedżerze sekretów lub sprzętowym magazynie kluczy. Nie generuj nowego klucza przy odszyfrowaniu istniejących rekordów.
  3. Szyfruj przez szyfruj(tekst, klucz, aad) i przechowuj zwrócony zapis w jednej kolumnie. IV nie jest tajny i celowo znajduje się przy szyfrogramie; metoda losuje nową wartość dla każdego wywołania.
  4. Przy odszyfrowaniu przekaż identyczne AAD, np. identyfikator użytkownika i typ rekordu. AAD nie jest szyfrowane, ale jest objęte tagiem — nie da się podmienić kontekstu bez wykrycia manipulacji.

Warto wiedzieć

  • Nigdy nie użyj drugi raz tego samego IV z tym samym kluczem AES-GCM. To nie jest ostrożnościowe zalecenie: powtórzony nonce ujawnia wprost XOR obu tekstów jawnych, a na dokładkę pozwala odtworzyć podklucz uwierzytelniający, czyli fałszować dowolne wiadomości pod tym kluczem. Losowe 96 bitów jest właściwym domyślnym rozmiarem, ale przy miliardach rekordów na jednym kluczu przejdź na licznik albo rotuj klucz, zanim zbliżysz się do granicy urodzinowej.
  • Klucz jest sekretem; IV i szyfrogram nimi nie są. Trzymanie klucza w tej samej bazie i pod tymi samymi uprawnieniami co dane niewiele daje po jej przejęciu — użyj menedżera sekretów, KMS albo keystore i ogranicz dostęp. Jeśli planujesz rotację, dopisz do zapisu własny identyfikator klucza, np. AESGCM1.k3.IV.SZYFROGRAM: prefiks AESGCM1 wersjonuje format, a nie klucz, więc na gołym formacie po rotacji nie odgadniesz, czym odszyfrować stary rekord.
  • Wyjątku AEADBadTagException nie wolno ignorować ani zastępować częściowo odszyfrowanym tekstem. Oznacza zły klucz, inne AAD albo manipulację szyfrogramem — rekord odrzuć i odnotuj zdarzenie, ale bez wypisywania poufnej treści do logów. Uszkodzony albo obcy zapis to z kolei IllegalArgumentException, które nie jest GeneralSecurityException: w handlerze webowym łap oba, inaczej byle śmieć w kolumnie zamieni się w błąd 500.
  • AES-GCM służy do danych, które trzeba później odzyskać. Haseł użytkowników nie szyfruj — przechowuj je przez jednokierunkową funkcję przeznaczoną do haseł, taką jak PBKDF2, Argon2id, scrypt albo bcrypt, z osobną losową solą dla każdego rekordu. Zwróć też uwagę, że tekst jawny wędruje tu przez String, którego nie wyzerujesz — przy naprawdę wrażliwych danych operuj na byte[] i czyść bufory zaraz po użyciu.
#bezpieczeństwo #kryptografia #securerandom #base64

Podobne skrypty