在加密字节后从Cipher获取更新的IV

我正在开发一个需要附加到AES / CTR加密文件的项目。 现在,由于它是计数器模式,我知道我可以将计数器推进到任何位置并开始读取文件中的位置。 我想知道的是,如果有一种方法让我获取Cipher在使用后可以访问的当前IV。

Cipher c = Cipher.getInstance("AES/CTR/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(aeskey, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv); c.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); CipherOutputStream cipher_out = new CipherOutputStream(output, c); try { while (true) { cipher_out.write(input.readByte()); } } catch (EOFException e) { } byte curIV[] = c.getIV(); 

相反,我发现curIV而不是更新的IV具有与我开始进入ivSpec相同的IV。 有没有办法得到目前的IV?

想法是存储:

  

在非对称加密文件中,这种方式可以解密,读取,我们可以从头开始读取AES加密文件,或者我们可以使用我们存储的将新数据附加到输出文件中。

关于如何实现这个的任何其他建议?


Java根据我发现的文档使用以下(接近RFC3686 ):

  

作为其输入CTR,并更新计数器,它被认为是一个大端数。

这是作为上面看到的IvParameterSpec提供的。


除了这一点之外,我想要回来的是计数器,无论我们想要称之为IV,还是我们想把它称为计数器,还是nonce + iv +计数器。


有关太阳能实施点击率的更多信息: http : //javamex.ning.com/forum/topics/questions-on-aes-ctr-mode

在试验“SunJCE”提供商时,CTR模式下的 AES遵循NIST发布的提议,其中初始计数器值仅对每个连续的块递增1。 这与NIST SP 800-38A附录B.1中给出的一般指导一致。 当增加的位数m是块中的位数时, b

这与RFC 3686相反。也就是说,整个计数器递增,而不仅仅是RFC 3686中指定的有限部分。

您可以通过计算块(从零开始),或通过测量密文的长度并按块大小执行整数除法来了解块索引。 如果这些选项看起来太简单,您还可以使用相应的纯文本对最后一个密文块进行异或,解密该结果,并减去IV以产生块索引。

要追加,只需将IV设置为原始IV加上块索引。 如果您正在编写可以以部分块结尾的流,那么您将需要做一些额外的工作来使流进入正确的状态。

 int BLOCK_SIZE = 16; BigInteger MODULUS = BigInteger.ONE.shiftLeft(BLOCK_SIZE * 8); ... /* Retrieve original IV. */ byte[] iv = ... ; /* Compute the index of the block to which data will be appended. */ BigInteger block = BigInteger.valueOf(file.length() / BLOCK_SIZE); /* Add the block to the nonce to find the current counter. */ BigInteger nonce = new BigInteger(1, iv); byte[] tmp = nonce.add(block).mod(MODULUS).toByteArray(); /* Right-justify the counter value in a block-sized array. */ byte[] ctr = new byte[BLOCK_SIZE]; System.arraycopy(tmp, 0, ctr, BLOCK_SIZE - tmp.length, tmp.length); /* Use this to initialize the appending cipher. */ IvParameterSpec param = new IvParameterSpec(ctr); 

(糟糕的forms,我知道,但回答我自己的问题,为其他人记录)

更新柜台……

 public static byte[] update_iv(byte iv[], long blocks) { ByteBuffer buf = ByteBuffer.wrap(iv); buf.order(ByteOrder.BIG_ENDIAN); long tblocks = buf.getLong(8); tblocks += blocks; buf.putLong(8, tblocks); return buf.array(); } 

AES / CTR-BE的说明

这是基本的想法。 如果您阅读埃里克森对我的问题的回答,您将看到IV基本上是:

 <8 bytes nonce><8 bytes counter> 

计数器以BIG_ENDIAN格式存储,因此如果你要在状态1取出计数器,你会得到这个:

 0x0 0x0 0x0 0x1 

然后,当它到达第二个块时,它将其更新为

 0x0 0x0 0x0 0x2 

依此类推,它可以在技术上溢出到现时,但不建议首先加密那么多的数据。

现在我个人随机创建nonce / counter。 因此,它变得更难以猜测,这不是一个要求。

上面做的是允许你更新counter ,你想要进入计数器的块数,无论你是从0x1开始还是任何其他计数器值(像我一样随机)都没关系。

现在,如果我们以半个或更少的块结束,我们需要确保我们在AES-CTR中向前移动几个字节,所以我们可以简单地做到:

 c.update(new byte[count]) 

其中count是到块中的距离的字符count

我的实施解释

我将keyfile存储在磁盘上的方式(明文,请不要这样做!)如下:

 <16 bytes AES key> <8 bytes nonce> <8 bytes counter> <8 bytes (long) block count> <4 byte partial block count> 

这为我们提供了在已经加密的文件末尾附加内容所需的所有信息,而无需先解密任何内容。 对于需要加密的日志文件以及可以流式传输的任何其他内容来说,这绝对是太棒了。

测试

我测试它实际工作的方式如下:

 echo "1234567890ABCDEF" > file1 echo "0987654321ABCDEFGHIJKLMNOPQRSTUVWXYZ" > file2 cat file1 file2 > file3 

现在,如果我们加密file1然后追加file2我们应该获得与加密file3时相同的输出,只要我们对两者使用相同的密钥/ IV。

 javac AESTest.java # Compile the java file java AESTest key file1 append.aes java AESTest key file2 append.aes append 

添加append告诉程序进入追加模式并向前移动块计数并使用前面提到的c.update()方法部分进入下一个CTR周期。 从那里开始加密就像任何其他时间一样,只需将数据附加到输出文件即可。

 java AESTest key file3 noappend.aes 

由于我的程序将忽略块计数/部分块计数,除非您传入参数append,这将只是使用与以前相同的键/ IV开始加密文件。

现在,如果我们使用HexEditor或vbindiff查看这两个文件,我们可以validation这两个文件是完全相同的,但事实之后还有一个内容附加到它上面。

完整来源……

(请注意,这是我自高中以来第一次使用Java编程,这是几年前的,请原谅可怕的代码)

我的程序的完整源代码,其中所有这些都已实现。

 import java.util.Random; import java.security.*; import javax.crypto.*; import javax.crypto.spec.*; import java.lang.String; import java.io.File; import java.io.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; public class AESTest { public static byte[] update_iv(byte iv[], long blocks) { ByteBuffer buf = ByteBuffer.wrap(iv); buf.order(ByteOrder.BIG_ENDIAN); long tblocks = buf.getLong(8); tblocks += blocks; buf.putLong(8, tblocks); return buf.array(); } public static void main(String args[]) throws Exception { if (args.length < 3) { System.out.println("Not enough parameters:"); System.out.println("keyfile input output [append]"); return; } File keyfile = new File(args[0]); DataInputStream key_in; DataOutputStream key_out; DataInputStream input = new DataInputStream(new FileInputStream(args[1])); DataOutputStream output = null; byte key[] = new byte[16 + 16]; byte aeskey[] = new byte[16]; byte iv[] = new byte[16]; byte ivOrig[] = new byte[16]; long blocks = 0; int count = 0; if (!keyfile.isFile()) { System.out.println("Creating new key"); Random ranGen = new SecureRandom(); ranGen.nextBytes(aeskey); ranGen.nextBytes(iv); iv = update_iv(iv, 0); System.arraycopy(iv, 0, ivOrig, 0, 16); } else { System.out.println("Using existing key..."); key_in = new DataInputStream(new FileInputStream(keyfile)); try { for (int i = 0; i < key.length; i++) key[i] = key_in.readByte(); } catch (EOFException e) { } System.arraycopy(key, 0, aeskey, 0, 16); System.arraycopy(key, 16, iv, 0, 16); System.arraycopy(key, 16, ivOrig, 0, 16); if (args.length == 4) { if (args[3].compareTo("append") == 0) { blocks = key_in.readLong(); count = key_in.readInt(); System.out.println("Moving IV " + blocks + " forward"); iv = update_iv(iv, blocks); output = new DataOutputStream(new FileOutputStream(args[2], true)); // Open file in append mode } } } if (output == null) output = new DataOutputStream(new FileOutputStream(args[2])); // Open file at the beginnging key_out = new DataOutputStream(new FileOutputStream(keyfile)); Cipher c = Cipher.getInstance("AES/CTR/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(aeskey, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv); c.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); if (count != 0) { c.update(new byte[count]); } byte cc[] = new byte[1]; try { while (true) { cc[0] = input.readByte(); cc = c.update(cc); output.writeByte(cc[0]); if (count == 15) { blocks++; count = 0; } else { count++; } } } catch (EOFException e) { } cc = c.doFinal(); if (cc.length != 0) output.writeByte(cc[0]); // Before we quit, lets write our AES key, start IV, and current IV to disk for (int i = 0; i < aeskey.length; i++) key_out.writeByte(aeskey[i]); for (int i = 0; i < ivOrig.length; i++) key_out.writeByte(ivOrig[i]); System.out.println("Blocks: " + blocks); System.out.println("Extra: " + count); key_out.writeLong(blocks); key_out.writeInt(count); } } 

你是对的, getIV返回原始的IV,而不是在发生一些加密/解密后的当前IV。

在Java中,在CTR模式下传递给AES块密码的16个字节是IV加上当前块编号(添加好像两个都是大端格式的16字节bignums,请参见下面的代码)。

请务必阅读此StackOverflowpost ,它有很多很好的建议,可以避免CTR模式的安全隐患(摘要:NEVER使用相同的IV加密两次)。

对于您的用例,您只需要存储起始IV加上块编号(如果您可以通过其他方式获取文件大小,则甚至不需要存储块编号)。 您可以从中计算当前IV,以进行进一步加密或随机搜索解密。

在给定块编号的情况下计算正确IV的代码(第一个块为0):

 int block = ...; byte[] iv = ...; byte[] blockbytes = new byte[16]; for (int i = 0; i < 4; i++) blockbytes[15 - i] = (byte)(block >> 8*i); int carry = 0; for (int i = 15; i >= 0; i--) { int sum = (iv[i] & 255) + (blockbytes[i] & 255) + carry; iv[i] = (byte)sum; carry = sum >> 8; } 

警告:我从弄清楚代码的作用得到了这个 – 我没有在规范中看到它,所以算法可能因提供者而异。

这是一个更完整的测试程序,您可以尝试:

 import java.math.*; import javax.crypto.*; import javax.crypto.spec.*; public class TestCTR { static SecretKeySpec keySpec = new SecretKeySpec(new BigInteger("112233445566778899aabbccddeeff00", 16).toByteArray(), "AES"); static IvParameterSpec ivSpec = new IvParameterSpec(new BigInteger("66778899aaffffffffffffffffffffff", 16).toByteArray()); public static void main(String[] args) throws Exception { byte[] plaintext = new byte[256]; for (int i = 0; i < 256; i++) plaintext[i] = (byte)i; // encrypt with CTR mode byte[] ciphertext = new byte[256]; Cipher c = Cipher.getInstance("AES/CTR/NoPadding"); c.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); c.doFinal(plaintext, 0, 256, ciphertext, 0); // decrypt, implementing CTR mode ourselves Cipher b = Cipher.getInstance("AES/ECB/NoPadding"); b.init(Cipher.ENCRYPT_MODE, keySpec); for (int block = 0; block < 16; block++) { byte[] iv = ivSpec.getIV(); int carry = 0; byte[] blockbytes = new byte[16]; for (int i = 0; i < 4; i++) blockbytes[15 - i] = (byte)(block >> 8*i); for (int i = 15; i >= 0; i--) { int sum = (iv[i] & 255) + (blockbytes[i] & 255) + carry; iv[i] = (byte)sum; carry = sum >> 8; } b.doFinal(iv, 0, 16, iv, 0); for (int i = 0; i < 16; i++) plaintext[block*16+i] = (byte)(ciphertext[block*16+i] ^ iv[i]); } // check it for(int i = 0; i < 256; i++) assert plaintext[i] == (byte)i; } }