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

时间:2011-10-04 03:06:12

标签: java cryptography

我正在开发一个需要附加到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

3 个答案:

答案 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();
}

AES / CTR-BE

的说明

这是基本的想法。如果您阅读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;
  }
}