我正在开发一个需要附加到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 key><begin IV><current IV>
在非对称加密文件中,这种方式可以解密,读取,我们可以从头开始读取AES加密文件,或者我们可以使用我们存储的<current IV>
将新数据附加到输出文件
关于如何实现这一点的任何其他建议?
根据我发现的文档,Java使用以下内容(接近RFC3686):
<NONCE><COUNTER>
作为对CTR的输入,并更新计数器,它被认为是一个大端数。
这是以上面提到的IvParameterSpec
提供的。
除了重点之外,我想要回复的是计数器,无论我们是想要调用IV,还是我们想把它称为计数器,还是nonce + iv + counter。
有关太阳队实施点击率的更多信息:http://javamex.ning.com/forum/topics/questions-on-aes-ctr-mode
答案 0 :(得分:4)
在试验“SunJCE”提供程序时,CTR模式下的AES跟在the proposal published by NIST,之后,其中初始计数器值在每个连续的块中简单地递增1。这与NIST SP 800‑38A, Appendix 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);
答案 1 :(得分:1)
(糟糕的形式,我知道,但回答我自己的问题,为其他人记录)
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();
}
这是基本的想法。如果您阅读erickson对我的问题的回答,您会看到IV基本上是:
<8 bytes nonce><8 bytes counter>
计数器以BIG_ENDIAN格式存储,因此,如果你要在状态1取出计数器,你就会得到这个:
0x0 0x0 0x0 0x1
然后当它到达第二个块时,它将其更新为
0x0 0x0 0x0 0x2
等等,它可以在技术上溢出到nonce中,但是不建议首先加密那么多数据。
现在我个人随机创建nonce / counter。因此,它变得更难以猜测,这不是一个要求。
以上操作允许您更新counter
计数器中有多少个块,无论您是从0x1开始还是以任何其他计数器值开始(像我一样随机)
现在,如果我们以半个或更少的块结束,我们需要确保我们在AES-CTR中向前移动几个字节,所以我们可以这样做:
c.update(new byte[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。 / p>
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
查看这两个文件,我们可以验证这两个文件完全相同,但事实之后还有一个内容附加到其中。
(请注意,这是我自高中以来第一次用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);
}
}
答案 2 :(得分:0)
你是对的,getIV
返回原始IV,而不是当前加密/解密后的当前IV。
在Java中,在CTR模式下传递给AES分组密码的16个字节是IV加上当前的块编号(添加好像两端都是大端格式的16字节bignums,请参阅下面的代码)。
请务必阅读this StackOverflow post,它有很多建议可以避免CTR模式的安全隐患(摘要:永远不会使用相同的IV加密两次)。
对于您的用例,您只需要存储起始IV加上块编号(如果您可以通过其他方式获取文件大小,则甚至不需要存储块编号)。您可以从中计算当前IV,以进行进一步加密或随机搜索解密。
在给定块编号(第一个块为0)的情况下计算正确使用的IV的代码:
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;
}
}