256-bitowe szyfrowanie AES oparte na haśle Java


390

Muszę zaimplementować 256-bitowe szyfrowanie AES, ale wszystkie przykłady, które znalazłem w Internecie, wykorzystują „KeyGenerator” do wygenerowania 256-bitowego klucza, ale chciałbym użyć własnego klucza dostępu. Jak mogę stworzyć własny klucz? Próbowałem uzupełnić go do 256 bitów, ale potem pojawia się błąd informujący, że klucz jest za długi. Mam zainstalowaną łatkę do nieograniczonej jurysdykcji, więc to nie problem :)

To znaczy. KeyGenerator wygląda tak ...

// Get the KeyGenerator
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128); // 192 and 256 bits may not be available

// Generate the secret key specs.
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();

Kod pobrany stąd

EDYTOWAĆ

Właściwie uzupełniałem hasło do 256 bajtów, a nie bitów, co jest zbyt długie. Poniżej znajduje się kod, którego używam teraz, gdy mam z tym więcej doświadczenia.

byte[] key = null; // TODO
byte[] input = null; // TODO
byte[] output = null;
SecretKeySpec keySpec = null;
keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
output = cipher.doFinal(input)

Bity „TODO”, które musisz zrobić sam :-)


Czy możesz wyjaśnić: czy wywołanie kgen.init (256) działa?
Mitch Wheat

2
Tak, ale to automatycznie generuje klucz ... ale ponieważ chcę szyfrować dane między dwoma miejscami, muszę znać klucz wcześniej, więc muszę go określić zamiast „generować”. Mogę określić 16-bitowy, który działa dla 128-bitowego szyfrowania, które działa. Próbowałem 32-bitowego do szyfrowania 256-bitowego, ale nie działało to zgodnie z oczekiwaniami.
Nippysaurus

4
Jeśli dobrze rozumiem, próbujesz użyć wstępnie ułożonego, 256-bitowego klucza, określonego na przykład jako tablica bajtów. Jeśli tak, podejście DarkSquid przy użyciu SecretKeySpec powinno działać. Możliwe jest również uzyskanie klucza AES z hasła; jeśli o to ci chodzi, daj mi znać, a pokażę ci, jak to zrobić; samo wymyślenie hasła nie jest najlepszą praktyką.
erickson

Zachowaj ostrożność podczas uzupełniania numeru, ponieważ możesz sprawić, że AES będzie mniej bezpieczny.
Joshua

1
@erickson: właśnie to muszę zrobić (wyprowadzić klucz AES z hasła).
Nippysaurus

Odpowiedzi:


475

Udostępnij password(a char[]) i salt(- byte[]8 bajtów wybranych przez SecureRandommarkę stanowi dobrą sól - co nie musi być utrzymywane w tajemnicy) odbiorcy spoza pasma. Następnie, aby uzyskać dobry klucz z tych informacji:

/* Derive the key, given password and salt. */
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

Liczby magiczne (które można gdzieś zdefiniować jako stałe) 65536 i 256 to odpowiednio liczba iteracji pochodnych kluczy i rozmiar klucza.

Funkcja wyprowadzania klucza jest iterowana, co wymaga znacznego wysiłku obliczeniowego, co uniemożliwia atakującym szybkie wypróbowanie wielu różnych haseł. Liczbę iteracji można zmienić w zależności od dostępnych zasobów obliczeniowych.

Rozmiar klucza można zmniejszyć do 128 bitów, co nadal jest uważane za „silne” szyfrowanie, ale nie zapewnia większego marginesu bezpieczeństwa, jeśli zostaną wykryte ataki osłabiające AES.

W tym samym kluczu pochodnym, używanym z odpowiednim trybem łączenia bloków, można szyfrować wiele wiadomości. W łańcuchu bloków szyfrów (CBC) dla każdej wiadomości generowany jest losowy wektor inicjalizacji (IV), co daje inny tekst szyfru, nawet jeśli zwykły tekst jest identyczny. CBC może nie być najbezpieczniejszym dostępnym trybem (patrz AEAD poniżej); istnieje wiele innych trybów o różnych właściwościach bezpieczeństwa, ale wszystkie wykorzystują podobne losowe dane wejściowe. W każdym przypadku wyjściami każdej operacji szyfrowania są tekst zaszyfrowany i wektor inicjalizacji:

/* Encrypt the message. */
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret);
AlgorithmParameters params = cipher.getParameters();
byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
byte[] ciphertext = cipher.doFinal("Hello, World!".getBytes("UTF-8"));

Przechowuj ciphertexti iv. Podczas deszyfrowania plik SecretKeyjest regenerowany dokładnie w ten sam sposób, przy użyciu hasła o tych samych parametrach soli i iteracji. Zainicjuj szyfr za pomocą tego klucza i wektora inicjalizacji zapisanego z komunikatem:

/* Decrypt the message, given derived key and initialization vector. */
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
String plaintext = new String(cipher.doFinal(ciphertext), "UTF-8");
System.out.println(plaintext);

Java 7 zawiera obsługę API dla trybów szyfrowania AEAD , a dostawca „SunJCE” dołączony do dystrybucji OpenJDK i Oracle implementuje je począwszy od Java 8. Jeden z tych trybów jest zdecydowanie zalecany zamiast CBC; zapewni ochronę integralności danych, a także ich prywatności.


A java.security.InvalidKeyExceptionz komunikatem „Nielegalny rozmiar klucza lub parametry domyślne” oznacza, że ​​siła kryptografii jest ograniczona; pliki zasad jurysdykcji o nieograniczonej sile nie znajdują się we właściwej lokalizacji. W JDK należy je umieścić pod${jdk}/jre/lib/security

Na podstawie opisu problemu wygląda na to, że pliki zasad nie są poprawnie zainstalowane. Systemy mogą łatwo mieć wiele środowisk wykonawczych Java; sprawdź dwukrotnie, aby upewnić się, że używana jest poprawna lokalizacja.


29
@Nick: Przeczytaj PKCS # 5. Sole są niezbędne dla PBKDF2, dlatego API do szyfrowania opartego na hasłach wymaga ich jako danych wejściowych do uzyskiwania klucza. Bez soli można zastosować atak słownikowy, umożliwiając wstępnie obliczoną listę najbardziej prawdopodobnych symetrycznych kluczy szyfrujących. Szyfrowane IV i sole pochodnych kluczy służą różnym celom. IV umożliwiają ponowne użycie tego samego klucza dla wielu wiadomości. Sole zapobiegają atakom słownikowym na klucz.
erickson

2
Po pierwsze, byłoby to szyfrowanie DES, a nie AES. Większość dostawców nie ma dobrego wsparcia dla PBEwith<prf>and<encryption>algorytmów; na przykład SunJCE nie zapewnia i PBE dla AES. Po drugie, włączenie jasypt nie jest celem. Pakiet, który ma zapewniać bezpieczeństwo bez konieczności rozumienia podstawowych zasad, wydaje się niebezpieczny prima facie.
erickson

6
Zaimplementowałem odpowiedź @ erickson jako klasa: github.com/mrclay/jSecureEdit/tree/master/src/org/mrclay/crypto (PBE działa, PBEStorage jest obiektem wartościowym do przechowywania IV / szyfrogramu razem).
Steve Clay,

3
@AndyNuss Ten przykład dotyczy szyfrowania odwracalnego, które zasadniczo nie powinno być używane do haseł. Ty może użyć klucza wyprowadzenie PBKDF2 do „hash” hasłami bezpiecznie. Oznacza to, że w powyższym przykładzie zapisujesz wynik tmp.getEncoded()jako skrót. Powinieneś także przechowywać salti iteracje (65536 w tym przykładzie), abyś mógł ponownie obliczyć skrót, gdy ktoś próbuje się uwierzytelnić. W takim przypadku wygeneruj sól za pomocą kryptograficznego generatora liczb losowych przy każdej zmianie hasła.
erickson,

6
Aby uruchomić ten kod, upewnij się, że masz odpowiednie pliki zasad nieograniczonej jurysdykcji w swoim środowisku JRE, jak podano w ngs.ac.uk/tools/jcepolicyfiles
Amir Moghimi,

75

Rozważ użycie modułu szyfrującego Spring Security

Moduł Spring Security Crypto zapewnia obsługę szyfrowania symetrycznego, generowania kluczy i kodowania hasła. Kod jest rozpowszechniany jako część modułu podstawowego, ale nie jest zależny od żadnego innego kodu Spring Security (lub Spring).

Zapewnia prostą abstrakcję do szyfrowania i wydaje się pasować do wymaganych tutaj,

„Standardową” metodą szyfrowania jest 256-bitowy AES z wykorzystaniem PBKDF2 PKCS nr 5 (funkcja 2). Ta metoda wymaga Java 6. Hasło użyte do wygenerowania SecretKey powinno być przechowywane w bezpiecznym miejscu i nie może być udostępniane. Sól służy do zapobiegania atakom słownikowym na klucz w przypadku naruszenia bezpieczeństwa zaszyfrowanych danych. Stosowany jest również 16-bajtowy wektor losowej inicjalizacji, dzięki czemu każda zaszyfrowana wiadomość jest unikalna.

Spojrzenie na elementy wewnętrzne ujawnia strukturę podobną do odpowiedzi Ericksona .

Jak wspomniano w pytaniu, wymaga to również zasad jurysdykcji nieograniczonej siły Java Cryptography Extension (JCE) (w przeciwnym razie napotkasz InvalidKeyException: Illegal Key Size). Jest do pobrania dla Java 6 , Java 7 i Java 8 .

Przykładowe użycie

import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.security.crypto.keygen.KeyGenerators;

public class CryptoExample {
    public static void main(String[] args) {
        final String password = "I AM SHERLOCKED";  
        final String salt = KeyGenerators.string().generateKey();

        TextEncryptor encryptor = Encryptors.text(password, salt);      
        System.out.println("Salt: \"" + salt + "\"");

        String textToEncrypt = "*royal secrets*";
        System.out.println("Original text: \"" + textToEncrypt + "\"");

        String encryptedText = encryptor.encrypt(textToEncrypt);
        System.out.println("Encrypted text: \"" + encryptedText + "\"");

        // Could reuse encryptor but wanted to show reconstructing TextEncryptor
        TextEncryptor decryptor = Encryptors.text(password, salt);
        String decryptedText = decryptor.decrypt(encryptedText);
        System.out.println("Decrypted text: \"" + decryptedText + "\"");

        if(textToEncrypt.equals(decryptedText)) {
            System.out.println("Success: decrypted text matches");
        } else {
            System.out.println("Failed: decrypted text does not match");
        }       
    }
}

I próbka wyjściowa,

Sól: „feacbc02a3a697b0”
Tekst oryginalny: „* tajemnice królewskie *”
Zaszyfrowany tekst: „7c73c5a83fa580b5d6f8208768adc931ef3123291ac8bc335a1277a39d256d9a” 
Odszyfrowany tekst: „* tajemnice królewskie *”
Sukces: odszyfrowane dopasowania tekstu

Czy możesz użyć tego modułu bez ładowania całej wiosny? Wygląda na to, że nie udostępniono plików jar do pobrania.
theglauber

5
@theglauber Tak, możesz używać modułu bez Spring Security lub Spring Framework. Patrząc na pom , jedyną zależnością w czasie wykonywania jest apache commons-log 1.1.1 . Możesz pobrać słoik z maven lub pobrać go bezpośrednio z oficjalnego repozytorium binarnego (zobacz Pobieranie plików binarnych Spring 4, aby uzyskać więcej informacji na temat plików binarnych Spring).
John McCarthy

1
Czy można ustawić długość klucza na 128-bitów? Modyfikowanie folderu zabezpieczeń na każdym komputerze nie jest dla mnie opcją.
IvanRF,

1
@IvanRF przepraszam, nie wygląda na to. 256 jest na stałe zakodowane w źródle
John McCarthy

2
NULL_IV_GENERATORWykorzystywane przez narzędzie sprężyna nie jest bezpieczna. Jeśli aplikacja nie zapewnia IV, pozwól dostawcy wybrać go i zapytaj go po inicjalizacji.
erickson

32

Po przeczytaniu sugestii Ericksona i zebraniu, co mogłem z kilku innych postów i tego przykładu tutaj , próbowałem zaktualizować kod Douga zalecanymi zmianami. Edytuj, aby poprawić.

  • Wektor inicjalizacji nie jest już naprawiony
  • klucz szyfrowania jest uzyskiwany przy użyciu kodu z erickson
  • 8 bajtów soli jest generowanych w setupEncrypt () przy użyciu SecureRandom ()
  • klucz deszyfrujący jest generowany z soli szyfrującej i hasła
  • szyfr deszyfrujący jest generowany z klucza deszyfrującego i wektora inicjalizacji
  • usunięto kręcenie heksów zamiast org.apache.commons kodeków Hex procedur

Kilka uwag: Używa 128-bitowego klucza szyfrowania - Java najwyraźniej nie wykona 256-bitowego szyfrowania od razu po wyjęciu z pudełka. Implementacja 256 wymaga zainstalowania dodatkowych plików w katalogu instalacyjnym Java.

Poza tym nie jestem osobą kryptograficzną. Uważajcie.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

public class Crypto
{
    String mPassword = null;
    public final static int SALT_LEN = 8;
    byte [] mInitVec = null;
    byte [] mSalt = null;
    Cipher mEcipher = null;
    Cipher mDecipher = null;
    private final int KEYLEN_BITS = 128; // see notes below where this is used.
    private final int ITERATIONS = 65536;
    private final int MAX_FILE_BUF = 1024;

    /**
     * create an object with just the passphrase from the user. Don't do anything else yet 
     * @param password
     */
    public Crypto (String password)
    {
        mPassword = password;
    }

    /**
     * return the generated salt for this object
     * @return
     */
    public byte [] getSalt ()
    {
        return (mSalt);
    }

    /**
     * return the initialization vector created from setupEncryption
     * @return
     */
    public byte [] getInitVec ()
    {
        return (mInitVec);
    }

    /**
     * debug/print messages
     * @param msg
     */
    private void Db (String msg)
    {
        System.out.println ("** Crypt ** " + msg);
    }

    /**
     * this must be called after creating the initial Crypto object. It creates a salt of SALT_LEN bytes
     * and generates the salt bytes using secureRandom().  The encryption secret key is created 
     * along with the initialization vectory. The member variable mEcipher is created to be used
     * by the class later on when either creating a CipherOutputStream, or encrypting a buffer
     * to be written to disk.
     *  
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws NoSuchPaddingException
     * @throws InvalidParameterSpecException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws UnsupportedEncodingException
     * @throws InvalidKeyException
     */
    public void setupEncrypt () throws NoSuchAlgorithmException, 
                                                           InvalidKeySpecException, 
                                                           NoSuchPaddingException, 
                                                           InvalidParameterSpecException, 
                                                           IllegalBlockSizeException, 
                                                           BadPaddingException, 
                                                           UnsupportedEncodingException, 
                                                           InvalidKeyException
    {
        SecretKeyFactory factory = null;
        SecretKey tmp = null;

        // crate secureRandom salt and store  as member var for later use
         mSalt = new byte [SALT_LEN];
        SecureRandom rnd = new SecureRandom ();
        rnd.nextBytes (mSalt);
        Db ("generated salt :" + Hex.encodeHexString (mSalt));

        factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

        /* Derive the key, given password and salt. 
         * 
         * in order to do 256 bit crypto, you have to muck with the files for Java's "unlimted security"
         * The end user must also install them (not compiled in) so beware. 
         * see here:  http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml
         */
        KeySpec spec = new PBEKeySpec (mPassword.toCharArray (), mSalt, ITERATIONS, KEYLEN_BITS);
        tmp = factory.generateSecret (spec);
        SecretKey secret = new SecretKeySpec (tmp.getEncoded(), "AES");

        /* Create the Encryption cipher object and store as a member variable
         */
        mEcipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
        mEcipher.init (Cipher.ENCRYPT_MODE, secret);
        AlgorithmParameters params = mEcipher.getParameters ();

        // get the initialization vectory and store as member var 
        mInitVec = params.getParameterSpec (IvParameterSpec.class).getIV();

        Db ("mInitVec is :" + Hex.encodeHexString (mInitVec));
    }



    /**
     * If a file is being decrypted, we need to know the pasword, the salt and the initialization vector (iv). 
     * We have the password from initializing the class. pass the iv and salt here which is
     * obtained when encrypting the file initially.
     *   
     * @param initvec
     * @param salt
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     * @throws NoSuchPaddingException
     * @throws InvalidKeyException
     * @throws InvalidAlgorithmParameterException
     * @throws DecoderException
     */
    public void setupDecrypt (String initvec, String salt) throws NoSuchAlgorithmException, 
                                                                                       InvalidKeySpecException, 
                                                                                       NoSuchPaddingException, 
                                                                                       InvalidKeyException, 
                                                                                       InvalidAlgorithmParameterException, 
                                                                                       DecoderException
    {
        SecretKeyFactory factory = null;
        SecretKey tmp = null;
        SecretKey secret = null;

        // since we pass it as a string of input, convert to a actual byte buffer here
        mSalt = Hex.decodeHex (salt.toCharArray ());
       Db ("got salt " + Hex.encodeHexString (mSalt));

        // get initialization vector from passed string
        mInitVec = Hex.decodeHex (initvec.toCharArray ());
        Db ("got initvector :" + Hex.encodeHexString (mInitVec));


        /* Derive the key, given password and salt. */
        // in order to do 256 bit crypto, you have to muck with the files for Java's "unlimted security"
        // The end user must also install them (not compiled in) so beware. 
        // see here: 
      // http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml
        factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        KeySpec spec = new PBEKeySpec(mPassword.toCharArray (), mSalt, ITERATIONS, KEYLEN_BITS);

        tmp = factory.generateSecret(spec);
        secret = new SecretKeySpec(tmp.getEncoded(), "AES");

        /* Decrypt the message, given derived key and initialization vector. */
        mDecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        mDecipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(mInitVec));
    }


    /**
     * This is where we write out the actual encrypted data to disk using the Cipher created in setupEncrypt().
     * Pass two file objects representing the actual input (cleartext) and output file to be encrypted.
     * 
     * there may be a way to write a cleartext header to the encrypted file containing the salt, but I ran
     * into uncertain problems with that. 
     *  
     * @param input - the cleartext file to be encrypted
     * @param output - the encrypted data file
     * @throws IOException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public void WriteEncryptedFile (File input, File output) throws 
                                                                                          IOException, 
                                                                                          IllegalBlockSizeException, 
                                                                                          BadPaddingException
    {
        FileInputStream fin;
        FileOutputStream fout;
        long totalread = 0;
        int nread = 0;
        byte [] inbuf = new byte [MAX_FILE_BUF];

        fout = new FileOutputStream (output);
        fin = new FileInputStream (input);

        while ((nread = fin.read (inbuf)) > 0 )
        {
            Db ("read " + nread + " bytes");
            totalread += nread;

            // create a buffer to write with the exact number of bytes read. Otherwise a short read fills inbuf with 0x0
            // and results in full blocks of MAX_FILE_BUF being written. 
            byte [] trimbuf = new byte [nread];
            for (int i = 0; i < nread; i++)
                trimbuf[i] = inbuf[i];

            // encrypt the buffer using the cipher obtained previosly
            byte [] tmp = mEcipher.update (trimbuf);

            // I don't think this should happen, but just in case..
            if (tmp != null)
                fout.write (tmp);
        }

        // finalize the encryption since we've done it in blocks of MAX_FILE_BUF
        byte [] finalbuf = mEcipher.doFinal ();
        if (finalbuf != null)
            fout.write (finalbuf);

        fout.flush();
        fin.close();
        fout.close();

        Db ("wrote " + totalread + " encrypted bytes");
    }


    /**
     * Read from the encrypted file (input) and turn the cipher back into cleartext. Write the cleartext buffer back out
     * to disk as (output) File.
     * 
     * I left CipherInputStream in here as a test to see if I could mix it with the update() and final() methods of encrypting
     *  and still have a correctly decrypted file in the end. Seems to work so left it in.
     *  
     * @param input - File object representing encrypted data on disk 
     * @param output - File object of cleartext data to write out after decrypting
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws IOException
     */
    public void ReadEncryptedFile (File input, File output) throws 
                                                                                                                                            IllegalBlockSizeException, 
                                                                                                                                            BadPaddingException, 
                                                                                                                                            IOException
    {
        FileInputStream fin; 
        FileOutputStream fout;
        CipherInputStream cin;
        long totalread = 0;
        int nread = 0;
        byte [] inbuf = new byte [MAX_FILE_BUF];

        fout = new FileOutputStream (output);
        fin = new FileInputStream (input);

        // creating a decoding stream from the FileInputStream above using the cipher created from setupDecrypt()
        cin = new CipherInputStream (fin, mDecipher);

        while ((nread = cin.read (inbuf)) > 0 )
        {
            Db ("read " + nread + " bytes");
            totalread += nread;

            // create a buffer to write with the exact number of bytes read. Otherwise a short read fills inbuf with 0x0
            byte [] trimbuf = new byte [nread];
            for (int i = 0; i < nread; i++)
                trimbuf[i] = inbuf[i];

            // write out the size-adjusted buffer
            fout.write (trimbuf);
        }

        fout.flush();
        cin.close();
        fin.close ();       
        fout.close();   

        Db ("wrote " + totalread + " encrypted bytes");
    }


    /**
     * adding main() for usage demonstration. With member vars, some of the locals would not be needed
     */
    public static void main(String [] args)
    {

        // create the input.txt file in the current directory before continuing
        File input = new File ("input.txt");
        File eoutput = new File ("encrypted.aes");
        File doutput = new File ("decrypted.txt");
        String iv = null;
        String salt = null;
        Crypto en = new Crypto ("mypassword");

        /*
         * setup encryption cipher using password. print out iv and salt
         */
        try
      {
          en.setupEncrypt ();
          iv = Hex.encodeHexString (en.getInitVec ()).toUpperCase ();
          salt = Hex.encodeHexString (en.getSalt ()).toUpperCase ();
      }
      catch (InvalidKeyException e)
      {
          e.printStackTrace();
      }
      catch (NoSuchAlgorithmException e)
      {
          e.printStackTrace();
      }
      catch (InvalidKeySpecException e)
      {
          e.printStackTrace();
      }
      catch (NoSuchPaddingException e)
      {
          e.printStackTrace();
      }
      catch (InvalidParameterSpecException e)
      {
          e.printStackTrace();
      }
      catch (IllegalBlockSizeException e)
      {
          e.printStackTrace();
      }
      catch (BadPaddingException e)
      {
          e.printStackTrace();
      }
      catch (UnsupportedEncodingException e)
      {
          e.printStackTrace();
      }

        /*
         * write out encrypted file
         */
        try
      {
          en.WriteEncryptedFile (input, eoutput);
          System.out.printf ("File encrypted to " + eoutput.getName () + "\niv:" + iv + "\nsalt:" + salt + "\n\n");
      }
      catch (IllegalBlockSizeException e)
      {
          e.printStackTrace();
      }
      catch (BadPaddingException e)
      {
          e.printStackTrace();
      }
      catch (IOException e)
      {
          e.printStackTrace();
      }


        /*
         * decrypt file
         */
        Crypto dc = new Crypto ("mypassword");
        try
      {
          dc.setupDecrypt (iv, salt);
      }
      catch (InvalidKeyException e)
      {
          e.printStackTrace();
      }
      catch (NoSuchAlgorithmException e)
      {
          e.printStackTrace();
      }
      catch (InvalidKeySpecException e)
      {
          e.printStackTrace();
      }
      catch (NoSuchPaddingException e)
      {
          e.printStackTrace();
      }
      catch (InvalidAlgorithmParameterException e)
      {
          e.printStackTrace();
      }
      catch (DecoderException e)
      {
          e.printStackTrace();
      }

        /*
         * write out decrypted file
         */
        try
      {
          dc.ReadEncryptedFile (eoutput, doutput);
          System.out.println ("decryption finished to " + doutput.getName ());
      }
      catch (IllegalBlockSizeException e)
      {
          e.printStackTrace();
      }
      catch (BadPaddingException e)
      {
          e.printStackTrace();
      }
      catch (IOException e)
      {
          e.printStackTrace();
      }
   }


}

13
Jest to w zasadzie ta sama odpowiedź, co odpowiedź Ericksona, otoczona - nie tak dobrze zaprogramowanym - moim zdaniem - opakowaniem. printStackTrace()
Maarten Bodewes

2
@owlstead - To świetna odpowiedź. Pokazuje, jak zaszyfrować strumień, szyfrując bufor bajtów, zamiast mieć wszystko w pamięci. Odpowiedź Ericksona nie będzie działać w przypadku dużych plików, które nie mieszczą się w pamięci. Więc +1 do wufoo. :)
dynamokaj

2
@dynamokaj Korzystanie z CipherInputStreami CipherOutputStreamnie stanowi większego problemu. Problemem jest tasowanie wszystkich wyjątków pod stołem. Problem polega na tym, że sól nagle stała się polem i że wymagane jest podanie IV. Problem polega na tym, że nie jest zgodny z konwencjami kodowania Java. Problemem jest fakt, że działa to tylko na plikach, o które nie zapytano. To, że reszta kodu jest w zasadzie kopią, również nie pomaga. Ale może poprawię to, aby było lepiej, jak sugerowano ...
Maarten Bodewes

@owlstead Zgadzam się, że kodowanie mogłoby wyglądać lepiej, zmniejszyłem go do 1/4 lub coś takiego, ale podoba mi się, że przedstawił mnie do CipherInputStream i CipherOutputStream, ponieważ tego właśnie potrzebowałem wczoraj! ;)
dynamokaj

dlaczego dwa razy? fout.close (); fout.close ();
Marian Paździoch

7

Generowanie własnego klucza z tablicy bajtów jest łatwe:

byte[] raw = ...; // 32 bytes in size for a 256 bit key
Key skey = new javax.crypto.spec.SecretKeySpec(raw, "AES");

Ale utworzenie 256-bitowego klucza nie wystarczy. Jeśli generator kluczy nie może wygenerować 256-bitowych kluczy, Cipherklasa prawdopodobnie nie obsługuje również 256-bitowego AES. Mówisz, że masz zainstalowaną nielimitowaną łatkę jurysdykcyjną, więc szyfr AES-256 powinien być obsługiwany (ale wtedy klucze 256-bitowe też powinny być, więc może to być problem z konfiguracją).

Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skey);
byte[] encrypted = cipher.doFinal(plainText.getBytes());

Obejściem braku obsługi AES-256 jest wzięcie darmowej implementacji AES-256 i wykorzystanie jej jako niestandardowego dostawcy. Wymaga to stworzenia własnej Providerpodklasy i korzystania z niej Cipher.getInstance(String, Provider). Ale może to być proces zaangażowany.


5
Zawsze powinieneś wskazać tryb i algorytm wypełniania. Java domyślnie używa niebezpiecznego trybu EBC.
Maarten Bodewes

Nie możesz stworzyć własnego dostawcy, dostawcy muszą być podpisani (nie mogę uwierzyć, że początkowo przeczytałem ten błąd). Nawet jeśli możesz, ograniczenie wielkości klucza dotyczy implementacji Cipher, a nie samego dostawcy. Możesz używać AES-256 w Javie 8 i niższych, ale musisz użyć zastrzeżonego API. Lub środowisko wykonawcze, które nie stwarza ograniczeń co do wielkości klucza.
Maarten Bodewes

Najnowsze wersje OpenJDK (i Androida) nie mają ograniczeń dotyczących dodawania własnego dostawcy zabezpieczeń / kryptografii. Oczywiście robisz to na własne ryzyko. Jeśli zapomnisz aktualizować swoje biblioteki, możesz narazić się na zagrożenia bezpieczeństwa.
Maarten Bodewes

1
@ MaartenBodewes + OpenJDK nigdy nie miał problemu z ograniczoną polityką kryptograficzną, a Oracle JDK usunął go ponad rok temu dla wersji 8u161 i 9 (i może niektórych niższych wersji tylko teraz płatnych, ale ich nie sprawdziłem)
dave_thompson_085,

6

W przeszłości robiłem haszowanie klucza za pomocą czegoś takiego jak SHA256, a następnie wyodrębnianie bajtów z hasza do bajtu klucza [].

Po utworzeniu bajtu [] możesz po prostu zrobić:

SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(clearText.getBytes());

12
Dla innych: nie jest to bardzo bezpieczna metoda. Powinieneś użyć PBKDF 2 określonego w PKCS # 5. erickson powiedział, jak to zrobić powyżej. Metoda DarkSquid jest podatna na ataki hasłem i również nie działa, chyba że rozmiar twojego zwykłego tekstu jest wielokrotnością wielkości bloku AES (128 bitów), ponieważ pominął wypełnianie. Nie określa też trybu; przeczytaj zaniepokojone tryby działania blokowego szyfru Wikipedii.
Hut8

1
@DarkSquid Cipher aes256 = Cipher.getInstance("AES/OFB/NoPadding"); MessageDigest keyDigest = MessageDigest.getInstance("SHA-256"); byte[] keyHash = keyDigest.digest(secret.getBytes("UTF-8")); SecretKeySpec key = new SecretKeySpec(keyHash, "AES"); aes256.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(initializationVector)); Robię to samo, co zasugerowałem w Twojej odpowiedzi, ale nadal mam do czynienia z java.security.InvalidKeyException: Nielegalny rozmiar klucza Czy pobieranie pliku zasad JCE jest obowiązkowe?
Niranjan Subramanian

2
NIE UŻYWAJ tej metody w żadnym środowisku produkcyjnym. Zaczynając od szyfrowania opartego na hasłach, wielu użytkowników jest przytłoczonych ścianami kodu i nie rozumie, jak działają ataki słownikowe i inne proste ataki hakerskie. Uczenie się może być frustrujące, ale warto to zbadać. Oto dobry artykuł dla początkujących: adambard.com/blog/3-wrong-ways-to-store-a-password
IcedDante

1

Dodając do edycji @ Wufoo, następująca wersja używa InputStreams zamiast plików, aby ułatwić pracę z różnymi plikami. Przechowuje również IV i sól na początku pliku, dzięki czemu tylko hasło musi być śledzone. Ponieważ IV i Sól nie muszą być tajne, to ułatwia życie.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public class AES {
    public final static int SALT_LEN     = 8;
    static final String     HEXES        = "0123456789ABCDEF";
    String                  mPassword    = null;
    byte[]                  mInitVec     = null;
    byte[]                  mSalt        = new byte[SALT_LEN];
    Cipher                  mEcipher     = null;
    Cipher                  mDecipher    = null;
    private final int       KEYLEN_BITS  = 128;    // see notes below where this is used.
    private final int       ITERATIONS   = 65536;
    private final int       MAX_FILE_BUF = 1024;

    /**
     * create an object with just the passphrase from the user. Don't do anything else yet
     * @param password
     */
    public AES(String password) {
        mPassword = password;
    }

    public static String byteToHex(byte[] raw) {
        if (raw == null) {
            return null;
        }

        final StringBuilder hex = new StringBuilder(2 * raw.length);

        for (final byte b : raw) {
            hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
        }

        return hex.toString();
    }

    public static byte[] hexToByte(String hexString) {
        int    len = hexString.length();
        byte[] ba  = new byte[len / 2];

        for (int i = 0; i < len; i += 2) {
            ba[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4)
                                + Character.digit(hexString.charAt(i + 1), 16));
        }

        return ba;
    }

    /**
     * debug/print messages
     * @param msg
     */
    private void Db(String msg) {
        System.out.println("** Crypt ** " + msg);
    }

    /**
     * This is where we write out the actual encrypted data to disk using the Cipher created in setupEncrypt().
     * Pass two file objects representing the actual input (cleartext) and output file to be encrypted.
     *
     * there may be a way to write a cleartext header to the encrypted file containing the salt, but I ran
     * into uncertain problems with that.
     *
     * @param input - the cleartext file to be encrypted
     * @param output - the encrypted data file
     * @throws IOException
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     */
    public void WriteEncryptedFile(InputStream inputStream, OutputStream outputStream)
            throws IOException, IllegalBlockSizeException, BadPaddingException {
        try {
            long             totalread = 0;
            int              nread     = 0;
            byte[]           inbuf     = new byte[MAX_FILE_BUF];
            SecretKeyFactory factory   = null;
            SecretKey        tmp       = null;

            // crate secureRandom salt and store  as member var for later use
            mSalt = new byte[SALT_LEN];

            SecureRandom rnd = new SecureRandom();

            rnd.nextBytes(mSalt);
            Db("generated salt :" + byteToHex(mSalt));
            factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

            /*
             *  Derive the key, given password and salt.
             *
             * in order to do 256 bit crypto, you have to muck with the files for Java's "unlimted security"
             * The end user must also install them (not compiled in) so beware.
             * see here:  http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml
             */
            KeySpec spec = new PBEKeySpec(mPassword.toCharArray(), mSalt, ITERATIONS, KEYLEN_BITS);

            tmp = factory.generateSecret(spec);

            SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

            /*
             *  Create the Encryption cipher object and store as a member variable
             */
            mEcipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            mEcipher.init(Cipher.ENCRYPT_MODE, secret);

            AlgorithmParameters params = mEcipher.getParameters();

            // get the initialization vectory and store as member var
            mInitVec = params.getParameterSpec(IvParameterSpec.class).getIV();
            Db("mInitVec is :" + byteToHex(mInitVec));
            outputStream.write(mSalt);
            outputStream.write(mInitVec);

            while ((nread = inputStream.read(inbuf)) > 0) {
                Db("read " + nread + " bytes");
                totalread += nread;

                // create a buffer to write with the exact number of bytes read. Otherwise a short read fills inbuf with 0x0
                // and results in full blocks of MAX_FILE_BUF being written.
                byte[] trimbuf = new byte[nread];

                for (int i = 0; i < nread; i++) {
                    trimbuf[i] = inbuf[i];
                }

                // encrypt the buffer using the cipher obtained previosly
                byte[] tmpBuf = mEcipher.update(trimbuf);

                // I don't think this should happen, but just in case..
                if (tmpBuf != null) {
                    outputStream.write(tmpBuf);
                }
            }

            // finalize the encryption since we've done it in blocks of MAX_FILE_BUF
            byte[] finalbuf = mEcipher.doFinal();

            if (finalbuf != null) {
                outputStream.write(finalbuf);
            }

            outputStream.flush();
            inputStream.close();
            outputStream.close();
            outputStream.close();
            Db("wrote " + totalread + " encrypted bytes");
        } catch (InvalidKeyException ex) {
            Logger.getLogger(AES.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InvalidParameterSpecException ex) {
            Logger.getLogger(AES.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchAlgorithmException ex) {
            Logger.getLogger(AES.class.getName()).log(Level.SEVERE, null, ex);
        } catch (NoSuchPaddingException ex) {
            Logger.getLogger(AES.class.getName()).log(Level.SEVERE, null, ex);
        } catch (InvalidKeySpecException ex) {
            Logger.getLogger(AES.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Read from the encrypted file (input) and turn the cipher back into cleartext. Write the cleartext buffer back out
     * to disk as (output) File.
     *
     * I left CipherInputStream in here as a test to see if I could mix it with the update() and final() methods of encrypting
     *  and still have a correctly decrypted file in the end. Seems to work so left it in.
     *
     * @param input - File object representing encrypted data on disk
     * @param output - File object of cleartext data to write out after decrypting
     * @throws IllegalBlockSizeException
     * @throws BadPaddingException
     * @throws IOException
     */
    public void ReadEncryptedFile(InputStream inputStream, OutputStream outputStream)
            throws IllegalBlockSizeException, BadPaddingException, IOException {
        try {
            CipherInputStream cin;
            long              totalread = 0;
            int               nread     = 0;
            byte[]            inbuf     = new byte[MAX_FILE_BUF];

            // Read the Salt
            inputStream.read(this.mSalt);
            Db("generated salt :" + byteToHex(mSalt));

            SecretKeyFactory factory = null;
            SecretKey        tmp     = null;
            SecretKey        secret  = null;

            factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

            KeySpec spec = new PBEKeySpec(mPassword.toCharArray(), mSalt, ITERATIONS, KEYLEN_BITS);

            tmp    = factory.generateSecret(spec);
            secret = new SecretKeySpec(tmp.getEncoded(), "AES");

            /* Decrypt the message, given derived key and initialization vector. */
            mDecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");

            // Set the appropriate size for mInitVec by Generating a New One
            AlgorithmParameters params = mDecipher.getParameters();

            mInitVec = params.getParameterSpec(IvParameterSpec.class).getIV();

            // Read the old IV from the file to mInitVec now that size is set.
            inputStream.read(this.mInitVec);
            Db("mInitVec is :" + byteToHex(mInitVec));
            mDecipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(mInitVec));

            // creating a decoding stream from the FileInputStream above using the cipher created from setupDecrypt()
            cin = new CipherInputStream(inputStream, mDecipher);

            while ((nread = cin.read(inbuf)) > 0) {
                Db("read " + nread + " bytes");
                totalread += nread;

                // create a buffer to write with the exact number of bytes read. Otherwise a short read fills inbuf with 0x0
                byte[] trimbuf = new byte[nread];

                for (int i = 0; i < nread; i++) {
                    trimbuf[i] = inbuf[i];
                }

                // write out the size-adjusted buffer
                outputStream.write(trimbuf);
            }

            outputStream.flush();
            cin.close();
            inputStream.close();
            outputStream.close();
            Db("wrote " + totalread + " encrypted bytes");
        } catch (Exception ex) {
            Logger.getLogger(AES.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * adding main() for usage demonstration. With member vars, some of the locals would not be needed
     */
    public static void main(String[] args) {

        // create the input.txt file in the current directory before continuing
        File   input   = new File("input.txt");
        File   eoutput = new File("encrypted.aes");
        File   doutput = new File("decrypted.txt");
        String iv      = null;
        String salt    = null;
        AES    en      = new AES("mypassword");

        /*
         * write out encrypted file
         */
        try {
            en.WriteEncryptedFile(new FileInputStream(input), new FileOutputStream(eoutput));
            System.out.printf("File encrypted to " + eoutput.getName() + "\niv:" + iv + "\nsalt:" + salt + "\n\n");
        } catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
            e.printStackTrace();
        }

        /*
         * decrypt file
         */
        AES dc = new AES("mypassword");

        /*
         * write out decrypted file
         */
        try {
            dc.ReadEncryptedFile(new FileInputStream(eoutput), new FileOutputStream(doutput));
            System.out.println("decryption finished to " + doutput.getName());
        } catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
            e.printStackTrace();
        }
    }
}

1
Wydaje się, że to rozwiązanie korzysta z niezręcznej obsługi buforów i obsługi absolutnie niedopracowanych wyjątków, po prostu rejestrując je, a następnie zapominając o nich. Ostrzegamy, że używanie CBC jest odpowiednie dla plików, ale nie dla bezpieczeństwa transportu. Przy użyciu PBKDF2 i AES można oczywiście bronić, w tym sensie może to być dobry fundament dla rozwiązania.
Maarten Bodewes

1

(Może pomocny dla innych osób o podobnych wymaganiach)

Miałem podobny wymóg używania AES-256-CBCszyfrowania i deszyfrowania w Javie.

Aby osiągnąć (lub określić) 256-bajtowe szyfrowanie / deszyfrowanie, Java Cryptography Extension (JCE)zasady powinny być ustawione na"Unlimited"

Można go ustawić w java.securitypliku w $JAVA_HOME/jre/lib/security(dla JDK) lub $JAVA_HOME/lib/security(dla JRE)

crypto.policy=unlimited

Lub w kodzie jako

Security.setProperty("crypto.policy", "unlimited");

Java 9 i nowsze wersje mają tę opcję domyślnie włączoną.


0

Rozważ użycie Encryptor4j, którego jestem autorem.

Najpierw upewnij się, że masz zainstalowane pliki zasad nieograniczonej jurysdykcji , aby móc używać 256-bitowych kluczy AES.

Następnie wykonaj następujące czynności:

String password = "mysupersecretpassword"; 
Key key = KeyFactory.AES.keyFromPassword(password.toCharArray());
Encryptor encryptor = new Encryptor(key, "AES/CBC/PKCS7Padding", 16);

Możesz teraz użyć szyfratora do zaszyfrowania wiadomości. Możesz również wykonać szyfrowanie strumieniowe, jeśli chcesz. Dla Twojej wygody automatycznie generuje i przygotowuje bezpieczny IV.

Jeśli jest to plik, który chcesz skompresować, zapoznaj się z tą odpowiedzią Szyfrowanie dużego pliku za pomocą AES za pomocą JAVA dla jeszcze prostszego podejścia.


2
Cześć Martin, zawsze powinieneś wskazać, że jesteś pisarzem biblioteki, jeśli chcesz to podkreślić. Istnieje mnóstwo owijaczy kryptograficznych, które starają się ułatwić. Czy ten ma papier bezpieczeństwa, czy też otrzymał jakieś recenzje, które warte są naszej uwagi?
Maarten Bodewes

-1

Użyj tej klasy do szyfrowania. To działa.

public class ObjectCrypter {


    public static byte[] encrypt(byte[] ivBytes, byte[] keyBytes, byte[] mes) 
            throws NoSuchAlgorithmException,
            NoSuchPaddingException,
            InvalidKeyException,
            InvalidAlgorithmParameterException,
            IllegalBlockSizeException,
            BadPaddingException, IOException {

        AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES");
        Cipher cipher = null;
        cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec);
        return  cipher.doFinal(mes);

    }

    public static byte[] decrypt(byte[] ivBytes, byte[] keyBytes, byte[] bytes) 
            throws NoSuchAlgorithmException,
            NoSuchPaddingException,
            InvalidKeyException,
            InvalidAlgorithmParameterException,
            IllegalBlockSizeException,
            BadPaddingException, IOException, ClassNotFoundException {

        AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        SecretKeySpec newKey = new SecretKeySpec(keyBytes, "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec);
        return  cipher.doFinal(bytes);

    }
}

A to są ivBytes i losowy klucz;

String key = "e8ffc7e56311679f12b6fc91aa77a5eb";

byte[] ivBytes = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
keyBytes = key.getBytes("UTF-8");

10
„działa”… tak, ale nie spełnia wymagań dla stworzenia rozwiązania kryptograficznie bezpiecznego (moim zdaniem nie spełnia też standardów kodowania Java w odniesieniu do obsługi wyjątków).
Maarten Bodewes

2
IV jest inicjowany do zera. Wyszukaj ataki BEAST i ACPA.
Michele Giuseppe Fadda

Wyjątki stanowią wazoo, metoda generowania „losowego” klucza i zero IV jest problemem w tej implementacji, ale problemy te są łatwe do rozwiązania. +1.
Phil
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.