Java 256位AES基于密码的加密

420

我需要实现256位AES加密,但我在网上找到的所有示例都使用“KeyGenerator”生成256位密钥,但我想使用自己的密码。如何创建我的自己的密钥?我尝试将其填充到256位,但然后出现错误,提示密钥太长。我已经安装了无限制的权限补丁,所以那不是问题 :)

即,KeyGenerator看起来像这样...

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

这里获取代码

编辑

实际上我是将密码填充到256字节而不是256位,这太长了。以下是我现在使用的一些代码,因为我对此有了更多经验。

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)

你需要自己完成的“TODO”部分 :-)


请问一下:调用kgen.init(256)是否有效? - Mitch Wheat
2
是的,但这会自动生成一个密钥...但由于我想要在两个地方之间加密数据,所以我需要事先知道密钥,因此我需要指定一个而不是“生成”一个。我可以指定一个16位的密钥,适用于128位加密。我尝试过32位的密钥进行256位加密,但结果并不如预期。 - Nippysaurus
4
如果我理解正确,您正试图使用预先安排的、256位的密钥,例如指定为字节数组。如果是这样,DarkSquid使用SecretKeySpec的方法应该可以工作。还有一种从密码中派生AES密钥的方法;如果您想要这样做,请告诉我,我会向您展示正确的方法;仅仅对一个密码进行哈希处理并不是最佳实践。 - erickson
小心填充数字,否则可能会使您的AES less安全。 - Joshua
2
@erickson: 这正是我需要做的事情(从密码派生出一个AES密钥)。 - Nippysaurus
@erickson:不对。我曾尝试在.NET和Java中编译256位的一些内容,但结果却不同。也许我并不需要继续使用Java端了。最后我们可能会使用bouncycastle。 - Nippysaurus
9个回答

513

分享密码(一个char[])和盐(一个byte[] - 由SecureRandom选择的8个字节的盐是一个好的选择,不需要保密)给接收方。然后从这些信息中派生出一个好的密钥:

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

这些神奇数字(可以在某个地方定义为常量)65536和256是密钥派生迭代次数和密钥大小。
密钥派生函数被迭代以需要大量的计算工作,这可以防止攻击者快速尝试许多不同的密码。迭代次数可以根据可用的计算资源进行更改。
密钥大小可以减小到128位,这仍然被认为是“强大”的加密,但如果发现削弱AES的攻击,则不会给出太多安全余地。
在适当的块链接模式下使用,相同的派生密钥可以用于加密许多消息。在Cipher Block Chaining(CBC)中,为每个消息生成一个随机初始化向量(IV),即使明文相同,也会产生不同的密文。CBC可能不是您可用的最安全模式(请参见下面的AEAD);有许多其他具有不同安全属性的模式,但它们都使用类似的随机输入。无论如何,每个加密操作的输出都是密文初始化向量:
/* 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(StandardCharsets.UTF_8));

存储ciphertextiv。在解密时,使用相同的盐和迭代参数使用密码以完全相同的方式重新生成SecretKey。使用此密钥与消息一起存储的初始化向量初始化密码器:

/* 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), StandardCharsets.UTF_8);
System.out.println(plaintext);

Java 7包含了支持AEAD密码模式的API,而随OpenJDK和Oracle发行版一起提供的"SunJCE"提供程序从Java 8开始实现了这些功能。其中一个这些模式强烈建议用来替代CBC;它将保护数据的完整性以及隐私。
一个带有消息“非法密钥大小或默认参数”的java.security.InvalidKeyException表示密码学强度受到限制;无限制的强度管辖策略文件没有放置在正确的位置。在JDK中,它们应该放置在${jdk}/jre/lib/security下。
根据问题描述,似乎策略文件没有正确安装。系统可以轻松拥有多个Java运行时环境;请仔细检查以确保使用了正确的位置。

33
@Nick: 请阅读PKCS #5。盐是PBKDF2所必需的,这就是为什么基于密码的加密API需要它们作为密钥派生的输入。没有盐,可能会使用字典攻击,从而启用预先计算出最可能对称加密密钥的列表。密码IV和密钥派生盐具有不同的目的。IV允许重复使用同一密钥进行多个消息的加密。盐可以防止针对密钥的字典攻击。 - erickson
6
我已将@erickson的答案实现为一个类:https://github.com/mrclay/jSecureEdit/tree/master/src/org/mrclay/crypto(PBE完成了工作,PBEStorage是用于存储IV/密文的值对象。) - Steve Clay
3
这个例子是用于可逆加密,通常不应该用于密码。您可以使用PBKDF2密钥派生来安全地“散列”密码。这意味着在上面的示例中,您将存储tmp.getEncoded()的结果作为哈希值。您还应该存储salt和迭代(在本例中为65536),以便在有人尝试进行身份验证时可以重新计算哈希值。在这种情况下,每次更改密码时都要使用密码学随机数生成器生成salt - erickson
6
运行此代码前,请确保你的JRE中拥有正确的无限制强度权限策略文件,如http://www.ngs.ac.uk/tools/jcepolicyfiles中所述。 - Amir Moghimi
2
如果你按照答案所示发送加密数据到网络上,需要确保消息的完整性。换句话说,你需要生成另一个用于MAC的密钥,或者使用GCM等认证模式的加密方式。 - Maarten Bodewes
显示剩余46条评论

83
请考虑使用Spring Security Crypto Module Spring Security Crypto模块提供对对称加密、密钥生成和密码编码的支持。该代码作为核心模块的一部分分发,但不依赖于任何其他Spring Security(或Spring)代码。
它为加密提供了一个简单的抽象,并似乎符合此处所需的内容。
“标准”加密方法是使用PKCS#5的PBKDF2(基于密码的密钥派生函数)的256位AES。此方法需要Java 6。用于生成SecretKey的密码应保存在安全位置并且不共享。盐用于防止字典攻击,以防您的加密数据被攻破。还会应用一个16字节的随机初始化向量,因此每个加密消息都是唯一的。

查看internals,发现其结构类似于erickson的回答

正如问题中所指出的那样,这也需要Java加密扩展(JCE)无限制强度管辖策略(否则你会遇到InvalidKeyException: Illegal Key Size)。它可以在Java 6, Java 7Java 8上下载。

使用示例

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

以下是示例输出:

盐值: "feacbc02a3a697b0"
原始文本: "*royal secrets*"
加密后文本: "7c73c5a83fa580b5d6f8208768adc931ef3123291ac8bc335a1277a39d256d9a" 
解密后文本: "*royal secrets*"
成功:解密后的文本与原始文本匹配

1
你能在不加载Spring的全部内容的情况下使用该模块吗?他们似乎没有提供可供下载的jar文件。 - theglauber
5
是的,您可以在没有Spring Security或Spring框架的情况下使用该模块。从查看pom文件来看,唯一的运行时依赖项是apache commons-logging 1.1.1。您可以使用maven 拉取jar包,或者直接从官方二进制仓库下载jar包(有关Spring二进制文件的更多信息,请参见Spring 4二进制文件下载)。 - John McCarthy
1
是否可以将密钥长度设置为128位?修改每台计算机的安全文件夹对我来说不是一个选项。 - IvanRF
1
@IvanRF 抱歉,看起来不是这样。256在[source](http://grepcode.com/file/repo1.maven.org/maven2/org.springframework.security/spring-security-crypto/4.0.0.RELEASE/org/springframework/security/crypto/encrypt/AesBytesEncryptor.java#56)中是硬编码的。 - John McCarthy
4
Spring工具使用的NULL_IV_GENERATOR不安全。如果应用程序没有提供IV,则让提供者选择它,并在初始化后查询它。 - erickson
显示剩余5条评论

36

在阅读了Erickson的建议并从其他帖子和这个例子中获取了一些信息后,我尝试使用推荐的更改更新Doug的代码。随意编辑以使其更好。

  • 初始化向量不再固定
  • 加密密钥使用erickson的代码派生
  • 在setupEncrypt()中使用SecureRandom()生成8字节盐
  • 解密密钥从加密盐和密码生成
  • 解密密码从解密密钥和初始化向量生成
  • 删除十六进制调整,改用org.apache.commons codec Hex例程

一些注意事项:这使用128位加密密钥 - Java显然无法直接进行256位加密。实现256需要将一些额外文件安装到Java安装目录中。

此外,我不是加密专家。请谨慎使用。

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


}

14
这基本上是与 Erickson 的答案相同,被一个 - 我认为编程不是很好的 - 包装器包围着的 printStackTrace() - Maarten Bodewes
2
@owlstead - 这是一个很好的答案,它展示了如何通过加密字节缓冲区来加密流,而不是在内存中拥有所有内容。Erickson的答案对于大文件是行不通的,因为它们无法适应内存。所以给wufoo加一分。 :) - dynamokaj
2
@dynamokaj 使用CipherInputStreamCipherOutputStream并不是什么大问题。把所有的异常都隐藏起来才是问题。盐突然变成了一个字段,需要IV也是个问题。它不遵循Java编码规范也是个问题。而且它只能用于文件,这不是要求的。剩下的代码基本上是复制的,这也没有帮助。但也许我会像建议的那样对它进行调整以使其更好。 - Maarten Bodewes
我已经创建了一个带有数据完整性检查、密码快速失败和压缩的密码加密流:https://github.com/jjYBdx4IL/misc/tree/master/encryption-utils 欢迎添加更多内容。 - user1050755
@MarianPaździoch - 好发现。我发布这篇文章已经有一段时间了,但我认为这是一个打字错误。没有逻辑上的理由需要两个 fout() 调用。 - wufoo
显示剩余2条评论

11
从字节数组生成自己的密钥很容易:
byte[] raw = ...; // 32 bytes in size for a 256 bit key
Key skey = new javax.crypto.spec.SecretKeySpec(raw, "AES");

但仅仅创建一个256位的密钥是不够的。如果密钥生成器不能为您生成256位的密钥,那么Cipher类可能也不支持AES 256位。您说您已安装了无限制权限补丁,因此应该支持AES-256密码(但是256位密钥也应该被支持,因此这可能是配置问题)。

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

缺少AES-256支持的解决方法是使用一些自由可用的AES-256实现,并将其用作自定义提供程序。这涉及创建自己的Provider子类,并将其与Cipher.getInstance(String, Provider)一起使用。但这可能是一个复杂的过程。

5
你应该始终指明加密模式和填充算法。Java 默认使用不安全的 ECB 模式。 - Maarten Bodewes
你不能创建自己的提供程序,提供程序必须经过签名(我一开始居然忽视了这个错误)。即使你可以这样做,密钥大小的限制在 Cipher 的实现中,而不是提供程序本身。你可以在 Java 8 及以下版本中使用 AES-256,但需要使用专有 API 或者运行时不对密钥大小施加限制。 - Maarten Bodewes
最近版本的OpenJDK(和Android)没有限制添加自己的安全/加密提供程序。但是当然,您这样做是自担风险的。如果您忘记保持库的最新状态,可能会使自己面临安全风险。 - Maarten Bodewes
1
@MaartenBodewes+ OpenJDK从一开始就没有“有限加密策略”问题,而_Oracle_ JDK在8u161和9及以上版本中已经移除了它(也许还有一些较低的现在需要付费的版本,但我没有检查过)。 - dave_thompson_085

8
过去我所做的是使用类似于SHA256的哈希算法,对密钥进行哈希处理,然后从哈希中提取出字节并存储在密钥的byte[]数组中。
在获得byte[]数组之后,您可以简单地执行以下操作:
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] encryptedBytes = cipher.doFinal(clearText.getBytes());

12
对于其他人:这不是一种非常安全的方法。你应该使用 PKCS#5 规定的 PBKDF2。erickson 在上面解释了如何实现。DarkSquid 的方法容易受到密码攻击,并且除非明文大小是 AES 块大小(128位)的倍数,否则它不起作用,因为他省略了填充。此外,它也没有指定加密模式;请阅读维基百科关于分组密码操作模式的内容以了解相关问题。 - 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));我也按照你的建议做了同样的事情,但我仍然遇到了java.security.InvalidKeyException: Illegal key size的问题。下载JCE策略文件是强制性的吗? - Niranjan Subramanian
3
请勿在任何生产环境中使用此方法。开始使用基于密码的加密时,许多用户会被大量代码淹没,并不了解字典攻击和其他简单的黑客攻击如何运作。虽然学习可能很令人沮丧,但研究它是值得投资的。下面是一篇适合初学者的好文章:http://adambard.com/blog/3-wrong-ways-to-store-a-password/ - IcedDante

3

(对于其他有类似需求的人可能会有所帮助)

我有一个类似的需求,需要在Java中使用AES-256-CBC加密和解密。

要实现(或指定)256字节的加密/解密,Java加密扩展(JCE)策略应该设置为"无限制"

可以在$JAVA_HOME/jre/lib/security(适用于JDK)或$JAVA_HOME/lib/security(适用于JRE)下的java.security文件中进行设置。

crypto.policy=unlimited

或者在代码中写成


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

Java 9及更高版本默认启用此功能。


1
我尝试了Java 1.8.201版本,即使没有安全属性,它也可以正常工作。 - Adir D

2

除了@Wufoo的修改,以下版本使用InputStream而不是文件,使得处理各种文件更容易。它还将IV和Salt存储在文件开头,这样只需要跟踪密码即可。由于IV和Salt不需要保密,这使得生活变得更加轻松。

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
这个解决方案似乎使用了一些笨拙的缓冲处理和绝对低劣的异常处理,基本上只是记录它们然后忘记它们。请注意,使用CBC对于文件来说是可以的,但不适用于传输安全。当然,使用PBKDF2和AES可以被辩护,在这个意义上它可能是一个好的解决方案基础。 - Maarten Bodewes

1

考虑使用我所编写的Encryptor4j工具。

在开始之前,请确保您已经安装了无限强度权限策略文件,这样您就可以使用256位AES密钥。

然后按照以下步骤操作:

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

现在您可以使用加密器来加密您的消息。如果需要,您还可以执行流式加密。它会自动生成并预置一个安全的IV以方便您使用。

如果您想要压缩文件,请查看这个答案使用JAVA使用AES加密大文件,这是一种更简单的方法。


2
嗨,Martin,如果你想指出这个库是你写的,那么你应该始终标明。有很多加密包装器试图让事情变得更容易。这个库有安全论文或者已经接受了任何评论吗?这是否值得我们去关注? - Maarten Bodewes

-1
使用这个类进行加密。它有效。
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);

    }
}

这些是ivBytes和一个随机密钥;

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

11
“它有效”……但是,它不符合创建加密安全解决方案的要求(在我看来,也不符合Java编码标准关于异常处理的要求)。 - Maarten Bodewes
2
IV被初始化为零。搜索BEAST和ACPA攻击。 - Michele Giuseppe Fadda
异常处理、生成“随机”密钥的方法以及零IV是这个实现中的问题,但这些问题很容易解决。+1。 - Phil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接