【问题标题】:Java - Missing final characters when encrypting using blowfishJava - 使用河豚加密时缺少最终字符
【发布时间】:2012-06-02 03:09:56
【问题描述】:

我正在使用一些使用 Blowfish 加密文本文件内容的 Java 代码。当我将加密文件转换回来(即解密)时,字符串末尾缺少一个字符。任何想法为什么?我对 Java 很陌生,并且一直在摆弄这个几个小时,但没有运气。

文件war_and_peace.txt 只包含字符串“这是一些文本”。 decrypted.txt 包含“This is some tex”(最后没有 t)。这是java代码:

public static void encrypt(String key, InputStream is, OutputStream os) throws Throwable {
    encryptOrDecrypt(key, Cipher.ENCRYPT_MODE, is, os);
}

public static void decrypt(String key, InputStream is, OutputStream os) throws Throwable {
    encryptOrDecrypt(key, Cipher.DECRYPT_MODE, is, os);
}

private static byte[] getBytes(String toGet)
{
    try
    {
        byte[] retVal = new byte[toGet.length()];
        for (int i = 0; i < toGet.length(); i++)
        {
            char anychar = toGet.charAt(i);
            retVal[i] = (byte)anychar;
        }
        return retVal;
    }catch(Exception e)
    {
        String errorMsg = "ERROR: getBytes :" + e;
        return null;
    }
}

public static void encryptOrDecrypt(String key, int mode, InputStream is, OutputStream os) throws Throwable {


   String iv = "12345678";
   byte[] IVBytes = getBytes(iv);
   IvParameterSpec IV = new IvParameterSpec(IVBytes);


    byte[] KeyData = key.getBytes(); 
    SecretKeySpec blowKey = new SecretKeySpec(KeyData, "Blowfish"); 
    //Cipher cipher = Cipher.getInstance("Blowfish/CBC/PKCS5Padding");
    Cipher cipher = Cipher.getInstance("Blowfish/CBC/NoPadding");

    if (mode == Cipher.ENCRYPT_MODE) {
        cipher.init(Cipher.ENCRYPT_MODE, blowKey, IV);
        CipherInputStream cis = new CipherInputStream(is, cipher);
        doCopy(cis, os);
    } else if (mode == Cipher.DECRYPT_MODE) {
        cipher.init(Cipher.DECRYPT_MODE, blowKey, IV);
        CipherOutputStream cos = new CipherOutputStream(os, cipher);
        doCopy(is, cos);
    }
}

public static void doCopy(InputStream is, OutputStream os) throws IOException {
    byte[] bytes = new byte[4096];
    //byte[] bytes = new byte[64];
    int numBytes;
    while ((numBytes = is.read(bytes)) != -1) {
        os.write(bytes, 0, numBytes);
    }
    os.flush();
    os.close();
    is.close();
}   

public static void main(String[] args) {


    //Encrypt the reports
    try {
        String key = "squirrel123";

        FileInputStream fis = new FileInputStream("war_and_peace.txt");
        FileOutputStream fos = new FileOutputStream("encrypted.txt");
        encrypt(key, fis, fos);

        FileInputStream fis2 = new FileInputStream("encrypted.txt");
        FileOutputStream fos2 = new FileOutputStream("decrypted.txt");
        decrypt(key, fis2, fos2);
    } catch (Throwable e) {
        e.printStackTrace();
    }
}

`

【问题讨论】:

标签: java encryption blowfish


【解决方案1】:

密钥是二进制的,String 不是二进制数据的容器。使用字节[]。

【讨论】:

  • 我有一个名为 getBytes() 的方法,可以将密钥字符串转换为 byte[] - 这就是问题所在吗?
  • 如果您没有准确指定所需的编码,可能会出现问题。
【解决方案2】:

当我遇到这个问题时,我不得不在密码上调用 doFinal:

http://docs.oracle.com/javase/1.4.2/docs/api/javax/crypto/Cipher.html#doFinal()

【讨论】:

  • 调用 doFinal 很重要,但关闭 CipherInputStream/CipherOutputStream 已经为您完成了这项工作。这不是这里的问题,丢失字节的原因是没有使用填充。
【解决方案3】:

这里有几件事不是最佳的。

但是让我们首先解决您的问题。输入的最后一部分以某种方式丢失的原因是您指定的填充:无!在不指定填充的情况下,Cipher 可以只对全长块(Blowfish 为 8 个字节)进行操作。长度小于一个块的多余输入将被静默丢弃,并且您缺少文本。详细说明:“This is some text”的长度为 17 个字节,因此将解密两个完整的块,并丢弃最后的第 17 个字节“t”。

始终将填充与对称分组密码结合使用,PKCS5Padding 很好。

接下来,当使用Cipher 操作时,您不需要实现自己的getBytes() - String#getBytes 已经为您完成了这项工作。只要确保在获取字节时以及稍后从字节重构String 时使用相同的字符编码,这是常见的错误来源。

你应该看看JCE docs,他们会帮助你避免一些常见的错误。

例如,直接使用字符串密钥对于对称加密来说是不可行的,它们不包含足够的熵,这会使暴力破解这样的密钥变得更容易。 JCE 为您提供KeyGenerator 类,您应该始终使用它,除非您确切知道自己在做什么。它会为您生成适当大小的安全随机密钥,但除此之外,人们往往会忘记这一点,它还将确保它不会创建弱密钥。例如,在实际使用中应避免使用已知的 Blowfish 弱密钥。

最后,在进行 CBC 加密时不应使用确定性 IV。最近有一些攻击可以利用这一点,从而完全恢复消息,这显然不是很酷。 IV 应始终随机选择(使用SecureRandom)以使其不可预测。 Cipher 默认为您执行此操作,您可以简单地使用 Cipher#getIV 加密后获得使用的 IV。

另一方面,与安全性相关性较低:您应该关闭 finally 块中的流,以确保不惜一切代价关闭它们 - 否则您将留下一个打开的文件句柄,以防出现异常。

这是您的代码的更新版本,它考虑了所有这些方面(必须使用字符串而不是 main 中的文件,但您可以简单地将其替换为您那里的内容):

private static final String ALGORITHM = "Blowfish/CBC/PKCS5Padding";

/* now returns the IV that was used */
private static byte[] encrypt(SecretKey key, 
                              InputStream is, 
                              OutputStream os) {
    try {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key);
        CipherInputStream cis = new CipherInputStream(is, cipher);
        doCopy(cis, os);
        return cipher.getIV();
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

private static void decrypt(SecretKey key, 
                            byte[] iv, 
                            InputStream is, 
                            OutputStream os) 
{
    try {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        CipherInputStream cis = new CipherInputStream(is, cipher);
        doCopy(cis, os);
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

private static void doCopy(InputStream is, OutputStream os) 
throws IOException {
    try {
        byte[] bytes = new byte[4096];
        int numBytes;
        while ((numBytes = is.read(bytes)) != -1) {
            os.write(bytes, 0, numBytes);
        }
    } finally {
        is.close();
        os.close();
    }
}

public static void main(String[] args) {
    try {
        String plain = "I am very secret. Help!";

        KeyGenerator keyGen = KeyGenerator.getInstance("Blowfish");
        SecretKey key = keyGen.generateKey();
        byte[] iv;

        InputStream in = new ByteArrayInputStream(plain.getBytes("UTF-8"));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        iv = encrypt(key, in, out);

        in = new ByteArrayInputStream(out.toByteArray());
        out = new ByteArrayOutputStream();
        decrypt(key, iv, in, out);

        String result = new String(out.toByteArray(), "UTF-8");
        System.out.println(result);
        System.out.println(plain.equals(result)); // => true
    } catch (Exception e) {
        e.printStackTrace();
    }
}

【讨论】:

    【解决方案4】:

    您的CipherInputStreamCipherOutputStream 混淆了。要加密,您从普通输入流中读取并写入CipherOutputStream。解密……你懂的。

    编辑:

    发生的情况是您指定了 NOPADDING 并且您正在尝试使用 CipherInputStream 进行加密。前 16 个字节构成两个有效的完整块,因此被正确加密。然后只剩下 1 个字节,当 CipherInputStream 类收到文件结束指示时,它对密码对象执行Cipher.doFinal() 并收到 IllegalBlockSizeException。此异常被吞下,并且 read 返回 -1 指示文件结束。但是,如果您使用 PKCS5PADDING 一切都应该工作。

    编辑2:

    emboss 是正确的,因为真正的问题是使用带有 NOPADDING 选项的 CipherStream 类很棘手且容易出错。事实上,这些类明确声明它们会默默地吞下底层 Cipher 实例抛出的每个安全异常,因此它们可能不是初学者的好选择。

    【讨论】:

    • 这可能是最直观的方式,但没有必要这样做。 OP 的方式有效 - CipherInputStream 和 CipherOutputStream 分别是 FilterInputStreams 和 FilterOutputStreams,因此应用它们的“方向”无关紧要。
    • 我必须查看 CipherInputStream 的源代码,但如果您指定 NOPADDING,它显然会忽略任何部分块。这就是为什么它只读取前 16 个字节而不是第 17 个字节。
    • 啊,好的,我明白你的意思了。我想知道为什么它不抛出异常,我要检查 CipherOutputStream 的行为是否不同。如果是这样,那将是一个错误,你不觉得吗?
    • CipherOutputStream 以同样的方式吞下异常。因此,至少它的怪异之处是一致的 :) +1 用于在源代码中查找它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-03
    • 1970-01-01
    • 2012-01-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多