Java AES/CBC解密后初始字节不正确

119

以下示例有何问题?

问题在于解密字符串的第一部分是无意义的。不过,其余部分没问题,我成功获取了...

Result: `£eB6O�geS��i are you? Have a nice day.
@Test
public void testEncrypt() {
  try {
    String s = "Hello there. How are you? Have a nice day.";

    // Generate key
    KeyGenerator kgen = KeyGenerator.getInstance("AES");
    kgen.init(128);
    SecretKey aesKey = kgen.generateKey();

    // Encrypt cipher
    Cipher encryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    encryptCipher.init(Cipher.ENCRYPT_MODE, aesKey);

    // Encrypt
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, encryptCipher);
    cipherOutputStream.write(s.getBytes());
    cipherOutputStream.flush();
    cipherOutputStream.close();
    byte[] encryptedBytes = outputStream.toByteArray();

    // Decrypt cipher
    Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
    IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
    decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

    // Decrypt
    outputStream = new ByteArrayOutputStream();
    ByteArrayInputStream inStream = new ByteArrayInputStream(encryptedBytes);
    CipherInputStream cipherInputStream = new CipherInputStream(inStream, decryptCipher);
    byte[] buf = new byte[1024];
    int bytesRead;
    while ((bytesRead = cipherInputStream.read(buf)) >= 0) {
        outputStream.write(buf, 0, bytesRead);
    }

    System.out.println("Result: " + new String(outputStream.toByteArray()));

  } 
  catch (Exception ex) {
    ex.printStackTrace();
  }
}

51
请勿在严肃的项目中使用此问题的任何答案!本问题提供的所有示例都容易受到填充预言攻击,而且整体上使用密码学非常糟糕。如果使用下面任何片段,将会在您的项目中引入严重的密码学漏洞。 - HoLyVieR
17
@HoLyVieR,关于以下引用:“您不应该开发自己的加密库”和“使用框架提供的高级API。”这里没有人在开发自己的加密库。我们只是使用Java框架已经存在的高级API。您先生的说法非常不准确。 - k170
10
@MaartenBodewes,你们两个达成一致并不意味着你们都是正确的。优秀的开发者知道如何区分高级API的封装和低级API的重写。好的读者会注意到OP要求一个“简单的java AES加密/解密示例”,这正是他得到的内容。我也不同意其他答案,这就是为什么我发布了自己的答案。也许你们应该尝试做同样的事情,并以你们的专业知识启迪我们所有人。 - k170
6
@HoLyVieR,这真的是我在SO上读过的最荒谬的事情!你有什么资格告诉别人他们能做什么不能做什么? - TedTrippin
16
我仍然看不到@HoLyVieR提供的任何示例,请提供一些示例或者库的指针。这样并没有起到建设性作用。请提供更具体的内容。 - danieljimenez
显示剩余5条评论
10个回答

229

很多人,包括我自己,在使用这个功能时会遇到许多问题,例如缺少一些信息(如忘记将其转换为Base64、初始化向量、字符集等)。因此,我想提供一个完全功能的代码。

希望这对大家有所帮助: 要编译,您需要额外的Apache Commons Codec jar文件,可以在此处找到: http://commons.apache.org/proper/commons-codec/download_codec.cgi

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());
            System.out.println("encrypted string: "
                    + Base64.encodeBase64String(encrypted));

            return Base64.encodeBase64String(encrypted);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.decodeBase64(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        String key = "Bar12345Bar12345"; // 128 bit key
        String initVector = "RandomInitVector"; // 16 bytes IV

        System.out.println(decrypt(key, initVector,
                encrypt(key, initVector, "Hello World")));
    }
}

47
如果您不想依赖第三方Apache Commons Codec库,可以使用JDK的 javax.xml.bind.DatatypeConverter 进行Base64编码/解码:System.out.println("加密字符串:" + DatatypeConverter.printBase64Binary(encrypted));byte[] original = cipher.doFinal(DatatypeConverter.parseBase64Binary(encrypted)); - curd0
13
你是否正在使用固定的初始化向量(IV)?! - vianna77
38
Java 8已经拥有Base64工具:java.util.Base64.getDecoder()和java.util.Base64.getEncoder()。 - Hristo Stoyanov
18
IV并不需要保密,但对于CBC模式来说必须是不可预测的(对于CTR模式来说必须是唯一的)。它可以与密文一起发送。常见的方法是将IV放在密文前面,并在解密之前将其切掉。应使用SecureRandom生成IV。 - Artjom B.
15
密码不是钥匙。IV 应该是随机的。 - Maarten Bodewes
显示剩余10条评论

83

在这个答案中,我选择探讨“Java简单AES加密/解密示例”的主题,而不是具体的调试问题,因为我认为这将对大多数读者有所裨益。

这是我关于Java中AES加密的博客文章的简单总结,因此我建议在实施任何内容之前先阅读它。然而,我仍然会提供一个简单的示例供使用,并给出一些要注意的指针。

在这个示例中,我将选择使用认证加密Galois/Counter Mode或GCM模式。原因是在大多数情况下,您希望将完整性和真实性与机密性相结合(在博客中了解更多)。

AES-GCM加密/解密教程

以下是使用AES-GCMJava密码架构(JCA)进行加密/解密所需的步骤。 不要与其他示例混淆,因为微小的差异可能会使您的代码完全不安全。

1. 创建密钥

由于它取决于您的用例,我将假设最简单的情况:一个随机的秘密密钥。

SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, "AES");

重要提示:

2. 创建初始化向量

使用初始化向量(IV)可以使得相同的密钥生成不同的加密文本

byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);

重要提示:

  • 永远不要在相同的密钥下重复使用相同的IV(在GCM/CTR模式下非常重要)
  • IV必须是唯一的(即使用随机IV或计数器)
  • IV不需要保密
  • 始终使用强大的伪随机数生成器,例如{{link5:SecureRandom}}
  • 12字节IV是AES-GCM模式的正确选择

3. 使用IV和密钥进行加密

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
byte[] cipherText = cipher.doFinal(plainText);

重要:

  • 使用16字节/128位身份验证标签(用于验证完整性/真实性)
  • 身份验证标记将自动附加到密文中(在JCA实现中)
  • 由于GCM的行为类似于流密码,因此不需要填充
  • 加密大块数据时,请使用{{link2:CipherInputStream }}
  • 想要检查是否更改了其他(非机密)数据吗? 您可能希望使用{{link3:关联数据}}与cipher.updateAAD(associatedData); {{link4:此处有更多信息。}}

3. 序列化为单一消息

只需附加IV和密文即可。 如上所述,IV不需要保密。

ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();

如果您需要一个字符串表示,可以选择使用Base64进行编码。可以使用Android的Java 8内置的实现(不要使用Apache Commons Codec - 它是一个糟糕的实现)。编码用于将字节数组“转换”为字符串表示以使其符合ASCII标准,例如:

String base64CipherMessage = Base64.getEncoder().encodeToString(cipherMessage);

4. 准备解密:反序列化

如果您已经对消息进行了编码,请先将其解码为字节数组:

byte[] cipherMessage = Base64.getDecoder().decode(base64CipherMessage)

重要:

5. 解密

初始化密码,并设置与加密相同的参数:

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
//use first 12 bytes for iv
AlgorithmParameterSpec gcmIv = new GCMParameterSpec(128, cipherMessage, 0, 12);
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmIv);
//use everything from 12 bytes on as ciphertext
byte[] plainText = cipher.doFinal(cipherMessage, 12, cipherMessage.length - 12);

重要提示:

  • 如果在加密过程中添加了相关数据,请不要忘记使用cipher.updateAAD(associatedData);进行添加。

可以在此代码片段中找到可工作的代码示例。


请注意,最近的Android(SDK 21+)和Java(7+)实现应该具有AES-GCM。旧版本可能缺乏它。我仍然选择这种模式,因为它比类似的Encrypt-then-Mac(例如AES-CBC + HMAC)更容易实现,而且效率更高。请参阅此文章以了解如何使用HMAC实现AES-CBC

1
问题在于,在SO上要求示例明确是不相关的话题。更大的问题是这些未经审核的代码片段很难验证。我感谢你的努力,但我认为SO不应该成为这个问题的解决场所。 - Maarten Bodewes
2
我确实很欣赏这种努力,所以我只想指出一个错误:“iv必须在与唯一性相结合时是不可预测的(即使用随机iv)”- 这对于CBC模式是正确的,但对于GCM模式则不是。 - Maarten Bodewes
2
但我不认为SO应该是这个问题的地方。你可能是对的,但似乎大多数人都会坚持使用SO。也许大多数用户不会花费必要的时间来完全理解这个主题,但也许有一些人会朝着正确的方向前进 - 你认为初学者指南应该如何发布?事实是,在Java/JCE中,架构真的很难理解,特别是对于不来自密码学研究背景的人 - 而且几乎没有好的指南。 - Patrick
2
如果你不理解这个话题,那么你可能首先不应该使用低级原语。虽然应该是这样,但仍有许多开发人员这样做。我不确定在通常没有太多内容的地方避免发布关于安全/密码学的高质量内容是否是正确的解决方案。- 顺便感谢您指出我的错误。 - Patrick
1
好的,只是因为我喜欢答案中关于内容(而不是目的)的部分:在解密过程中,IV处理可以简化:Java使得直接从现有的字节数组创建IV变得容易。同样适用于解密,它不必从偏移量0开始。所有这些复制都是不必要的。此外,如果您必须发送IV的长度(您需要吗?),那么为什么不使用单个(无符号)字节-您不会超过255个字节的IV,对吧? - Maarten Bodewes
显示剩余4条评论

38

以下是不使用 Apache Commons CodecBase64 的解决方案:

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

public class AdvancedEncryptionStandard
{
    private byte[] key;

    private static final String ALGORITHM = "AES";

    public AdvancedEncryptionStandard(byte[] key)
    {
        this.key = key;
    }

    /**
     * Encrypts the given plain text
     *
     * @param plainText The plain text to encrypt
     */
    public byte[] encrypt(byte[] plainText) throws Exception
    {
        SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);

        return cipher.doFinal(plainText);
    }

    /**
     * Decrypts the given byte array
     *
     * @param cipherText The data to decrypt
     */
    public byte[] decrypt(byte[] cipherText) throws Exception
    {
        SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM);
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);

        return cipher.doFinal(cipherText);
    }
}

使用示例:

byte[] encryptionKey = "MZygpewJsCpRrfOr".getBytes(StandardCharsets.UTF_8);
byte[] plainText = "Hello world!".getBytes(StandardCharsets.UTF_8);
AdvancedEncryptionStandard advancedEncryptionStandard = new AdvancedEncryptionStandard(
        encryptionKey);
byte[] cipherText = advancedEncryptionStandard.encrypt(plainText);
byte[] decryptedCipherText = advancedEncryptionStandard.decrypt(cipherText);

System.out.println(new String(plainText));
System.out.println(new String(cipherText));
System.out.println(new String(decryptedCipherText));

打印:

Hello world!
դ;��LA+�ߙb*
Hello world!

6
这是一个完全可用的例子,就像@chandpriyankara的一样。但为什么要定义“encrypt(String)”的签名而不是“encrypt(byte[])”呢?加密(解密也是如此)是基于字节的过程(无论如何都使用AES)。加密将字节作为输入,并输出字节,解密也是如此(例如:Cipher对象)。现在,一个特定的用例可能是从字符串中获取加密的字节,或者将其作为字符串发送(电子邮件的基于base64的MIME附件...),但这是对编码字节的问题,存在数百个解决方案,与AES/加密完全无关。 - GPI
3
是的,但我发现在处理文本时使用Strings更有用,因为这基本上是我95%的工作时间所涉及的内容,而且最终你还是要进行转换。 - BullyWiiPlaza
11
不,这与chandpriyankara的代码不等价!你的代码使用了通常不安全且不需要的ECB模式。应明确指定CBC模式。如果指定了CBC模式,你的代码将会出现问题。 - Dan
2
完全可用,但极不安全,使用非常糟糕的编程实践。类名命名不当。密钥大小未提前检查。但最重要的是,该代码使用不安全的ECB模式,在原始问题中隐藏了这个问题。最后,它没有指定字符编码,这意味着在其他平台上解码文本可能会失败。 - Maarten Bodewes

24

在我看来,你没有正确处理你的初始化向量(IV)。

自从我最后一次阅读关于AES、IV和分块链接的内容以来已经很长时间了,但你的那一行代码

IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());

看起来不太对。在AES的情况下,您可以将初始化向量视为密码实例的“初始状态”,而且这个状态是一个信息位,您无法从密钥中获取,但可以从加密密码的实际计算中获取。 (有人可能会认为,如果IV可以从密钥中提取出来,那么它将毫无用处,因为在其初始化阶段期间,密钥已经被给予密码实例)。

因此,在加密结束时,应该从密码实例中获取IV作为byte[]。

  cipherOutputStream.close();
  byte[] iv = encryptCipher.getIV();

你需要使用以下byte[]来初始化Cipher并设置模式为DECRYPT_MODE

and you should initialize your Cipher in DECRYPT_MODE with this byte[] :


  IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

那么,你的解密应该没问题了。 希望这可以帮到你。


20
大部分情况下,你不想使用 ECB。只需搜索一下就知道为什么。 - João Fernandes
你好,我正在尝试直接使用这个(希望你不介意)并进行修复。在Java中找到一个可直接插入我的代码的加密/解密工作示例似乎是不可能的。我无法弄清楚的是,口令何时出现?您似乎是从空气中生成密钥,而没有用户输入。如果用户不需要提供任何秘密密钥就能解密数据,那这是什么样的加密方式呢?对我来说这毫无意义。 - Shaggydog
@Shaggydog:这是一个完全不同的问题。生成和存储密钥/IV是一个困难的问题(商标)。 你可以看看https://dev59.com/gHI-5IYBdhLWcg3wCz3y。但基本上,在这个示例中,我们是从无中生有生成密钥的。 另一种方法可能是提示用户输入密码,并将其用作密钥生成算法的输入...查看“基于密码的密钥派生”即PBKDF2作为关键字。 请参阅https://dev59.com/xXNA5IYBdhLWcg3wWsiK的被接受的解决方案。 - GPI
2
@Mushy:同意选择并明确设置来自可信随机源的IV比仅让Cipher实例选择一个更好。另一方面,这个答案解决了将初始化向量与密钥混淆的原始问题。这就是为什么它首先得到了赞成的原因。现在,这篇文章已经成为一个示例代码的参考点,这里的人们提供了一些很棒的例子 - 就在原始问题旁边。 - GPI
3
@GPI 点赞。其他“好例子”并不那么好,它们实际上并没有解答问题。相反,这似乎是新手盲目复制加密样本的地方,而他们并没有意识到可能存在安全问题——而且,总是会有安全问题。 - Maarten Bodewes
显示剩余3条评论

19

您用于解密的 IV 不正确。请更换此代码。

//Decrypt cipher
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(aesKey.getEncoded());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

使用这段代码

//Decrypt cipher
Cipher decryptCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(encryptCipher.getIV());
decryptCipher.init(Cipher.DECRYPT_MODE, aesKey, ivParameterSpec);

那应该解决了您的问题。


以下是Java中简单AES类的示例。我不建议在生产环境中使用此类,因为它可能无法考虑到您应用程序的所有特定需求。

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
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;
import java.util.Base64;

public class AES 
{
    public static byte[] encrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {       
        return AES.transform(Cipher.ENCRYPT_MODE, keyBytes, ivBytes, messageBytes);
    }

    public static byte[] decrypt(final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {       
        return AES.transform(Cipher.DECRYPT_MODE, keyBytes, ivBytes, messageBytes);
    }

    private static byte[] transform(final int mode, final byte[] keyBytes, final byte[] ivBytes, final byte[] messageBytes) throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        final SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        final IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        byte[] transformedBytes = null;

        try
        {
            final Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");

            cipher.init(mode, keySpec, ivSpec);

            transformedBytes = cipher.doFinal(messageBytes);
        }        
        catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException e) 
        {
            e.printStackTrace();
        }
        return transformedBytes;
    }

    public static void main(final String[] args) throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        //Retrieved from a protected local file.
        //Do not hard-code and do not version control.
        final String base64Key = "ABEiM0RVZneImaq7zN3u/w==";

        //Retrieved from a protected database.
        //Do not hard-code and do not version control.
        final String shadowEntry = "AAECAwQFBgcICQoLDA0ODw==:ZtrkahwcMzTu7e/WuJ3AZmF09DE=";

        //Extract the iv and the ciphertext from the shadow entry.
        final String[] shadowData = shadowEntry.split(":");        
        final String base64Iv = shadowData[0];
        final String base64Ciphertext = shadowData[1];

        //Convert to raw bytes.
        final byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        final byte[] ivBytes = Base64.getDecoder().decode(base64Iv);
        final byte[] encryptedBytes = Base64.getDecoder().decode(base64Ciphertext);

        //Decrypt data and do something with it.
        final byte[] decryptedBytes = AES.decrypt(keyBytes, ivBytes, encryptedBytes);

        //Use non-blocking SecureRandom implementation for the new IV.
        final SecureRandom secureRandom = new SecureRandom();

        //Generate a new IV.
        secureRandom.nextBytes(ivBytes);

        //At this point instead of printing to the screen, 
        //one should replace the old shadow entry with the new one.
        System.out.println("Old Shadow Entry      = " + shadowEntry);
        System.out.println("Decrytped Shadow Data = " + new String(decryptedBytes, StandardCharsets.UTF_8));
        System.out.println("New Shadow Entry      = " + Base64.getEncoder().encodeToString(ivBytes) + ":" + Base64.getEncoder().encodeToString(AES.encrypt(keyBytes, ivBytes, decryptedBytes)));
    }
}
请注意,AES与编码无关,这就是为什么我选择单独处理它并且不需要使用任何第三方库的原因。

首先,你还没有回答原始问题。其次,为什么要回答一个已经回答并被广泛接受的问题呢?我原以为这个保护措施可以阻止垃圾信息的传播。 - TedTrippin
14
和被接受的答案一样,我选择通过举例回答你的问题。我提供了一段完全可运行的代码,向你展示如何正确处理初始化向量(IV)等内容。至于你的第二个问题,我认为需要一个更新的答案,因为Apache编解码器现在已不再必要。所以这不是垃圾邮件,请停止纠缠。 - k170
8
IV(Initialization Vector)的特定目的是为了使密文随机化并提供语义安全。如果您使用相同的密钥+IV对,则攻击者可以确定您是否发送了具有与以前相同前缀的消息。IV不必保密,但必须是不可预测的。常见的方法是将IV简单地添加到密文的前缀,并在解密之前将其切掉。 - Artjom B.
4
反对票:硬编码 IV,请参考上面Artjom B.的评论,了解为什么这样做是不好的。 - Murmel
1
CTR模式应该与NoPadding配对使用。除非存在填充预言攻击,否则CTR模式肯定不是替代CBC的必需品,但如果使用CTR,则应使用“/NoPadding”。CTR是一种将AES转换为流密码的模式,流密码操作字节而不是块。 - Maarten Bodewes

1

在线编辑器可运行版本:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
//import org.apache.commons.codec.binary.Base64;
import java.util.Base64;

public class Encryptor {
    public static String encrypt(String key, String initVector, String value) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));

            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());

            //System.out.println("encrypted string: "
              //      + Base64.encodeBase64String(encrypted));

            //return Base64.encodeBase64String(encrypted);
            String s = new String(Base64.getEncoder().encode(encrypted));
            return s;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static String decrypt(String key, String initVector, String encrypted) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encrypted));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static void main(String[] args) {
        String key = "Bar12345Bar12345"; // 128 bit key
        String initVector = "RandomInitVector"; // 16 bytes IV

        System.out.println(encrypt(key, initVector, "Hello World"));
        System.out.println(decrypt(key, initVector, encrypt(key, initVector, "Hello World")));
    }
}

酷,很高兴它有所帮助! - Bhupesh Pant
1
密码不是密钥,IV 不应该是静态的。仍然使用强类型代码,这使得破坏密钥变得不可能。没有指示如何处理 IV,也没有任何概念表明它应该是不可预测的。 - Maarten Bodewes

1

这是对已接受答案的改进。

更改:

(1) 使用随机IV并将其前置到加密文本中

(2) 使用SHA-256从口令生成密钥

(3) 不依赖于Apache Commons

public static void main(String[] args) throws GeneralSecurityException {
    String plaintext = "Hello world";
    String passphrase = "My passphrase";
    String encrypted = encrypt(passphrase, plaintext);
    String decrypted = decrypt(passphrase, encrypted);
    System.out.println(encrypted);
    System.out.println(decrypted);
}

private static SecretKeySpec getKeySpec(String passphrase) throws NoSuchAlgorithmException {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    return new SecretKeySpec(digest.digest(passphrase.getBytes(UTF_8)), "AES");
}

private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
    return Cipher.getInstance("AES/CBC/PKCS5PADDING");
}

public static String encrypt(String passphrase, String value) throws GeneralSecurityException {
    byte[] initVector = new byte[16];
    SecureRandom.getInstanceStrong().nextBytes(initVector);
    Cipher cipher = getCipher();
    cipher.init(Cipher.ENCRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector));
    byte[] encrypted = cipher.doFinal(value.getBytes());
    return DatatypeConverter.printBase64Binary(initVector) +
            DatatypeConverter.printBase64Binary(encrypted);
}

public static String decrypt(String passphrase, String encrypted) throws GeneralSecurityException {
    byte[] initVector = DatatypeConverter.parseBase64Binary(encrypted.substring(0, 24));
    Cipher cipher = getCipher();
    cipher.init(Cipher.DECRYPT_MODE, getKeySpec(passphrase), new IvParameterSpec(initVector));
    byte[] original = cipher.doFinal(DatatypeConverter.parseBase64Binary(encrypted.substring(24)));
    return new String(original);
}

哈希仍然不是基于密码的密钥生成函数/PBKDF。您可以使用随机密钥或使用PBKDF,例如PBKDF2 /基于密码的加密。 - Maarten Bodewes
@MaartenBodewes,你能提出一个改进建议吗? - wvdz
PBKDF2在Java中存在,所以我认为我刚刚建议了一个。好吧,我没有编写一个,但在我看来,这有点过分要求了。有很多基于密码的加密的例子。 - Maarten Bodewes
@MaartenBodewes 我认为这可能是一个简单的修复。出于好奇,如果直接使用此代码,会有哪些具体的漏洞? - wvdz

0

通常依赖于标准库提供的解决方案是一个好主意:

private static void stackOverflow15554296()
    throws
        NoSuchAlgorithmException, NoSuchPaddingException,        
        InvalidKeyException, IllegalBlockSizeException,
        BadPaddingException
{

    // prepare key
    KeyGenerator keygen = KeyGenerator.getInstance("AES");
    SecretKey aesKey = keygen.generateKey();
    String aesKeyForFutureUse = Base64.getEncoder().encodeToString(
            aesKey.getEncoded()
    );

    // cipher engine
    Cipher aesCipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

    // cipher input
    aesCipher.init(Cipher.ENCRYPT_MODE, aesKey);
    byte[] clearTextBuff = "Text to encode".getBytes();
    byte[] cipherTextBuff = aesCipher.doFinal(clearTextBuff);

    // recreate key
    byte[] aesKeyBuff = Base64.getDecoder().decode(aesKeyForFutureUse);
    SecretKey aesDecryptKey = new SecretKeySpec(aesKeyBuff, "AES");

    // decipher input
    aesCipher.init(Cipher.DECRYPT_MODE, aesDecryptKey);
    byte[] decipheredBuff = aesCipher.doFinal(cipherTextBuff);
    System.out.println(new String(decipheredBuff));
}

这将打印出“Text to encode”。

解决方案基于Java加密架构参考指南https://dev59.com/LnnZa4cB1Zd3GeqPsJzd#20591539的答案。


6
永远不要使用ECB模式。没商量。 - Konstantino Sparakis
1
如果使用相同的密钥加密多个数据块,则不应使用ECB,因此对于“要编码的文本”来说,这已经足够了。https://dev59.com/NnM_5IYBdhLWcg3wzmkw#1220869 - andrej
@AndroidDev 键在准备键部分生成:aesKey = keygen.generateKey()。 - andrej

-1

使用 Spring Boot 和 java.util.Base64 的另一种解决方案

加密器类

package com.jmendoza.springboot.crypto.cipher;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

@Component
public class Encryptor {

    @Value("${security.encryptor.key}")
    private byte[] key;
    @Value("${security.encryptor.algorithm}")
    private String algorithm;

    public String encrypt(String plainText) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(key, algorithm);
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        return new String(Base64.getEncoder().encode(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8))));
    }

    public String decrypt(String cipherText) throws Exception {
        SecretKeySpec secretKey = new SecretKeySpec(key, algorithm);
        Cipher cipher = Cipher.getInstance(algorithm);
        cipher.init(Cipher.DECRYPT_MODE, secretKey);
        return new String(cipher.doFinal(Base64.getDecoder().decode(cipherText)));
    }
}

加密器控制器类

package com.jmendoza.springboot.crypto.controller;

import com.jmendoza.springboot.crypto.cipher.Encryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/cipher")
public class EncryptorController {

    @Autowired
    Encryptor encryptor;

    @GetMapping(value = "encrypt/{value}")
    public String encrypt(@PathVariable("value") final String value) throws Exception {
        return encryptor.encrypt(value);
    }

    @GetMapping(value = "decrypt/{value}")
    public String decrypt(@PathVariable("value") final String value) throws Exception {
        return encryptor.decrypt(value);
    }
}

application.properties

server.port=8082
security.encryptor.algorithm=AES
security.encryptor.key=M8jFt46dfJMaiJA0

示例

http://localhost:8082/cipher/encrypt/jmendoza

2h41HH8Shzc4BRU3hVDOXA==

http://localhost:8082/cipher/decrypt/2h41HH8Shzc4BRU3hVDOXA==

jmendoza


-2

优化后的已接受答案版本。

  • 无第三方库

  • 将IV包含在加密消息中(可以是公共的)

  • 密码可以是任意长度

代码:

import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class Encryptor {
    public static byte[] getRandomInitialVector() {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            SecureRandom randomSecureRandom = SecureRandom.getInstance("SHA1PRNG");
            byte[] initVector = new byte[cipher.getBlockSize()];
            randomSecureRandom.nextBytes(initVector);
            return initVector;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    public static byte[] passwordTo16BitKey(String password) {
        try {
            byte[] srcBytes = password.getBytes("UTF-8");
            byte[] dstBytes = new byte[16];

            if (srcBytes.length == 16) {
                return srcBytes;
            }

            if (srcBytes.length < 16) {
                for (int i = 0; i < dstBytes.length; i++) {
                    dstBytes[i] = (byte) ((srcBytes[i % srcBytes.length]) * (srcBytes[(i + 1) % srcBytes.length]));
                }
            } else if (srcBytes.length > 16) {
                for (int i = 0; i < srcBytes.length; i++) {
                    dstBytes[i % dstBytes.length] += srcBytes[i];
                }
            }

            return dstBytes;
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String encrypt(String key, String value) {
        return encrypt(passwordTo16BitKey(key), value);
    }

    public static String encrypt(byte[] key, String value) {
        try {
            byte[] initVector = Encryptor.getRandomInitialVector();
            IvParameterSpec iv = new IvParameterSpec(initVector);
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);

            byte[] encrypted = cipher.doFinal(value.getBytes());
            return Base64.getEncoder().encodeToString(encrypted) + " " + Base64.getEncoder().encodeToString(initVector);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }

    public static String decrypt(String key, String encrypted) {
        return decrypt(passwordTo16BitKey(key), encrypted);
    }

    public static String decrypt(byte[] key, String encrypted) {
        try {
            String[] encryptedParts = encrypted.split(" ");
            byte[] initVector = Base64.getDecoder().decode(encryptedParts[1]);
            if (initVector.length != 16) {
                return null;
            }

            IvParameterSpec iv = new IvParameterSpec(initVector);
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");

            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);

            byte[] original = cipher.doFinal(Base64.getDecoder().decode(encryptedParts[0]));

            return new String(original);
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return null;
    }
}

使用方法:

String key = "Password of any length.";
String encrypted = Encryptor.encrypt(key, "Hello World");
String decrypted = Encryptor.decrypt(key, encrypted);
System.out.println(encrypted);
System.out.println(decrypted);

示例输出:

QngBg+Qc5+F8HQsksgfyXg== yDfYiIHTqOOjc0HRNdr1Ng==
Hello World

你的密码派生函数不安全。我不会期望在所谓的优化代码中看到 e.printStackTrace() - Maarten Bodewes

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