Java – 使用blowfish加密时缺少最终字符

我正在使用一些使用Blowfish加密文本文件内容的j​​ava代码。 当我将加密文件转换回来(即解密它)时,字符串从末尾开始缺少一个字符。 有什么想法吗? 我对Java很陌生,并且在没有运气的情况下花了好几个小时。

war_and_peace.txt文件只包含字符串“This is some text”。 decrypted.txt包含“这是一些tex”(最后没有)。 这是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(); } } 

`

这里有一些不太理想的事情。

但是,让我们首先解决你的问题。 输入的最后一部分以某种方式丢失的原因是您指定的填充:无! 在没有指定填充的情况下, Cipher可以在全长块上运行(Blowfish为8个字节)。 超过一个块长的超额输入将被静默丢弃,并且存在您丢失的文本。 详细说明:“这是一些文本”长度为17个字节,因此将解密两个完整的块,并丢弃最后的第17个字节“t”。

总是使用填充与对称分组密码组合,PKCS5Padding很好。

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

您应该查看JCE文档 ,它们将帮助您避免一些常见错误。

例如,直接使用String键是对称密码术的禁忌,它们不包含足够的熵,这样可以更容易地强制使用这样的密钥。 JCE为您提供KeyGenerator类,除非您确切知道自己在做什么,否则应始终使用它。 它为您生成一个适当大小的安全随机密钥,但此外,这是人们往往会忘记的东西,它还将确保它不会创建一个弱密钥。 例如,在实际使用中应该避免使用已知的Blowfish弱键。

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

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

这是您的代码的更新版本,它考虑了所有这些方面(必须使用字符串而不是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(); } } 

您将CipherInputStreamCipherOutputStream混淆了。 要加密,可以从普通输入流中读取并写入CipherOutputStream 。 要解密……你明白了。

编辑:

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

编辑2:

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

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

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

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