Criptografar e descriptografar texto [Resolvido]

Amigos,
Como eu faço para criptografar um arquivo (.txt, por exemplo) utilizando uma senha informada pelo usuário.
Vejam bem, eu não quero criptografar uma senha, quero que o usuário informe uma senha para o documento, esse seja criptografado e somente seja descriptografado se a mesma senha for informada para isso.
Seria algo parecido com os documentos do MS Word onde é possível informar uma senha para o documento e este só abre com a mesma senha informada.
Ou seja, toda a String passa pelo algorítimo usando uma chave informada pelo usuário. Uma criptografia simétrica: pensei no AES, é possível?

Como não sei ainda por onde começar minha classe só tem isso:

import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;

/**
 *
 * @author Paulo Lopes
 */
public class Cifrador
{
    public Cifrador()
    {
        
    }
    
    public void criptografa(String texto)
    {
        String textoCifrado = texto;
        //Aqui seria a parte de cifrar o texto, algo como cifraTexto(textoCifrado);
        JFileChooser arquivo = new JFileChooser();
        int retorno = arquivo.showSaveDialog(null);  
        if(retorno == JFileChooser.APPROVE_OPTION)  
        {
            String caminho = arquivo.getSelectedFile().getAbsolutePath();  
            String extensao = caminho + ".criptografado";
            try  
            {
                try (FileWriter fw = new FileWriter(extensao))
                {
                    fw.write(textoCifrado);
                    System.out.println(textoCifrado);
                }
            }  
            catch(FileNotFoundException e)  
            {
                JOptionPane.showMessageDialog(null, "Erro ao salvar arquivo!\n" + e, "Erro!", 0);    
            }  
            catch(IOException e)  
            {
                JOptionPane.showMessageDialog(null, "Erro ao salvar arquivo\n" + e, "Erro", 0);    
            }
        }
    }
    
    public String decriptografa(String texto)
    {
        //Vou implementar depois.
        texto = null;
        return texto;
    }
}

Grato.

Galera fiz um pouco mais buscando em sítios afora.
Fiz minha classe dessa forma aqui, mas quando eu informo a senha tenho um erro.
A nova classe:

package editor;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 *
 * @author Paulo Lopes
 */
public class Cifrador
{
    private String textoNormal; //Texto a ser criptografado.
    private String textoCifrado;
    private final String ALGORITIMO = "AES/CTR/NoPadding";
    private static Key chave; //A chave é gerada através da senha.
    private final IvParameterSpec ivps ;
    private static final String IV = "1234567890123456";
    
    public Cifrador(String senha)
    {
        byte[] ivpsArray = Conversor.ASCIIToByteArray(IV, false);
        ivps = new IvParameterSpec(ivpsArray);
        //Gerando a chave usando a senha informada pelo usuário.
        byte[] chaveArray = senha.getBytes();
        chave = new SecretKeySpec(chaveArray, "AES"); 
    }
    
    public void criptografa(String texto) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
    {
        this.textoNormal = texto;
        Cipher cipher = Cipher.getInstance(ALGORITIMO);
        cipher.init(Cipher.ENCRYPT_MODE, chave, ivps);
        byte[] textoArray = Conversor.HexStringToByteArray(texto);
        byte[] cifrar = cipher.doFinal(textoArray);
        this.textoCifrado = Conversor.ByteArrayToHexString(cifrar);
    }
    
    public String getTextoCifrado()
    {
        return textoCifrado;
    }
    
    public void decriptografa(String texto) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException
    {
        this.textoCifrado = texto;
        Cipher cipher = Cipher.getInstance(ALGORITIMO);
        cipher.init(Cipher.DECRYPT_MODE, chave, ivps);
        byte[] textoArray = Conversor.HexStringToByteArray(textoCifrado);
        byte[] mensagem = cipher.doFinal(textoArray);
        this.textoNormal = Conversor.ByteArrayToHexString(mensagem);
    }
    
    public String getTextoDecifrado()
    {
        return textoNormal;
    }
}

O erro que me retorna é:

java.security.InvalidKeyException: Invalid AES key length: 4 bytes

Como eu resolvo para que minha senha, após o usuário informar, eu deixe ela de acordo com o necessário para o AES?
Achei algo como um tal de “Salt”, mas não entendi.
Valeu…

O tamanho da chave no AES é fixo. Você é obrigado a usar chaves com 128, 192 ou 256 bits. Cada caractere ocupa 8 bits, portanto, as senhas são limitadas a 16, 32 ou 64 caracteres.

Uma estratégia comum é usar um algoritmo de Hash, como o DES ou SHA-1, na senha digitada pelo usuário para gerar a seqüência que será usada como entrada para o AES.

No caso do SALT. Existe um ataque chamado “Rainbow Tables” que permite reverter um texto “hasheado” rapidamente. Ele consiste na elaboração de um cache. Por exemplo, você poderia criar um mapa com palavras comuns e seus hashes correspondentes e procurar por esses hashes num BD (o ataque é um pouco mais complexo que isso, mas a idéia básica é por aí).

A técnica de SALT consiste simplesmente em usar uma informação conhecida (como o id do usuário, ou um valor aleatório que será guardado junto da senha) junto ao Hash para evitar o esse ataque. Assim, por exemplo, se os usuários Vinicius, de id=1 e Paulo, de id=50 tiverem a senha “senhaguj”, você processaria o hash de “senha1” para o Vinicius e “senha50” para o Paulo, gerando hashes diferentes.

O fato dessa informação extra estar concatenada forçaria o atacante a ter uma tabela de hash extremamente gigante, o que torna o ataque inviável.

PS: Minhas explicações são em linhas gerais e didáticas. É lógico que há recomendações mais específicas sobre o assunto para gerar SALTs e Hashes mais efetivos.

Boas referências sobre esse assunto:
https://crackstation.net/hashing-security.htm
https://www.owasp.org/index.php/Hashing_Java

Recomendo que as leia com calma, pois são bem interessantes. A primeira referência está um pouco mais atualizada que a segunda, mas a segunda fornece exemplos em Java.

[quote=ViniGodoy]O tamanho da chave no AES é fixo. Você é obrigado a usar chaves com 128, 192 ou 256 bits. Cada caractere ocupa 8 bits, portanto, as senhas são limitadas a 16, 32 ou 64 caracteres.

Uma estratégia comum é usar um algoritmo de Hash, como o DES ou SHA-1, na senha digitada pelo usuário para gerar a seqüência que será usada como entrada para o AES.

No caso do SALT. Existe um ataque chamado “Rainbow Tables” que permite reverter um texto “hasheado” rapidamente. Ele consiste na elaboração de um cache. Por exemplo, você poderia criar um mapa com palavras comuns e seus hashes correspondentes e procurar por esses hashes num BD (o ataque é um pouco mais complexo que isso, mas a idéia básica é por aí).

A técnica de SALT consiste simplesmente em usar uma informação conhecida (como o id do usuário, ou um valor aleatório que será guardado junto da senha) junto ao Hash para evitar o esse ataque. Assim, por exemplo, se os usuários Vinicius, de id=1 e Paulo, de id=50 tiverem a senha “senhaguj”, você processaria o hash de “senha1” para o Vinicius e “senha50” para o Paulo, gerando hashes diferentes.

O fato dessa informação extra estar concatenada forçaria o atacante a ter uma tabela de hash extremamente gigante, o que torna o ataque inviável.[/quote]

Valeu Vini, eu li os dois sites que me recomendou e eu entendi bastante. Salt parece ser mais usado para banco de dados, onde se pode ter todos os hashes de senhas através de um ataque ao BD.
Como eu não terei nenhum armazenamento de senhas não precisarei do Salt.
Então resolvi usar um algorítimo de hash para minha senha e escolhi o SHA-256. O quê me dá 32 bits.
Entretanto ainda continuo com erro. Veja:
A classe criptográfica.

package editor;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 *
 * @author Paulo Lopes
 */
public class Cifrador
{
    private String textoNormal; //Texto a ser criptografado.
    private String textoCifrado;
    private final String ALGORITIMO = "AES";
    private Hasher hash;
    
    public void criptografa(String senha, String texto) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, EncryptorException, UnsupportedEncodingException
    {
        hash = new Hasher();
        byte[] chave = hash.getHash(senha);
        try
        {
            SecretKeySpec skeySpec = new SecretKeySpec(chave, ALGORITIMO);
            Cipher cipher = Cipher.getInstance(ALGORITIMO);
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
            byte[] encrypted = cipher.doFinal(texto.getBytes());
            this.textoCifrado = new BASE64Encoder().encode(encrypted);
        }
        catch(NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e)
        {
            throw new EncryptorException("Erro ao criptografar informações.\n" + e.getMessage());
        }
    }
    
    public String getTextoCifrado()
    {
        return textoCifrado;
    }
    
    public void decriptografa(String senha, String texto) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException, EncryptorException
    {
        byte[] textoDecifrado = null;
        hash = new Hasher();
        try
        {
            byte[] chave = hash.getHash(senha);
            SecretKeySpec skeySpec = new SecretKeySpec(chave, ALGORITIMO);
            byte[] decoded = new BASE64Decoder().decodeBuffer(texto);
            Cipher cipher = Cipher.getInstance(ALGORITIMO);
            cipher.init(Cipher.DECRYPT_MODE, skeySpec);
            textoDecifrado = cipher.doFinal(decoded);
        }
        catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e)
        {
            throw new EncryptorException("Erro ao descriptografar informações.\n" + e.getMessage());
        }
        this.textoNormal = new String(textoDecifrado);
    }
    
    public String getTextoDecifrado()
    {
        return textoNormal;
    }
}

A classe de Hash.

/*
 * This class is responsible for provide a hash of password.
 * This will return a hash of 256 bits necessary to AES key.
 * Evertime that user going to cipher or decipher this class is called.
 */

package editor;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 *
 * @author Paulo Lopes
 */
public class Hasher
{
    private final String ALGORITIMO = "SHA-256";
 
    public byte[] getHash(String senha) throws NoSuchAlgorithmException, UnsupportedEncodingException
    {
        MessageDigest messageDigest = MessageDigest.getInstance(ALGORITIMO);
        messageDigest.update(senha.getBytes());
        byte[] senhaByte = messageDigest.digest();
        return senhaByte;
    }
}

E recebo o erro:

editor.EncryptorException: Erro ao criptografar informações. Illegal key size or default parameters

Fazendo um chave.length() simples, eu obtenho 32 bits normalmente da senha.
Onde pode estar ocorrendo o erro eu não tenho ideia.
Obrigado novamente!
Abraços.

Veja a resposta desse tópico te ajuda, eu já usei essa receita de bolo aqui (a da resposta, não a da pergunta):

[quote=ViniGodoy]Veja a resposta desse tópico te ajuda, eu já usei essa receita de bolo aqui (a da resposta, não a da pergunta):

Tive o mesmo erro, Vini!

editor.EncryptorException: Erro ao criptografar informações. Illegal key size or default parameters

Eu já tinha visto esse site e fiz exatamente do mesmo jeito e o erro é o mesmo.
Claro, copiando a resposta do “erickson”.
Abraços.

Bem. Não consegui resolver da forma inicial.
Mas usando o MD5 eu consigo facilmente.
Então vou ficar com ele mesmo. Apesar dele já ter colisões.
Como a senha não é armazenada em nenhum lugar o risco é menor.
Valeu galera.