Tuesday, January 28, 2014

Cipher Mode - CFB(Cipher Feedback)

Block Cipher在效率上要明显高于Stream Cipher。但是,并不能武断的认为Block Cipher就一定比Stream Cipher好。在不同的应用环境下,适合的Cipher种类也是不一样的。比如想要实现Putty这类终端模拟器,在传输命令的时候需要对命令进行加密传输。此时,就不应该使用Block Cipher。可以想象一下,我们使用Putty输入一个命令ls,等了半天,没有反映,原因是输入的数据不足一个block。这样的用户体验很明显是不能被接受的。通常的做法是每输入一个字符,就将这个字符加密并且传输到服务器上。在Java中,所有的Cipher都是Block Cipher,那么想要实现这种Stream Cipher应该怎么办呢?需要通过使用能够模拟Stream Cipher的Mode来完成。

CFB - Cipher Feedback

在CFB Mode中,我们可以在Block Size的基础上设置每次加密的数据量,以下称之为Actual Size(Actual Size必须小于或者等于算法的Block Size)。对于一种特例,是Actual Size正好等于Block Size,我们先从这种最容易理解的情况开始入手。

这种特例的加密流程如下:

  1. Ci = EK(Ci-1) ⊕ Pi

    第i个Block的加密结果是这样得到的:首先,将第i-1个Block的加密结果进行加密即EK(Ci-1) 我们称之为Encrypted Block,然后,将这个加密结果与第i个Block进行异或操作。

  2. C0 = IV

    对于第一个Block,因为它前面没有任何Block的加密结果,因此需要将IV进行加密EK(IV),然后,将加密结果和第一个Block进行异或,得到第一个Block的加密结果。

加密过程如下图:

CFB encryption.svg

同样是针对这种特殊情况,解密的过程是相反的:

  1. Pi = EK(Ci-1) ⊕ Ci

    第i个Block的解密过程是这样:首先,将第i-1个Block的加密结果进行加密,即EK(Ci-1),然后将这个加密结果和第i个Block的密文进行异或操作。

    这里需要注意下,无论是加密还是解密,都需要使用加密算法对前一个Block进行加密。可能会觉得很奇怪,但仔细想想我们就会明白:因为在整个的加密过程中,我们实际上是把一连串的明文Block与一连串的Encrypted Block (即EK(Ci-1)) 进行异或操作(我们没有直接加密明文数据的过程,明文数据是通过与Encrypted Block异或后转变成密文的),进而得到最终的加密结果。因此CFB Mode的本质,就是计算一连串的Encrypted Block,因为加密和解密的时候IV是一样的,所以我们必须使用相同的方式来计算这一连串的Encrypted Block。因此,“在加密和解密过程中都需要使用加密算法对Ci-1进行加密”,这一点就说得通了。

  2. C0 = IV

    同样,这里和加密是一样的。

解密过程如下图:

CFB decryption.svg

因为在CFB Mode内部,无论是加密还是解密,都要使用加密来计算Encrypted Block(EK(Ci-1)),那么如果我们把CFB Mode应用到非对称加密算法中,就会产生一个比较奇怪的现象。无论是加密还是解密,使用的都是同一个Key(如果加密使用Private Key,那么解密也需要使用Private Key;如果加密使用Public Key,那么解密也需要使用Public Key),因为内部进行的都是加密操作,因此需要使用相同的Key才能得到相同的Encrypted Block序列。这样看来,非对称加密算法,在CFB Mode下,变成了对称加密的表现形式。这里仅仅是纯粹的理论分析,实际上非对称加密算法的CFB Mode没有任何意义,因为表现形式和对称加密一样,但是效率却是非对称加密的效率(把对称加密算法和非对称加密算法的缺点都占了)。可能正是因为如此,Java中没有实现RSA或者DSA算法的CFB Mode。

下面是一个AES应用CFB Mode的例子:

public class CFBDemo {
    public static byte[] transform(String transformation, int mode, Key key, byte[] iv, byte[] data) {
        try {
            Cipher cipher = Cipher.getInstance(transformation);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            cipher.init(mode, key, ivParameterSpec);
            byte[] transformedData = cipher.doFinal(data);
            return transformedData;
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static byte[] encrypt(byte[] data, Key key, byte[] iv) {
        return transform("AES/CFB/PKCS5Padding", Cipher.ENCRYPT_MODE, key, iv, data);
    }

    public static byte[] decrypt(byte[] cipherData, Key key, byte[] iv) {
        return transform("AES/CFB/PKCS5Padding", Cipher.DECRYPT_MODE, key, iv, cipherData);
    }

    public static void main(String[] args) throws Exception {
        Key key = new SecretKeySpec(new byte[]{ 42, 9, -69, -94, -58, -55, 40, -68, 56, -53, 68, 68, 95, -114, -73, 5, 115, -65, -115, -113, -72, -61, 70, 23, -71, -59, -39, 49, 46, -127, -116, -70 }, "AES");
        byte[] iv = { 37, -101, 93, -90, -70, 108, 14, -6, -59, -109, 53, -89, -116, -110, 16, -124 };

        byte[] data = "Hello RSA CFB".getBytes("UTF-8");
        System.out.println(Arrays.toString(data));
        byte[] cipherData = encrypt(data, key, iv);
        System.out.println(Arrays.toString(cipherData));
        byte[] plainData = decrypt(cipherData, key, iv);
        System.out.println(Arrays.toString(plainData));
    }
}
    

运行上面程序,将得到如下输出:

[72, 101, 108, 108, 111, 32, 82, 83, 65, 32, 67, 70, 66]
[17, 80, 5, -46, 44, 36, -103, -5, 120, -86, -34, -77, -123, -27, 79, 54]
[72, 101, 108, 108, 111, 32, 82, 83, 65, 32, 67, 70, 66]
    

第一行是源数据,第二行是加密后的结果,第三行是解密后的结果。可以看到,因为加密的数据不足16 Bytes,所以Cipher对数据进行了Padding。因为CFB Mode可以模拟Stream Cipher,因此可以不适用Padding。通过改变上面代码的Transformation就可以做到这一点。将上面的代码中的Transformation改成AES/CFB/NoPadding,运行程序,将得到如下输出:

[72, 101, 108, 108, 111, 32, 82, 83, 65, 32, 67, 70, 66]
[17, 80, 5, -46, 44, 36, -103, -5, 120, -86, -34, -77, -123]
[72, 101, 108, 108, 111, 32, 82, 83, 65, 32, 67, 70, 66]
    

可以看到,padding的三个字节已经没有了。CFB Mode具体是如何做到这一点的,将会在下面介绍。

CFB的变体

到此为止,我们所看到的CFB Mode和CBC Mode相比,并没有什么本质上的区别。仔细观察它们的流程图,我们同样会发现它们的流程基本是一样的。那么为什么说CFB可以让Block Cipher模拟Stream Cipher呢。接下来我们就来看一下CFB的变体。

我们可以简单的把CFB Mode分成两个部分:

  1. 对前一个Cipher Block进行加密,得到Encrypted Block;(加密部分)
  2. 将Encrypted Block和当前Plain Block进行异或,得到加密结果Cipher Block。(异或部分)

我们可以想象,加密操作要求数据的长度必须是Block Size,而异或操作对数据长度没有要求。而我们已经知道,CFB Mode仅在异或部分使用了Plain Block,因此我们就可以确定,CFB Mode对于Plain Block的长度是没有要求的。而在加密部分,Encrypted Block的长度需要是Block Size。所以在CFB Mode中,我们可以一次加密任意长度的明文数据(必须小于Block Size),只需要保证Ci-1的长度是Block Size即可。

标准的CFB Mode是这样做的,引入一个移位器(Shift Register),移位器的大小和Block Size 的大小一致。

加密:

  1. 将IV存入移位器(Shift Register)中。
  2. 对移位器(Shift Register)中的数据进行加密,将结果保存到Encrypted Buffer中。此处注意:移位器中的数据没有变化。(之前一直以为直接将结果保存在了Shift Register中,后来经过分析实验错误传递机制,才发现了这个理解错误)
  3. 从Encrypted Buffer左侧取出Actual Size个字节,与长度为Actual Size的Plain Block进行异或操作,得到长度为Actual Size的加密结果。当前Encrypted Buffer在这里被丢弃。
  4. 将移位器中的数据左移Actual Size个字节,即,在移位器右侧空出长度为Actual Size的空间。
  5. 把第3步的加密结果从右侧移入移位器中,放在第4步空出的空间里面。
  6. 转到第2步进行下一轮Actual Size个字节的加密。

解密:

  1. 将IV存入移位器中。
  2. 对移位器(Shift Register)中的数据进行加密,将结果保存到Encrypted Buffer中。此处注意:移位器中的数据没有变化。
  3. 从Encrypted Buffer左侧取出Actual Size个字节,与长度为Actual Size的加密数据进行异或操作,得到长度为Actual Size的解密结果。当前Encrypted Buffer在这里被丢弃。
  4. 将移位器中的数据左移Actual Size个字节,即,在移位器右侧空出长度为Actual Size的空间。
  5. 把第3步使用到的加密数据从右侧移入移位器中,放在第4步空出的空间里面。
  6. 转到第2步进行下一轮Actual Size个字节的解密。

比较常用的CFB Mode变体是CFB-8 Mode。CFB-8 Mode——一次加密8 Bits数据,也就是一个Byte。在Java的默认实现中,Byte是最小的加密单位,尽管在技术上CFB Mode可以支持更小的加密单位,但是在Java并没有支持。其实仔细想想,加密单位小于一个Byte是没有什么实际意义的,因为计算机处理数据的基本单位就是Byte,所以Byte已经可以正常满足我们的需求了。

/**
 * 这里在加密的时候没有调用doFinal方法,取而代之的是只调用了update方法。
 * doFinal方法是告诉Cipher消息传输完毕了,可以进行padding了。
 * 因为CFB-8是每次加密一个Byte,所以不需要任何Padding,所以,
 * 这里也就没有必要调用doFinal方法了。
 *
 */
public class CFB8Demo {
    private Cipher cipher;

    public CFB8Demo(String transformation, int mode, Key key, byte[] iv) {
        try {
            cipher = Cipher.getInstance(transformation);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            cipher.init(mode, key, ivParameterSpec);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
            throw new RuntimeException(ex);
        }
    }

    public byte[] encrypt(byte[] data) {
        return cipher.update(data);
    }

    public byte[] decrypt(byte[] cipherData) {
        return cipher.update(cipherData);
    }

    public static void main(String[] args) throws Exception {
        Key key = new SecretKeySpec(new byte[]{ -7, -35, 69, 95, 107, 32, 49, 13, 13, -15, -89, -42, -40, -47, -128, 104, 49, -120, 5, 80, 48, 79, -74, 116, -125, -62, -124, 29, -71, -115, 127, 76 }, "AES");
        byte[] iv = { -86, 119, -62, -100, -21, -63, 38, -88, -75, 27, -9, 29, 115, -17, -117, 106 };

        CFB8Demo cfb8Cipher = new CFB8Demo("AES/CFB8/NoPadding", Cipher.ENCRYPT_MODE, key, iv);
        try (Scanner scanner = new Scanner(System.in)) {
            while (true) {
                String inputText = scanner.nextLine();
                byte[] data = inputText.getBytes("UTF-8");
                byte[] cipherData = cfb8Cipher.encrypt(data);
                /*使用16进制表示法将加密结果输出,实际项目中最好使用Base64*/
                System.out.println(DatatypeConverter.printHexBinary(cipherData));
            }
        }
    }
}
    

运行上面的代码,随便输入任意长度的字符,回车之后都会直接对输入的字符进行加密,而且加密结果中不包含任何的Padding。

CFB vs. CFB-X

让我们来讨论一个问题:在相同的算法,相同的Key,相同的IV的前提下,使用CFB Mode和CFB-8 Mode加密相同的数据,得到的加密结果是否相同?

  1. 将IV进行加密,在这一步CFB和CFB-8得到的结果都是一样的,我们把这个结果标记为EK(C0)
  2. CFB Mode 将第一个Plain Block和EK(C0)进行异或操作,得到的是一个长度为Block Size的Byte数组。

    CFB-8 Mode 将第一个Byte和EK(C0)的第一个Byte进行异或操作,得到长度为1 Byte的加密结果,记作C1

    如此一来可以想象,CFB Mode第一个Block的第一个Byte和CFB-8的第一个Byte是相同的。

  3. CFB处理完了一个Block,CFB-8处理了一个字节。我们让CFB Mode暂时先等待一下CFB-8 Mode。
  4. CFB-8 Mode中,将C0左移8 bits,并且将#2中加密好的一个Byte从右边移入C0中,记作S(C0, C1)。原有C0的第二个Byte现在变成了第一个。
  5. CFB-8 Mode会继续将S(C0, C1)进行加密,之后用于和第二个明文Byte进行异或,得到第二个Byte的加密结果。正如我们所想的,S(C0, C1)和C0的加密结果肯定是不一样的。

结论:CFB Mode和CFB-8 Mode,在上述条件下,只有第一个字节的加密结果是相同的。那么,我们也可以把这个结论更加延伸一下:CFB Mode和CFB-X Mode,在加密算法,Key,IV,明文数据都相同的情况下,只有前X Bits的加密结果是一样的。

我们可以利用下面的代码来验证我们的结论:

public class CFBXCipher {
    private Cipher cipher;

    public CFBXCipher(Key key, int cipherMode, byte[] iv, int length) {
        try {
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            cipher = Cipher.getInstance(String.format("AES/CFB%d/NoPadding", length * 8));
            cipher.init(cipherMode, key, ivParameterSpec);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
            throw new RuntimeException(ex);
        }
    }

    public byte[] encrypt(byte[] data) {
        try {
            byte[] cipherData = cipher.doFinal(data);
            return cipherData;
        } catch (BadPaddingException | IllegalBlockSizeException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * 以下代码将打引出,相同数据在不同的CFB Mode下的加密结果,从CFB-8 ~ CFB-128
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        SecretKey key = new SecretKeySpec(new byte[] { 67, 96, -48, -65, 18, -124, -14, 109, -32, 20, 85, -22, -96, -20, 109, -14, -53, 28, -49, -33, 39, -80, -42, 43, 123, -48, -35, 122, 84, 77, 53, 12 }, "AES");
        byte[] iv = { 111, 50, -91, -79, -45, 75, -30, 110, 75, -83, -27, 7, -94, -35, -41, -2 };

        /*AES 的 Block Size是16 Byte,因此Actual Size不能大于16 Byte*/
        for (int i=1;i<=16;i++) {
            CFBXCipher cipher = new CFBXCipher(key, Cipher.ENCRYPT_MODE, iv, i);
            byte[] data = "ABCDEFGHIJKLMNOP".getBytes("UTF-8");

            byte[] cipherData = cipher.encrypt(data);
            System.out.println(String.format("CFB-%d\t%s", i * 8, Arrays.toString(cipherData)));
        }

    }
}
    
运行上面的程序,将得到如下输出:
CFB-8   [22, -16, 36, -101, -62, -26, -15, -5, 31, -63, -8, -28, 111, -102, 77, -72]
CFB-16  [22, 120, 57, -25, 52, 56, 114, -39, -63, 1, -107, -44, 102, 68, 61, 58]
CFB-24  [22, 120, 119, -4, 12, -119, -2, 82, -120, 68, -113, 79, 44, 30, 39, 96]
CFB-32  [22, 120, 119, 22, 84, 28, -41, -4, 99, -108, -60, 104, 57, 6, -103, -11]
CFB-40  [22, 120, 119, 22, -48, -5, 82, 113, 7, -6, -15, -100, 104, -66, 20, 23]
CFB-48  [22, 120, 119, 22, -48, 82, 124, -5, -102, -26, 40, 16, -91, -44, 65, -84]
CFB-56  [22, 120, 119, 22, -48, 82, 85, -29, 40, -41, -123, -65, 116, -108, 28, -19]
CFB-64  [22, 120, 119, 22, -48, 82, 85, -47, -85, -19, 123, -54, 62, -118, -84, 73]
CFB-72  [22, 120, 119, 22, -48, 82, 85, -47, -126, -34, -11, 100, -125, -61, 120, -37]
CFB-80  [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 83, 118, -56, 103, 60, 52]
CFB-88  [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 109, -116, -71, -119, 48, -124]
CFB-96  [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 109, -93, -89, 113, -28, -62]
CFB-104 [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 109, -93, 58, -79, -55, -47]
CFB-112 [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 109, -93, 58, -103, 48, -104]
CFB-120 [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 109, -93, 58, -103, -54, 80]
CFB-128 [22, 120, 119, 22, -48, 82, 85, -47, -126, -103, 109, -93, 58, -103, -54, -11]
    

仔细观察这个输出结果,完全符合我们上面的结论。

可能会有这样的疑问:为什么CFB-8中,每进行一个Byte的加密后,就要加密一次移位器?为什么不是等到一个完整的Block加密完,再进行移位器的加密,这样就可以让CFB Mode和CFB-8 Mode的加密结果完全一样了?

原因是这样的,如果Block Size正好是Actual Size的整数倍,那么的确可以这样进行,即提高了效率,又能使CFB和CFB-8产生同样的结果。但是CFB被设计成一种更加通用的Mode,也就是说不强制要求Actual Size能够整除Block Size,只要Actual Size小于Block Size即可。想想看,如果Block Size不是Actual Size的整数倍,那么就没法找到一个合适的时间去加密移位器。因此,CFB Mode被设计为这样的加密流程。

错误传递性

如果密文中的一个bit损坏,那么,会对解密后的明文造成什么影响呢?

我们先从最简单的情况开始分析——当Actual Size 和Block Size相等的时候:

CFB Mode在解密的时候,并没有直接对Cipher Block进行解密,而是将Cipher Block和Encrypted Block通过异或操作来计算Plain Block。

  • 对Ci解密,即Pi = EK(Ci-1) ⊕ Ci。如果Ci中有一个bit损坏,并不会影响整个Ci的解密过程,受到影响的只有损坏的那一个bit。
  • 对Ci+1进行解密。同样,我们套用解密公式:Pi+1 = EK(Ci) ⊕ Ci+1。因为我们知道Ci中有一个bit损坏了,这会导致EK(Ci)产生一个和正确数据完全不同的结果(因为是整个Block进行加密的),从而会导致整个Pi+1都是错误的,因此损坏的数据是一个Block Size
  • 对Ci+2进行解密。使用公式计算Pi+2 = EK(Ci+1) ⊕ Ci+2。因为,损坏的bit在Ci中,而解密Ci+2的时候,和Ci没有任何关系,所以损坏的bit对解密Ci+2不会造成任何影响。

综上所述,当密文中1个bit损坏,且Actual Size和Block Size相等的情况下,将会导致解密结果中有1 bit + 1 Block Size的数据损坏,其中1 Bit就对应着密文中损坏的1 bit,而1个 Block Size对应着损坏的1 Bit所在Block之后的一个Block。

接下来,我们再来分析一下Actual Size和Block Size不相等的情况:

CFB Mode加入了Shift Register来处理Actual Size和Block Size不等的情况。但是无论如何Shift,损坏的1 bit始终是无法正确解密的,所以,在最终的解密结果中,肯定会有1 Bit数据受到影响。

因为解密过程是EK(Shift Register) ⊕ Ci所得到的结果,又因为,除了损坏的1 Bit,其它的加密数据都是完好的。所以除了损坏的1 Bit,其它的Ci都是正确的。现在,我们可以把问题简化为“只要保证EK(Shift Register)是正确的,就能保证解密结果的正确性”。那么,我们可以把问题进一步简化“只要保证Shift Register中的数据是正确的,就可以保证解密结果的正确性”。现在我们来想想,什么时候Shift Register中的数据会不正确呢?那就是,当损坏的1 Bit数据驻留在Shift Register中的时候。我们知道,Shift Register每次Shift长度为Actual Size的数据,每次Shift后会进行加密,并且和密文进行异或,得到长度为Actual Size的明文数据。那么受到影响的明文数据量是:T * Actual Size.其中T表示损坏的1 Bit数据从进入Shift Register开始,到离开Shift Register为止,这段期间Shift的次数。那么,问题进一步转化,我们只需要求出这1 Bit在Shift Register的Shift次数即可。这里又分为两种情况:

如果Actual Size能够整除Block Size,那么Shift的次数将是Block Size / Actual Size.

如果Actual Size不能整除Block Size,那么还需要关注损坏的1 Bit在经历了Block Size / Actual Size次Shift之后是否还驻留在Shift Register中。如果损坏的1 Bit已经不在Shift Register了,那么Shift次数就是Block Size / Actual Size;同理,如果还存在,那么Shift的次数是Block Size / Actual Size + 1。

现在我们将算好的T放入上面的公式(T * Actual Size)中,就可以得到受到影响的数据量了。

最小值:Block Size / Actual Size * Actual Size

最大值:(Block Size / Actual Size + 1) * Actual Size

综上所述,当密文中1个bit损坏,且Actual Size和Block Size不相等的情况下,将会导致解密结果中有1 bit + Block Size / Actual Size * Actual Size 到 1 bit + (Block Size / Actual Size + 1) * Actual Size的数据损坏。

让我们写段程序来验证以下上面的结论:

public class CFBXPropagation {
    private Cipher cipher;

    public CFBXPropagation(Key key, int cipherMode, byte[] iv, int length) {
        try {
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            cipher = Cipher.getInstance("AES/CFB" + (length * 8) + "/NoPadding");
            cipher.init(cipherMode, key, ivParameterSpec);
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException ex) {
            throw new RuntimeException(ex);
        }
    }

    public byte[] encrypt(byte[] data) {
        try {
            byte[] cipherData = cipher.doFinal(data);
            return cipherData;
        } catch (BadPaddingException | IllegalBlockSizeException ex) {
            throw new RuntimeException(ex);
        }
    }

    public byte[] decrypt(byte[] cipherData) {
        try {
            byte[] plainData = cipher.doFinal(cipherData);
            return plainData;
        } catch (BadPaddingException | IllegalBlockSizeException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static void main(String[] args) throws Exception {
        SecretKey key = new SecretKeySpec(new byte[] { 67, 96, -48, -65, 18, -124, -14, 109, -32, 20, 85, -22, -96, -20, 109, -14, -53, 28, -49, -33, 39, -80, -42, 43, 123, -48, -35, 122, 84, 77, 53, 12 }, "AES");
        byte[] iv = { 111, 50, -91, -79, -45, 75, -30, 110, 75, -83, -27, 7, -94, -35, -41, -2 };

        byte[] data = "这是一段加密过的消息,如果加密数据中的1 bit出现问题,会对解密结果造成什么样的影响呢?".getBytes();
        System.out.println(Arrays.toString(data));

        CFBXPropagation encryptCipher = new CFBXPropagation(key, Cipher.ENCRYPT_MODE, iv, 1);
        byte[] cipherData = encryptCipher.encrypt(data);
        /*此处通过改变第9个byte的值,来模拟加密数据损坏*/
        cipherData[8]++;

        CFBXPropagation decryptCipher = new CFBXPropagation(key, Cipher.DECRYPT_MODE, iv, 1);
        byte[] plainData = decryptCipher.decrypt(cipherData);
        System.out.println(Arrays.toString(plainData));
        System.out.println(new String(plainData));
    }
}
    

执行上面的程序,将得到如下输出:

[-43, -30, -54, -57, -46, -69, -74, -50, -68, -45, -61, -36, -71, -3, -75, -60, -49, -5, -49, -94, -93, -84, -56, -25, -71, -5, -68, -45, -61, -36, -54, -3, -66, -35, -42, -48, -75, -60, 49, 32, 98, 105, 116, -77, -10, -49, -42, -50, -54, -52, -30, -93, -84, -69, -31, -74, -44, -67, -30, -61, -36, -67, -31, -71, -5, -44, -20, -77, -55, -54, -78, -61, -76, -47, -7, -75, -60, -45, -80, -49, -20, -60, -40, -93, -65]
[-43, -30, -54, -57, -46, -69, -74, -50, -67, 2, 90, 66, -37, -29, 9, 121, -24, 64, -17, 103, -88, 41, 82, -44, 31, -5, -68, -45, -61, -36, -54, -3, -66, -35, -42, -48, -75, -60, 49, 32, 98, 105, 116, -77, -10, -49, -42, -50, -54, -52, -30, -93, -84, -69, -31, -74, -44, -67, -30, -61, -36, -67, -31, -71, -5, -44, -20, -77, -55, -54, -78, -61, -76, -47, -7, -75, -60, -45, -80, -49, -20, -60, -40, -93, -65]
这是一段?ZB坫    y鐯飃?R?用苁葜械? bit出现问题,会对解密结果造成什么样的影响呢?
    

第一行是原始数据,第二行是加密数据损坏1bit后的解密结果。仔细比对这两行数据,我们可以发现一共有17 Bytes是不一样的,正好是1 Bit + 1 Block的数据量,其中1 Bit的损坏被体现在第一个不同的Byte中。

优点:

  1. 可以模拟Stream Cipher,处理需要即时加密的数据。同时,不再需要对数据进行padding。
  2. 并行解密,因为在解密的时候,每个Block仅和前一个Block的加密结果有关,所以多个Block可以并行解密。

缺点:

  1. 尽管可以模拟Stream Cipher,但是降低了效率。因为每次加密一个Actual Size大小的数据,实际上都要加密一次移位器中的数据,而移位器中的数据长度是Block Size。因此CFB-X Mode的效率,要比正常的CBC或者原始的CFB慢 BlockSize / X 倍。同时,增大X的值,效率将会不断的提高,直到X = Block Size,CFB的效率将会和CBC的效率一样。
  2. 因为加密的过程中,需要使用前一个Block的加密结果,因此无法进行多个Block的并行加密。

OFB - Output feedback

与CFB Mode类似的是OFB Mode,不同点是OFB Mode每次不是加密上一步的Cipher Block,而是加密上一步产生的Encrypted Block,并且将得到的结果和Plain Block进行异或,得到这个Block的加密结果。也就是说加密的部分和Plain Data只有异或的关系,没有任何加密的关系。这里对OFB Mode不再做深入介绍,详细信息请参考维基百科,加密解密流程图如下:

OFB encryption.svg

CFB decryption.svg

总结

本文主要介绍了CFB Mode。通过CFB Mode我们可以模拟Stream Cipher来对任意长度的数据进行加密。其中也包含了我在学习CFB Mode的时候的一些自己的分析。

对于这些东西,其实记住了结论即可(比如错误传递性)。之所以这里写下详细的分析过程,都是为了更好的理解CFB Mode。例如,我就是通过分析CFB Mode的错误传递机制,才修正了之前对于CFB Mode原理的一项错误理解。基本使用的分析方法如下:

  1. 查找原理,通过书籍/网上资料对需要分析的东西进行查找,了解其理论原理。
  2. 理解原理,对于查找到的资料进行自我理解,并且在原有资料的基础上充实细节。
  3. 对资料中的原理,细节进行实验验证。
  4. 如果理解的东西无法通过验证,需要进一步分析原因。可能需要查找更多的资料,或者查看源码等。
  5. 解决问题的方式不仅仅有一种,活学活用。

最后附上一段我在研究CFB Mode的过程中写的一个简单的模拟程序,用于模拟实现CFB Mode,其中缺少了一些应有的错误验证,仅供参考:

public class CFBXSimulator {

    private InternalAESCipher cipher;

    private int numBytes = 0;

    private byte[] shiftRegister = new byte[16];

    private byte[] iv = new byte[16];

    private static final int BLOCK_SIZE = 16;

    public CFBXSimulator(Key key, byte[] iv, int numBits) {
        this.cipher = new InternalAESCipher(key);
        this.iv = iv;
        System.arraycopy(this.iv, 0, shiftRegister, 0, iv.length);
        numBytes = numBits / 8;
    }

    public void reset() {
        System.arraycopy(this.iv, 0, shiftRegister, 0, iv.length);
    }

    public byte[] encrypt(byte[] plainData) {
        byte[] out = new byte[plainData.length];
        int loopCount = plainData.length / numBytes;
        for (int i = 0; i < loopCount; i++) {
            byte[] cipherRegister = cipher.encrypt(shiftRegister);
            System.arraycopy(shiftRegister, numBytes, shiftRegister, 0, BLOCK_SIZE - numBytes);
            for (int j = 0; j < numBytes; j++) {
                out[i * numBytes + j] = (byte) (plainData[i * numBytes + j] ^ cipherRegister[j]);
                shiftRegister[(BLOCK_SIZE - numBytes) + j] = out[i * numBytes + j];
            }
        }
        return out;
    }

    public byte[] decrypt(byte[] cipherData) {
        byte[] out = new byte[cipherData.length];
        int loopCount = cipherData.length / numBytes;
        for (int i = 0; i < loopCount; i++) {
            byte[] cipherRegister = cipher.encrypt(shiftRegister);
            System.arraycopy(shiftRegister, numBytes, shiftRegister, 0, BLOCK_SIZE - numBytes);
            for (int j = 0; j < numBytes; j++) {
                out[i * numBytes + j] = (byte) (cipherData[i * numBytes + j] ^ cipherRegister[j]);
                shiftRegister[(BLOCK_SIZE - numBytes) + j] = cipherData[i * numBytes + j];
            }
        }
        return out;
    }
    
    private class InternalAESCipher {
        private Cipher cipher;

        public InternalAESCipher(Key key) {
            try {
                cipher = Cipher.getInstance("AES/ECB/NoPadding");
                cipher.init(Cipher.ENCRYPT_MODE, key);
            } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException ex) {
                throw new RuntimeException(ex);
            }
        }

        public byte[] encrypt(byte[] data) {
            try {
                byte[] cipherData = cipher.doFinal(data);
                return cipherData;
            } catch (IllegalBlockSizeException | BadPaddingException ex) {
                throw new RuntimeException(ex);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        SecretKey key = new SecretKeySpec(new byte[] { 67, 96, -48, -65, 18, -124, -14, 109, -32, 20, 85, -22, -96, -20, 109, -14, -53, 28, -49, -33, 39, -80, -42, 43, 123, -48, -35, 122, 84, 77, 53, 12 }, "AES");
        byte[] iv = { 111, 50, -91, -79, -45, 75, -30, 110, 75, -83, -27, 7, -94, -35, -41, -2 };
        byte[] data = "这是一段加密过得消息,如果加密数据中的1 bit出现问题,会对解密结果造成什么样的影响呢?".getBytes("UTF-8");
        System.out.println(Arrays.toString(data));

        CFBXSimulator simulator = new CFBXSimulator(key, iv, 8);
        byte[] cipherData = simulator.encrypt(data);
        System.out.println(Arrays.toString(cipherData));
        simulator.reset();
        System.out.println(new String(simulator.decrypt(cipherData), "UTF-8"));
    }
}
    

标准的Java实现请参考JDK源码中的CipherFeedback类。

本文图片来自维基百科

参考资料:

  • 维基百科
  • 《Java Cryptography》—— Jonathan B. Knudsen

Thanks for reading

1 comment :