Cifrado Java AES de 256 bits basado en contraseña


390

Necesito implementar el cifrado AES de 256 bits, pero todos los ejemplos que he encontrado en línea usan un "KeyGenerator" para generar una clave de 256 bits, pero me gustaría usar mi propia contraseña. ¿Cómo puedo crear mi propia clave? He intentado rellenarlo a 256 bits, pero luego aparece un error que dice que la clave es demasiado larga. Tengo instalado el parche de jurisdicción ilimitada, así que ese no es el problema :)

Es decir. El KeyGenerator se ve así ...

// 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();

Código tomado de aquí

EDITAR

En realidad, estaba rellenando la contraseña a 256 bytes, no bits, que es demasiado largo. El siguiente es un código que estoy usando ahora que tengo más experiencia con esto.

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)

Los bits "TODO" que debes hacer tú mismo :-)


¿Podría aclarar: llamar a kgen.init (256) funciona?
Mitch Wheat

2
Sí, pero esto genera automáticamente una clave ... pero dado que quiero cifrar datos entre dos lugares, necesito conocer la clave de antemano, por lo que necesito especificar una en lugar de "generar" una. Puedo especificar uno de 16 bits que funciona para el cifrado de 128 bits que funciona. He probado uno de 32 bits para cifrado de 256 bits, pero no funcionó como se esperaba.
Nippysaurus

44
Si entiendo correctamente, está tratando de usar una clave preestablecida de 256 bits, especificada, por ejemplo, como una matriz de bytes. Si es así, el enfoque de DarkSquid usando SecretKeySpec debería funcionar. También es posible derivar una clave AES de una contraseña; si eso es lo que busca, hágamelo saber y le mostraré la forma correcta de hacerlo; simplemente cambiar una contraseña no es la mejor práctica.
erickson

Tenga cuidado al rellenar un número, puede hacer que su AES sea menos seguro.
Joshua

1
@erickson: eso es exactamente lo que necesito hacer (derivar una clave AES de una contraseña).
Nippysaurus

Respuestas:


476

Comparta el password(a char[]) y salt(un byte[]—8 bytes seleccionados por un SecureRandomhace una buena sal — que no necesita mantenerse en secreto) con el receptor fuera de banda. Luego, para obtener una buena clave de esta información:

/* 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");

Los números mágicos (que podrían definirse como constantes en alguna parte) 65536 y 256 son el recuento de iteración de derivación de clave y el tamaño de clave, respectivamente.

La función de derivación de clave se repite para requerir un esfuerzo computacional significativo, y eso evita que los atacantes prueben rápidamente muchas contraseñas diferentes. El recuento de iteraciones se puede cambiar según los recursos informáticos disponibles.

El tamaño de la clave se puede reducir a 128 bits, lo que todavía se considera cifrado "fuerte", pero no da mucho margen de seguridad si se descubren ataques que debilitan AES.

Utilizado con un modo de encadenamiento de bloques adecuado, la misma clave derivada se puede utilizar para cifrar muchos mensajes. En Cipher Block Chaining (CBC) , se genera un vector de inicialización aleatorio (IV) para cada mensaje, produciendo un texto de cifrado diferente incluso si el texto plano es idéntico. CBC puede no ser el modo más seguro disponible para usted (vea AEAD a continuación); Hay muchos otros modos con diferentes propiedades de seguridad, pero todos usan una entrada aleatoria similar. En cualquier caso, las salidas de cada operación de cifrado son el texto cifrado y el vector de inicialización:

/* 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"));

Almacene el ciphertexty el iv. En el descifrado, SecretKeyse regenera exactamente de la misma manera, utilizando la contraseña con los mismos parámetros de sal e iteración. Inicialice el cifrado con esta clave y el vector de inicialización almacenado con el mensaje:

/* 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 incluyó soporte API para modos de cifrado AEAD , y el proveedor "SunJCE" incluido con las distribuciones OpenJDK y Oracle implementa estos a partir de Java 8. Se recomienda uno de estos modos en lugar de CBC; Protegerá la integridad de los datos y su privacidad.


A java.security.InvalidKeyExceptioncon el mensaje "Tamaño de clave ilegal o parámetros predeterminados" significa que la intensidad de la criptografía es limitada; los archivos de política de jurisdicción de fuerza ilimitada no están en la ubicación correcta. En un JDK, deben colocarse debajo${jdk}/jre/lib/security

Según la descripción del problema, parece que los archivos de políticas no están instalados correctamente. Los sistemas pueden tener fácilmente múltiples tiempos de ejecución de Java; vuelva a verificar para asegurarse de que se esté utilizando la ubicación correcta.


29
@ Nick: lea PKCS # 5. Las sales son necesarias para PBKDF2, por lo que la API para el cifrado basado en contraseña las requiere como entrada para la derivación de claves. Sin sales, se podría usar un ataque de diccionario, lo que permite una lista precalculada de las claves de cifrado simétricas más probables. Cipher IVs y sales de derivación clave tienen diferentes propósitos. Los IV permiten reutilizar la misma clave para múltiples mensajes. Las sales evitan ataques de diccionario en la clave.
erickson

2
Primero, eso sería cifrado DES, no AES. La mayoría de los proveedores no tienen un buen soporte para los PBEwith<prf>and<encryption>algoritmos; por ejemplo, SunJCE no proporciona y PBE para AES. En segundo lugar, habilitar jasypt no es un objetivo. Un paquete que pretende ofrecer seguridad sin requerir una comprensión de los principios subyacentes parece peligroso a primera vista.
erickson

66
Implementé la respuesta de @ erickson como una clase: github.com/mrclay/jSecureEdit/tree/master/src/org/mrclay/crypto (PBE hace el trabajo, PBEStorage es un objeto de valor para almacenar el texto IV / cifrado juntos).
Steve Clay

3
@AndyNuss Este ejemplo es para el cifrado reversible, que generalmente no debe usarse para contraseñas. Usted puede utilizar la derivación de claves PBKDF2 a las contraseñas de "troceo" de forma segura. Eso significa que en el ejemplo anterior, almacenarías el resultado tmp.getEncoded()como el hash. También debe almacenar las saltiteraciones y (65536 en este ejemplo) para que pueda volver a calcular el hash cuando alguien intenta autenticarse. En este caso, genere la sal con un generador de números aleatorios criptográficos cada vez que se cambie la contraseña.
erickson

66
Para ejecutar este código, asegúrese de tener los archivos de política de jurisdicción de fuerza ilimitada correctos en su JRE como se indica en ngs.ac.uk/tools/jcepolicyfiles
Amir Moghimi

75

Considere usar el Módulo criptográfico Spring Security

El módulo Spring Security Crypto brinda soporte para el cifrado simétrico, la generación de claves y la codificación de contraseñas. El código se distribuye como parte del módulo principal pero no depende de ningún otro código Spring Security (o Spring).

Proporciona una abstracción simple para el cifrado y parece coincidir con lo que se requiere aquí,

El método de cifrado "estándar" es AES de 256 bits que utiliza PBKDF2 de PKCS # 5 (Función de derivación de clave basada en contraseña # 2). Este método requiere Java 6. La contraseña utilizada para generar la SecretKey debe mantenerse en un lugar seguro y no compartirse. La sal se usa para evitar ataques de diccionario contra la clave en caso de que sus datos cifrados se vean comprometidos. También se aplica un vector de inicialización aleatoria de 16 bytes para que cada mensaje cifrado sea único.

Una mirada a las partes internas revela una estructura similar a la respuesta de Erickson .

Como se señaló en la pregunta, esto también requiere la Política de jurisdicción de fuerza ilimitada de Java Cryptography Extension (JCE) (de lo contrario, se encontrará InvalidKeyException: Illegal Key Size). Es descargable para Java 6 , Java 7 y Java 8 .

Ejemplo de uso

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");
        }       
    }
}

Y salida de muestra,

Sal: "feacbc02a3a697b0"
Texto original: "* secretos reales *"
Texto cifrado: "7c73c5a83fa580b5d6f8208768adc931ef3123291ac8bc335a1277a39d256d9a" 
Texto descifrado: "* secretos reales *"
Éxito: coincidencias de texto descifrado

¿Puedes usar ese módulo sin cargar todo Spring? No parecen haber puesto archivos jar disponibles para descargar.
theglauber

55
@theglauber Sí, puede usar el módulo sin Spring Security o Spring framework. Al mirar el pom , la única dependencia de tiempo de ejecución es apache commons-logging 1.1.1 . Puede extraer el frasco con maven o descargarlo directamente desde el repositorio binario oficial (consulte la descarga de binarios de Spring 4 para obtener más información sobre los binarios de Spring).
John McCarthy

1
¿Es posible establecer la longitud de la clave en 128 bits? Modificar la carpeta de seguridad en cada PC no es una opción para mí.
IvanRF

1
@IvanRF lo siento, no lo parece. 256 está codificado en la fuente
John McCarthy

2
El NULL_IV_GENERATORutilizado por la utilidad Spring no es seguro. Si la aplicación no proporciona un IV, deje que el proveedor lo elija y consulte después de la inicialización.
erickson

32

Después de leer las sugerencias de Erickson y obtener lo que pude de otras publicaciones y este ejemplo aquí , intenté actualizar el código de Doug con los cambios recomendados. Siéntase libre de editar para mejorarlo.

  • El vector de inicialización ya no es fijo
  • la clave de cifrado se deriva usando el código de erickson
  • La sal de 8 bytes se genera en setupEncrypt () usando SecureRandom ()
  • la clave de descifrado se genera a partir de la sal de cifrado y la contraseña
  • el cifrado de descifrado se genera a partir de la clave de descifrado y el vector de inicialización
  • se eliminó el giro hexadecimal en lugar de org.apache.commons codec Hex rutinas

Algunas notas: esto utiliza una clave de cifrado de 128 bits; Java aparentemente no hará un cifrado de 256 bits listo para usar. La implementación de 256 requiere la instalación de algunos archivos adicionales en el directorio de instalación de Java.

Además, no soy una persona criptográfica. Presta atención.

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
Esta es básicamente la misma respuesta que la de Erickson, rodeada por un contenedor no tan bien programado en mi opinión. printStackTrace()
Maarten Bodewes

2
@owlstead - Esta es una gran respuesta. Muestra cómo cifrar una secuencia cifrando el búfer de bytes, en lugar de tener todo en la memoria. La respuesta de Erickson no funcionará para archivos grandes, que no caben en la memoria. Entonces +1 a wufoo. :)
dynamokaj

2
@dynamokaj El uso de CipherInputStreamy CipherOutputStreamno es un gran problema. Mezclar todas las excepciones debajo de la tabla es un problema. El hecho de que la sal de repente se haya convertido en un campo y que se requiera la IV es un problema. El hecho de que no siga las convenciones de codificación de Java es un problema. Y el hecho de que esto solo funcione en archivos mientras no se solicitó es un problema. Y que el resto del código es básicamente una copia tampoco ayuda. Pero tal vez lo modifique para mejorarlo, como se sugiere ...
Maarten Bodewes

@owlstead Estoy de acuerdo en que la codificación podría haberse visto mejor. Lo he reducido a 1/4 o algo así, pero me gusta que me haya presentado CipherInputStream y CipherOutputStream, ¡ya que eso era justo lo que necesitaba ayer! ;)
dynamokaj

¿Por qué dos veces? fout.close (); fout.close ();
Marian Paździoch

7

Generar su propia clave a partir de una matriz de bytes es fácil:

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

Pero crear una clave de 256 bits no es suficiente. Si el generador de claves no puede generar claves de 256 bits para usted, entonces la Cipherclase probablemente tampoco sea compatible con AES de 256 bits. Dice que tiene instalado el parche de jurisdicción ilimitada, por lo que el cifrado AES-256 debe ser compatible (pero también las claves de 256 bits también, así que esto podría ser un problema de configuración).

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

Una solución alternativa para la falta de compatibilidad con AES-256 es tomar alguna implementación de AES-256 disponible gratuitamente y usarla como un proveedor personalizado. Esto implica crear su propia Providersubclase y usarla con Cipher.getInstance(String, Provider). Pero esto puede ser un proceso complicado.


55
Siempre debe indicar el modo y el algoritmo de relleno. Java usa el modo ECB inseguro por defecto.
Maarten Bodewes

No puede crear su propio proveedor, los proveedores deben estar firmados (no puedo creer que haya leído este error inicialmente). Incluso si pudiera, la restricción del tamaño de la clave está en la implementación de Cipher, no en el propio proveedor. Puede usar AES-256 en Java 8 y versiones anteriores, pero debe usar una API patentada. O un tiempo de ejecución que no plantea restricciones en el tamaño de la clave, por supuesto.
Maarten Bodewes

Las versiones recientes de OpenJDK (y Android) no tienen restricciones para agregar su propio proveedor de seguridad / criptografía. Pero lo hace bajo su propio riesgo, por supuesto. Si olvida mantener sus bibliotecas actualizadas, puede exponerse a riesgos de seguridad.
Maarten Bodewes

1
@ MaartenBodewes + OpenJDK nunca tuvo el problema de 'política de cifrado limitada' en primer lugar, y Oracle JDK lo eliminó hace más de un año por 8u161 y 9 en adelante (y tal vez algunas versiones más bajas de pago ahora, pero no las he comprobado)
dave_thompson_085

6

Lo que he hecho en el pasado es hash la clave a través de algo como SHA256, luego extraiga los bytes del hash en el byte de clave [].

Después de tener su byte [] simplemente puede hacer:

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

12
Para otros: este no es un método muy seguro. Debe usar PBKDF 2 especificado en PKCS # 5. Erickson dijo cómo hacer esto arriba. El método de DarkSquid es vulnerable a ataques de contraseña y tampoco funciona a menos que el tamaño de su texto plano sea un múltiplo del tamaño de bloque de AES (128 bits) porque omitió el relleno. Además, no especifica el modo; lea los modos de operación de cifrado de bloque de Wikipedia para conocer las preocupaciones.
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)); También estoy haciendo lo mismo que lo sugerido en su respuesta, pero todavía termino con este java.security.InvalidKeyException: tamaño de clave ilegal ¿Es obligatorio descargar el archivo de política JCE?
Niranjan Subramanian

2
NO USE este método en ningún tipo de entorno de producción. Al comenzar con el cifrado basado en contraseña, muchos usuarios se sienten abrumados por los muros de código y no entienden cómo funcionan los ataques de diccionario y otros hacks simples. Si bien puede ser frustrante aprender, es una inversión que vale la pena investigar esto. Aquí hay un buen artículo para principiantes: adambard.com/blog/3-wrong-ways-to-store-a-password
IcedDante

1

Además de las ediciones de @ Wufoo, la siguiente versión usa InputStreams en lugar de archivos para facilitar el trabajo con una variedad de archivos. También almacena el IV y Salt al comienzo del archivo, por lo que solo es necesario rastrear la contraseña. Dado que el IV y la sal no necesitan ser secretos, esto hace la vida un poco más fácil.

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
Esta solución parece utilizar un manejo de búfer incómodo y un manejo de excepciones absolutamente inferior a la media, básicamente registrándolos y luego olvidándolos. Tenga en cuenta que el uso de CBC está bien para los archivos, pero no para la seguridad del transporte. El uso de PBKDF2 y AES, por supuesto, puede defenderse, en ese sentido puede ser una buena base para una solución.
Maarten Bodewes

1

(Tal vez útil para otros con un requisito similar)

Tenía un requisito similar para usar AES-256-CBCcifrar y descifrar en Java.

Para lograr (o especificar) el cifrado / descifrado de 256 bytes, la Java Cryptography Extension (JCE)política debe establecerse en"Unlimited"

Se puede configurar en el java.securityarchivo bajo $JAVA_HOME/jre/lib/security(para JDK) o $JAVA_HOME/lib/security(para JRE)

crypto.policy=unlimited

O en el código como

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

Java 9 y versiones posteriores tienen esto habilitado por defecto.


0

Considere usar Encryptor4j del cual soy el autor.

Primero asegúrese de tener instalados los archivos de la Política de jurisdicción de fuerza ilimitada antes de proceder para que pueda usar las claves AES de 256 bits.

Luego haz lo siguiente:

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

Ahora puede usar el encriptador para encriptar su mensaje. También puede realizar el cifrado de transmisión si lo desea. Genera y antepone automáticamente un IV seguro para su conveniencia.

Si es un archivo que desea comprimir, eche un vistazo a esta respuesta Cifrar un archivo grande con AES usando JAVA para un enfoque aún más simple.


2
Hola Martin, siempre debes indicar que eres el escritor de la biblioteca si quieres señalarlo. Hay montones de envoltorios de cifrado que intentan facilitar las cosas. ¿Este tiene un documento de seguridad o ha recibido alguna crítica para que valga la pena?
Maarten Bodewes

-1

Use esta clase para el cifrado. Funciona.

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);

    }
}

Y estos son ivBytes y una clave aleatoria;

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
"funciona" ... sí, pero no cumple con los requisitos para crear una solución criptográficamente segura (ni cumple con los estándares de codificación Java con respecto al manejo de excepciones, en mi opinión).
Maarten Bodewes

2
IV se inicializa a cero. Busca los ataques de BESTIA y ACPA.
Michele Giuseppe Fadda

Excepciones al wazoo, el método de generar la clave "aleatoria", y un IV cero es un problema con esta implementación, pero esos problemas son triviales de solucionar. +1.
Phil
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.