Java AES encryption and decryption

author image
By mkyong | Ostatnia aktualizacja: 2 czerwca 2020 r.
Obserwowane: 64,491 | +1,553 pcw

Szyfrowanie AES

Algorytm Advanced Encryption Standard (AES, Rijndael) jest algorytmem szyfrowania i deszyfrowania szyfrem blokowym, najczęściej stosowanym algorytmem szyfrowania na świecie. AES przetwarza blok 128 bitów używając tajnego klucza o długości 128, 192 lub 256 bitów.

W artykule przedstawiono kilka przykładów szyfrowania i deszyfrowania AES w Javie:

  • AES Szyfrowanie ciągów znaków – (szyfrowanie i deszyfrowanie ciągów znaków).
  • AES Szyfrowanie oparte na haśle – (tajny klucz będzie pochodził od danego hasła).
  • AES Szyfrowanie plików. (oparte na haśle).

W tym artykule, skupimy się na 256-bitowym szyfrowaniu AES z trybem licznika Galois (GCM).

GCM = CTR + Authentication.

Dalsza lektura
Przeczytaj to – NIST – Recommendation for Galois/Counter Mode (GCM)

Nie używaj trybu AES Electronic codebook (ECB) Mode
Tryb AES ECB, lub AES/ECB/PKCS5Padding (w Javie) nie jest semantycznie bezpieczny – Szyfrogram zaszyfrowany w trybie ECB może wyciekać informacje o tekście jawnym. Tutaj jest dyskusja na temat Dlaczego nie powinienem używać szyfrowania ECB?

Java i wejścia szyfrowania AES.

W szyfrowaniu i deszyfrowaniu AES potrzebujemy następujących danych wejściowych:

Najlepsza praktyka szyfrowania AES
Nie używaj ponownie IV z tym samym kluczem.

1.1 IV (wartość początkowa lub wektor początkowy), jest to losowy bajt, zwykle 12 bajtów lub 16 bajtów. W Javie, możemy użyć SecureRandom do wygenerowania losowego IV.

 // 16 bytes IV public static byte getRandomNonce() { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; } // 12 bytes IV public static byte getRandomNonce() { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; }

1.2 Klucz tajny AES, albo AES-128 lub AES-256. W Javie możemy użyć KeyGenerator do wygenerowania klucza tajnego AES.

 // 256 bits AES secret key public static SecretKey getAESKey() throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(256, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); }

1.3 Klucz tajny AES, który pochodzi od podanego hasła. W Javie możemy wykorzystać SecretKeyFactory oraz PBKDF2WithHmacSHA256 do wygenerowania klucza AES z podanego hasła.

 // AES key derived from a password public static SecretKey getAESKeyFromPassword(char password, byte salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // iterationCount = 65536 // keyLength = 256 KeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); return secret; }

Używamy salt do ochrony przed atakami tęczowymi, jest to również losowy bajt, możemy użyć tego samego 1.1 getRandomNonce do jego wygenerowania.

1.4 Powyższe metody grupujemy w jedną klasę util, aby nie powtarzać ciągle tego samego kodu.

CryptoUtils.java
package com.mkyong.crypto.utils;import javax.crypto.KeyGenerator;import javax.crypto.SecretKey;import javax.crypto.SecretKeyFactory;import javax.crypto.spec.PBEKeySpec;import javax.crypto.spec.SecretKeySpec;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;import java.security.spec.InvalidKeySpecException;import java.security.spec.KeySpec;import java.util.ArrayList;import java.util.List;public class CryptoUtils { public static byte getRandomNonce(int numBytes) { byte nonce = new byte; new SecureRandom().nextBytes(nonce); return nonce; } // AES secret key public static SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException { KeyGenerator keyGen = KeyGenerator.getInstance("AES"); keyGen.init(keysize, SecureRandom.getInstanceStrong()); return keyGen.generateKey(); } // Password derived AES 256 bits secret key public static SecretKey getAESKeyFromPassword(char password, byte salt) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); // iterationCount = 65536 // keyLength = 256 KeySpec spec = new PBEKeySpec(password, salt, 65536, 256); SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); return secret; } // hex representation public static String hex(byte bytes) { StringBuilder result = new StringBuilder(); for (byte b : bytes) { result.append(String.format("%02x", b)); } return result.toString(); } // print hex with block size split public static String hexWithBlockSize(byte bytes, int blockSize) { String hex = hex(bytes); // one hex = 2 chars blockSize = blockSize * 2; // better idea how to print this? List<String> result = new ArrayList<>(); int index = 0; while (index < hex.length()) { result.add(hex.substring(index, Math.min(index + blockSize, hex.length()))); index += blockSize; } return result.toString(); }}

Szyfrowanie i deszyfrowanie AES.

AES-GSM jest najszerzej stosowanym szyfrem uwierzytelniającym. Ten przykład zaszyfruje i odszyfruje ciąg znaków przy użyciu 256-bitowego AES w trybie licznika Galois (GCM).

Wejścia AES-GCM:

  • Klucz tajny AES (256 bitów)
  • IV – 96 bitów (12 bajtów)
  • Długość (w bitach) znacznika uwierzytelniania – 128 bitów (16 bajtów)

2.1 W Javie używamy AES/GCM/NoPadding do reprezentowania algorytmu AES-GCM. Dla zaszyfrowanego wyjścia, prefiksujemy 16 bajtów IV do zaszyfrowanego tekstu (szyfrogramu), ponieważ potrzebujemy tego samego IV do deszyfrowania.

Czy to jest w porządku, jeśli IV jest publicznie znany? tym przykładzie użyjemy AES do zaszyfrowania tekstu jawnego Hello World AES-GCM i późniejszego odszyfrowania go z powrotem do oryginalnego tekstu jawnego.

EncryptorAesGcm.java
package com.mkyong.crypto.encryptor;import com.mkyong.crypto.utils.CryptoUtils;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;/** * AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption. * <p> * The output consist of iv, encrypted content, and auth tag in the following format: * output = byte {i i i c c c c c c ...} * <p> * i = IV bytes * c = content bytes (encrypted content, auth tag) */public class EncryptorAesGcm { private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; private static final int IV_LENGTH_BYTE = 12; private static final int AES_KEY_BIT = 256; private static final Charset UTF_8 = StandardCharsets.UTF_8; // AES-GCM needs GCMParameterSpec public static byte encrypt(byte pText, SecretKey secret, byte iv) throws Exception { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.ENCRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte encryptedText = cipher.doFinal(pText); return encryptedText; } // prefix IV length + IV bytes to cipher text public static byte encryptWithPrefixIV(byte pText, SecretKey secret, byte iv) throws Exception { byte cipherText = encrypt(pText, secret, iv); byte cipherTextWithIv = ByteBuffer.allocate(iv.length + cipherText.length) .put(iv) .put(cipherText) .array(); return cipherTextWithIv; } public static String decrypt(byte cText, SecretKey secret, byte iv) throws Exception { Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, secret, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte plainText = cipher.doFinal(cText); return new String(plainText, UTF_8); } public static String decryptWithPrefixIV(byte cText, SecretKey secret) throws Exception { ByteBuffer bb = ByteBuffer.wrap(cText); byte iv = new byte; bb.get(iv); //bb.get(iv, 0, iv.length); byte cipherText = new byte; bb.get(cipherText); String plainText = decrypt(cipherText, secret, iv); return plainText; } public static void main(String args) throws Exception { String OUTPUT_FORMAT = "%-30s:%s"; String pText = "Hello World AES-GCM, Welcome to Cryptography!"; // encrypt and decrypt need the same key. // get AES 256 bits (32 bytes) key SecretKey secretKey = CryptoUtils.getAESKey(AES_KEY_BIT); // encrypt and decrypt need the same IV. // AES-GCM needs IV 96-bit (12 bytes) byte iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE); byte encryptedText = EncryptorAesGcm.encryptWithPrefixIV(pText.getBytes(UTF_8), secretKey, iv); System.out.println("\n------ AES GCM Encryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText)); System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded()))); System.out.println(String.format(OUTPUT_FORMAT, "IV (hex)", CryptoUtils.hex(iv))); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) ", CryptoUtils.hex(encryptedText))); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16))); System.out.println("\n------ AES GCM Decryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (hex)", CryptoUtils.hex(encryptedText))); System.out.println(String.format(OUTPUT_FORMAT, "Input (hex) (block = 16)", CryptoUtils.hexWithBlockSize(encryptedText, 16))); System.out.println(String.format(OUTPUT_FORMAT, "Key (hex)", CryptoUtils.hex(secretKey.getEncoded()))); String decryptedText = EncryptorAesGcm.decryptWithPrefixIV(encryptedText, secretKey); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText)); }}

Wyjście

Tekst jawny : Hello World AES-GCM

Terminal
------ AES GCM Encryption ------Input (plain text) :Hello World AES-GCMKey (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436IV (hex) :bdb271ce5235996a0709e09cEncrypted (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3Encrypted (hex) (block = 16) :------ AES GCM Decryption ------Input (hex) :bdb271ce5235996a0709e09c2d03eefe319e9329768724755c56291aecaef88cd1e6bdf72b8c7b54d75a94e66b0cd3Input (hex) (block = 16) :Key (hex) :603d87185bf855532f14a77a91ec7b025c004bf664e9f5c6e95613ee9577f436Decrypted (plain text) :Hello World AES-GCM

Tekst jawny : Hello World AES-GCM, Welcome to Cryptography!

Terminal
------ AES GCM Encryption ------Input (plain text) :Hello World AES-GCM, Welcome to Cryptography!Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095cIV (hex) :b05d6aedf023f73b9e1e2d11Encrypted (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9Encrypted (hex) (block = 16) :------ AES GCM Decryption ------Input (hex) :b05d6aedf023f73b9e1e2d11f6f5137d971aea8c5cdd5b045e0960eb4408e0ee4635cccc2dfeec2c13a89bd400f659be82dc2329e9c36e3b032f38bd42296a8495ac840b0625c097d9Input (hex) (block = 16) :Key (hex) :ddc24663d104e1c2f81f11aef98156503dafdc435f81e3ac3d705015ebab095cDecrypted (plain text) :Hello World AES-GCM, Welcome to Cryptography!

Szyfrowanie i deszyfrowanie oparte na haśle AES.

Do szyfrowania opartego na haśle możemy użyć specyfikacji PKCS (Password-Based Cryptography Specification), zdefiniowanej RFC 8018, do wygenerowania klucza z podanego hasła.

Dla danych wejściowych PKCS:

  • Hasło, podajesz to.
  • Sól – Co najmniej 64 bity (8 bajtów) losowych bajtów.
  • Liczba iteracji – Zalecana minimalna liczba iteracji 1,000.

Co to jest sól i liczba iteracji?

  • Sól salt produkuje szeroki zestaw kluczy dla danego hasła. Na przykład, jeśli sól wynosi 128 bitów, będzie aż 2^128 kluczy dla każdego hasła. Zwiększa to zatem trudność ataków tęczowych. Co więcej, tęczowa tablica, którą atakujący budują dla hasła jednego użytkownika, staje się bezużyteczna dla innego użytkownika.
  • Więcej o iteration count zwiększa koszt produkcji kluczy z hasła, przez co zwiększa trudność i spowalnia szybkość ataków.

3.1 Dla zaszyfrowanego wyjścia, prefiksujemy 12 bytes IV i password salt do szyfrogramu, ponieważ potrzebujemy tego samego IV i soli hasła (dla tajnego klucza) do deszyfrowania. Ponadto używamy Base64 kodera, aby zakodować zaszyfrowany tekst do reprezentacji łańcuchowej, tak abyśmy mogli wysłać zaszyfrowany tekst lub szyfrogram w formacie łańcuchowym (była to tablica bajtów).

Czy to jest w porządku, jeśli sól hasła jest publicznie znana?
Jest tak samo z IV, i jest w porządku, aby sól hasła była publicznie znana, jedynym sekretem jest klucz, utrzymując go prywatnym i poufnym.

EncryptorAesGcmPassword.java
package com.mkyong.crypto.encryptor;import com.mkyong.crypto.utils.CryptoUtils;import javax.crypto.Cipher;import javax.crypto.SecretKey;import javax.crypto.spec.GCMParameterSpec;import java.nio.ByteBuffer;import java.nio.charset.Charset;import java.nio.charset.StandardCharsets;import java.util.Base64;/** * AES-GCM inputs - 12 bytes IV, need the same IV and secret keys for encryption and decryption. * <p> * The output consist of iv, password's salt, encrypted content and auth tag in the following format: * output = byte {i i i s s s c c c c c c ...} * <p> * i = IV bytes * s = Salt bytes * c = content bytes (encrypted content) */public class EncryptorAesGcmPassword { private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; private static final int TAG_LENGTH_BIT = 128; // must be one of {128, 120, 112, 104, 96} private static final int IV_LENGTH_BYTE = 12; private static final int SALT_LENGTH_BYTE = 16; private static final Charset UTF_8 = StandardCharsets.UTF_8; // return a base64 encoded AES encrypted text public static String encrypt(byte pText, String password) throws Exception { // 16 bytes salt byte salt = CryptoUtils.getRandomNonce(SALT_LENGTH_BYTE); // GCM recommended 12 bytes iv? byte iv = CryptoUtils.getRandomNonce(IV_LENGTH_BYTE); // secret key from password SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); // ASE-GCM needs GCMParameterSpec cipher.init(Cipher.ENCRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte cipherText = cipher.doFinal(pText); // prefix IV and Salt to cipher text byte cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) .put(iv) .put(salt) .put(cipherText) .array(); // string representation, base64, send this string to other for decryption. return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); } // we need the same password, salt and iv to decrypt it private static String decrypt(String cText, String password) throws Exception { byte decode = Base64.getDecoder().decode(cText.getBytes(UTF_8)); // get back the iv and salt from the cipher text ByteBuffer bb = ByteBuffer.wrap(decode); byte iv = new byte; bb.get(iv); byte salt = new byte; bb.get(salt); byte cipherText = new byte; bb.get(cipherText); // get back the aes key from the same password and salt SecretKey aesKeyFromPassword = CryptoUtils.getAESKeyFromPassword(password.toCharArray(), salt); Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); cipher.init(Cipher.DECRYPT_MODE, aesKeyFromPassword, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); byte plainText = cipher.doFinal(cipherText); return new String(plainText, UTF_8); } public static void main(String args) throws Exception { String OUTPUT_FORMAT = "%-30s:%s"; String PASSWORD = "this is a password"; String pText = "AES-GSM Password-Bases encryption!"; String encryptedTextBase64 = EncryptorAesGcmPassword.encrypt(pText.getBytes(UTF_8), PASSWORD); System.out.println("\n------ AES GCM Password-based Encryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (plain text)", pText)); System.out.println(String.format(OUTPUT_FORMAT, "Encrypted (base64) ", encryptedTextBase64)); System.out.println("\n------ AES GCM Password-based Decryption ------"); System.out.println(String.format(OUTPUT_FORMAT, "Input (base64)", encryptedTextBase64)); String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, PASSWORD); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText)); }}

Output

Terminal
------ AES GCM Password-based Encryption ------Input (plain text) :AES-GSM Password-Bases encryption!Encrypted (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3B------ AES GCM Password-based Decryption ------Input (base64) :KmrvjnMusJTQo/hB7T5BvlQpvi3bVbdjpZP51NT7I/enrIfSQuDfSK6iXgdPzvUP2IE54mwrKiyHqMkG8224lRZ9tXHcclmdh98I8b3BDecrypted (plain text) :AES-GSM Password-Bases encryption!

3.2 Jeśli hasło nie jest zgodne, Java rzuca AEADBadTagException: Tag mismatch!

 // change the password to something else String decryptedText = EncryptorAesGcmPassword.decrypt(encryptedTextBase64, "other password"); System.out.println(String.format(OUTPUT_FORMAT, "Decrypted (plain text)", decryptedText));

Output

Terminal
Exception in thread "main" javax.crypto.AEADBadTagException: Tag mismatch! at java.base/com.sun.crypto.provider.GaloisCounterMode.decryptFinal(GaloisCounterMode.java:623) at java.base/com.sun.crypto.provider.CipherCore.finalNoPadding(CipherCore.java:1118) at java.base/com.sun.crypto.provider.CipherCore.fillOutputBuffer(CipherCore.java:1055) at java.base/com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:855) at java.base/com.sun.crypto.provider.AESCipher.engineDoFinal(AESCipher.java:446) at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2207) at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.decrypt(EncryptorAesGcmPassword.java:88) at com.mkyong.crypto.encryptor.EncryptorAesGcmPassword.main(EncryptorAesGcmPassword.java:109)

Szyfrowanie i deszyfrowanie pliku AES.

Ten przykład to szyfrowanie plików w oparciu o hasło AES. Idee są takie same, ale potrzebujemy kilku klas IO do pracy z zasobami lub plikami.

Tutaj jest plik tekstowy, w folderze resources.

readme.txt
This is line 1.This is line 2.This is line 3.This is line 4.This is line 5.This is line 9.This is line 10.

4.1 Ten przykład jest podobny do 3.1 EncryptorAesGcmPassword.java, z kilkoma drobnymi zmianami, takimi jak zwrócenie byte zamiast zakodowanego łańcucha base64.

 public static byte encrypt(byte pText, String password) throws Exception { //... // prefix IV and Salt to cipher text byte cipherTextWithIvSalt = ByteBuffer.allocate(iv.length + salt.length + cipherText.length) .put(iv) .put(salt) .put(cipherText) .array(); // it works, even if we save the based64 encoded string into a file. // return Base64.getEncoder().encodeToString(cipherTextWithIvSalt); // we save the byte into a file. return cipherTextWithIvSalt; }

Dodaj encryptFile i decryptFile do pracy z plikiem.

 public static void encryptFile(String fromFile, String toFile, String password) throws Exception { // read a normal txt file byte fileContent = Files.readAllBytes(Paths.get(ClassLoader.getSystemResource(fromFile).toURI())); // encrypt with a password byte encryptedText = EncryptorAesGcmPasswordFile.encrypt(fileContent, password); // save a file Path path = Paths.get(toFile); Files.write(path, encryptedText); } public static byte decryptFile(String fromEncryptedFile, String password) throws Exception { // read a file byte fileContent = Files.readAllBytes(Paths.get(fromEncryptedFile)); return EncryptorAesGcmPasswordFile.decrypt(fileContent, password); }

4.2 Odczytaj powyższy plik readme.txt ze ścieżki klas, zaszyfruj go, a zaszyfrowane dane do nowego pliku c:\test\readme.encrypted.txt.

 String password = "password123"; String fromFile = "readme.txt"; // from resources folder String toFile = "c:\\test\\readme.encrypted.txt"; // encrypt file EncryptorAesGcmPasswordFile.encryptFile(fromFile, toFile, password);

Output

Szyfrowanie pliku metodą AES

4.3 Odczytanie zaszyfrowanego pliku, odszyfrowanie go i wydrukowanie danych wyjściowych.

 String password = "password123"; String toFile = "c:\\test\\readme.encrypted.txt"; // decrypt file byte decryptedText = EncryptorAesGcmPasswordFile.decryptFile(toFile, password); String pText = new String(decryptedText, UTF_8); System.out.println(pText);

Wyjście

Terminal
This is line 1.This is line 2.This is line 3.This is line 4.This is line 5.This is line 9.This is line 10.

P.S Szyfrowanie obrazu AES to ta sama koncepcja.

Pobierz kod źródłowy

$ git clone https://github.com/mkyong/core-java

$ cd java-crypto

Daj mi znać, jeśli artykuł wymaga poprawy. Thanks.

  • Wikipedia – Cipher JavaDoc
  • Wikipedia – Cipher Block Chaining (CBC)
  • Wikipedia – Galois/Counter Mode (GCM)
  • Oracle – KeyGenerator Algorithms JavaDoc
  • Java – Jak wygenerować losowe 12 bajtów?
  • Dlaczego nie powinienem używać szyfrowania ECB?
  • Spring Security Crypto Module
  • Wikipedia – PBKDF2
  • RFC 8018 – PKCS
  • Java – Jak dołączyć i podzielić tablicę bajtów
  • Java Security Standard Algorithm Names
  • NIST – Recommendation for Galois/Counter Mode (GCM)
author image

mkyong

Założyciel Mkyong.com, uwielbia Javę i open source. Śledź go na Twitterze. Jeśli podobają Ci się moje tutoriale, rozważ przekazanie darowizny na rzecz tych organizacji charytatywnych.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *