Showing posts with label ECB. Show all posts
Showing posts with label ECB. Show all posts

Wednesday, January 22, 2014

Cipher Mode - EBC & CBC & PCBC

使用Block Cipher的时候可以为Cipher指定不同的Mode。Mode的作用是在加密Block之前或者之后对Block进行一些处理,目的是为了增加安全系数。

EBC - Electronic Codebook

最简单的一种Cipher Mode是ECB(Electronic Codebook),它没有对Block进行任何处理。利用加密算法,对每一个Block无差别地进行加密,并且进行输出。解密的过程则是相反,对每个Block独立进行解密。

ECB encryption.svg

ECB decryption.svg

优点:

  1. 速度比较快,因为Block之间是彼此独立的,甚至可以同时加密或者解密多个Block。
  2. 错误隔离,对于加密的数据,如果某个bit损坏,影响的只是损坏bit所在的Block,其它Block中的数据依然可以正常解密。

缺点:

  • 安全性低,对于数据完全相同的两个Block,将会产生两个完全相同的加密结果。因此在对比多次消息后,可能会被推测出部分消息,或者对某些Block中的内容进行篡改。正因为EBC的安全隐患,所以EBC很少用于正式产品中。除非是用于加密一些完全随机的数据,比如说一个Hash结果等。

CBC - Cipher-block chaining

为了避免出现两个相同的Block产生相同的加密结果,IBM在1976年发明了CBC Mode。它的基本原理是每个Block在加密之前,先与之前的一个Block的加密结果进行异或(XOR),然后再将异或的结果进行加密。对于第一个Block,因为之前没有任何加密过的Block,因此需要一个初始化向量IV(Initialization Vector),与第一个Block进行异或。

加密的流程如下:

CBC encryption.svg

因为A XOR B XOR B = A, 所以在解密的时候需要先将Block进行解密,然后将解密的结果与它之前的Block的密文进行异或,进而得到明文。当然,对于第一个密文Block解密后,依然需要与IV进行异或运算来计算第一个Block的明文,而且IV必须和加密时候使用的IV一致。流程如下:

CBC decryption.svg

优点:

  1. 即使加密前是两个完全相同的Block,在加密之后的结果也是不相同的。因为每一个Block的加密都以赖于前一个Block的加密结果。
  2. 因为在解密一个单独的Block的过程中,仅仅以赖于前一个Block的密文,所以每个Block可以独立解密。可以同时对多个block并行解密。

缺点:

  1. 速度较EBC慢,很容易理解,因为多了一步异或操作。
  2. 错误局部传递,当加密数据中的一个bit损坏,那么会造成这个损坏bit所在的block以及之后的一个block无法正常解密。错误的传递比EBC多了一个Block。
  3. 加密时,每个Block需要用到之前一个Block的加密结果,因此无法并行的加密多个Block。

PCBC - Propagating Cipher-block chaining

PCBC和CBC非常相似,唯一的不同点是CBC在加密之前需要和之前Block的密文进行异或操作,而PCBC需要和前一个Block的密文和明文都进行异或操作。类似的,PCBC也需要一个IV,用于处理第一个Block。流程如下:

PCBC encryption.svg

PCBC decryption.svg

优点:

  1. 即使加密前是两个完全相同的Block,在加密之后也不会产生相同的结果。因为每一个Block的加密,都以赖于前一个Block。
  2. 对于明文或者密文的任何一点改动,将影响到后续所有的Block的加密或者解密的结果。

缺点:

  1. 速度比CBC更慢,因为又多进行了一次异或操作。
  2. 错误传递性较强,如果一个bit损坏,将会导致所有后续的所有Block无法解密。
  3. 无论加密还是解密,都需要用到前一个Block的加密或者解密的结果,因此无法同时加密或者解密多个Block。

关于IV

CBC和PCBC都需要使用IV来加密第一个Block。因为这两种Mode在加密一个Block的时候都需要使用前一个Block的信息。而对于第一个Block,之前没有任何信息,所以需要使用一个IV来“伪造”一个Block。

  • 因为需要“伪造Block”,那么IV的长度需要与Block Size相一致。
  • IV需要是一组随机的Byte。
  • IV不需要进行保密,可以和加密信息一起提供给解密方。

使用方法

在Java中,使用各种Mode的方法基本都是一样的。一个主要的区别是EBC不需要IV,而其它两种Mode需要使用IV对Cipher进行初始化。下面是一个使用CBC进行加密的例子。

public class CBCDemo {
    public static byte[] encrypt(byte[] data, Key key, byte[] iv) {
        try {
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            System.out.println(cipher.getBlockSize());
            cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
            byte[] cipherData = cipher.doFinal(data);
            return cipherData;
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static void main(String[] args) throws Exception {
        /**
         * 生成AES Key
         */
        byte[] rawKey = { 33, -52, -105, 76, -47, 123, 109, 119, 5, 2, 124, 112, -116, 3, -120, -126 };
        SecretKey key = new SecretKeySpec(rawKey, "AES");

        /**
         * 构造IV
         */
        byte[] iv = { -26, -29, -38, -70, 127, -111, 47, -98, -119, 78, -30, -8, 6, 65, -100, -93 };
        byte[] data = "Hello CBC".getBytes("UTF-8");

        /**
         * 加密
         */
        byte[] cipherData = encrypt(data, key, iv);
        System.out.println(DatatypeConverter.printBase64Binary(cipherData));
    }
}

注意,这段代码中的IV是我指定的,如果在初始化Cipher的时候不指定IV,那么Cipher的实现类将会随机生成一个IV,为了能够解密,需要把Cipher生成的IV记录下来。可以通过调用Cipher.getIV()来获取当前Cipher的IV。

对于解密,大致过程和加密是一样的,只是在初始化Cipher的时候把状态换成Cipher.DECRYPT_MODE即可。

本文图片来自于维基百科

参考资料:

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

JCA Engine - Cipher

Cipher的分类

Java中的Cipher类提供了对数据加密解密的功能。根据加密算法的不同,大致可以分为三类:

  • 对称加密(Symmetric):加密和解密使用相同的密钥(SecretKey)。参与加密解密的双方,需要预先商定好密钥。如果保证了密钥的安全,理论上则可以保证加密数据的安全。
  • 非对称加密(Asymmetric):加密和解密使用不同的密钥。比如,加密的时候使用公钥(Public Key),解密的时候需要使用对应的私钥(Private Key),反之亦然。一般将公钥公开,私钥自己保留。比如,B需要给A发送一段加密信息。首先B会向A请求一个A的公钥。B利用请求到的公钥对消息进行加密,并且发送给A。即使消息在传输过程中被截获,因为没有A的私钥,所以也无法解密数据,得到的无非是一些没有意义的字节而已。因此,保证私钥的安全,理论上就可以保证数据的安全。
  • 混合加密(Hybrid):对称加密和非对称加密各有优点:
    • 对称加密的速度比较快,但是,在分发密钥的时候存在安全隐患(比如密钥在传输过中被人截获);
    • 非对称加密不需要事先商定密钥,只需要把公钥公开,因为只有持有私钥的人才能够解密使用公钥加密的数据,而私钥一般是不需要公开的,但是,缺点是非对称加密的效率较对称加密慢了许多。
    所以将二者混合使用,各取优点,便成了混合加密——使用非对称加密来传输对称加密中使用的密钥,使用对称加密来加密真正的数据。因为一组通信过程中,密钥商定只需要进行一次,所以使用非对称加密来商定,保证商定密钥过程的安全性。然而,通信可能需要进行很多次,所以使用对称加密,提高了通信的效率。因为对称加密的密钥是安全的,所以通信的数据也可以认为是安全的。

根据一次加密操作的数据量不同,也可以把加密分成块加密(Block Cipher)和流加密(Stream Cipher)。所谓Block Cipher,就是把数据被拆分成一些固定大小的数据块(比如64 Bits),一次加密一个数据块。与之相对的是流式加密(Stream Cipher),也就是说每次加密的数据长度更小,比如一个Bit或者一个Byte。目前比较流行的是Block Cipher,Java中的Cipher类就是针对于Block Cipher实现的。而且,通过给Block Cipher指定不同的Mode,也可以达到模拟Stream Cipher的作用。

Cipher类使用方法

Cipher是一个Engine类,想要获得它的实例,需要从JCA中请求——通过调用Cipher类的静态方法getInstace来完成请求。在请求Engine实例的时候,Cipher和其它的Engine类是不一样的:请求其它的Engine类时,一般是传入一个算法名称,进而得到一个实现了这种算法的Engine类的实例。但是,在请求Cipher实例的时候,需要传入的是一个“转换模式”——Transformation。一个Transformation包含3个信息:

  • Algorithm:表示Cipher使用的算法,如:DES、AES等。
  • Mode:用于在加密之前或者之后对Block做一些处理,提高安全性。如:ECB、CBC、CFB等。
  • Padding:对于Block Cipher而言,每种算法对于Block Size是有要求的。比如,DES算法的Block Size是8 Bytes;而AES算法的Block Size是16 Bytes。然而,如果需要加密的数据的长度不是Block Size的整数倍,那么,在加密之前先要进行Padding,将数据补全到Block Size的整数倍,以便于接下来进行分块加密。这个属性就使用于指定Padding的规则。

Transformation包含的三个信息需要使用“/”进行分隔。比如我想要使用AES算法,ECB Mode以及PKCS5Padding,那么,我们需要传入的Transformation为:AES/ECB/PKCS5Padding。

Cipher加密步骤

  1. 调用Cipher.getInstance通过传入Transformation向JCA请求对应的Cipher实例。
  2. 通过调用init方法,设置Cipher的状态和Key,对Cipher进行初始化。Cipher本身是有状态的,一个Cipher要么用于加密,要么用于解密。在调用init方法的时候,需要通过传入Cipher的状态来指定当前的Cipher将用于加密还是解密。Cipher的状态在Cipher类中有静态常量定义(ENCRYPT_MODE, DECRYPT_MODE)。另外,每种加密算法都有自己的Key,此处需要用加密算法对应的Key来初始化Cipher。
  3. 调用update方法,将待加密的数据传入Cipher中(可选)。
  4. 调用doFinal方法获得最终的加密结果。

注意:在调用update方法的时候,其返回值是传入的数据的加密结果。如果累计传入到update方法的数据量不足一个Block,那么update方法不会返回任何结果(一个空的byte[])。一旦累计的数据量超过了1倍或者1倍以上的Block Size,那么,这个方法将返回之前所有可以构成Block的数据的加密结果,并且清空已经返回了的Block。之后无论是调用doFinal还是update方法,都不会再返回这些已经返回过的加密结果了。所以说,如果使用update方式逐次的更新数据,那么需要将每次调用update方法的返回值保存起来,最终才能拼接成一个完整的加密结果。

无论目前累计传入Cipher的数据是否可以构成一个Block,如果调用了doFinal方法,那么cipher会将传入的数据进行padding,使之构成Block Size的整数倍,然后返回加密结果。

下面是一个AES加密的例子

public class AESDemo {
    public static byte[] encrypt(byte[] data, Key key) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); //此处可以省略Mode和Padding,Java的默认实现将会使用默认的Mode和Padding(ECB和PKCS5Padding),也就是说"AES"将是一样的效果
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] cipherData = cipher.doFinal(data);
            return cipherData;
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static String encrypt(String text, Key key) {
        try {
            byte[] data = text.getBytes("UTF-8");
            byte[] cipherData = encrypt(data, key);
            String cipherText = DatatypeConverter.printHexBinary(cipherData); //这里使用Base64更好一些,为了之后分析方便,我在这里使用了Hex
            return cipherText;
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static void main(String[] args) {
        byte[] encodedKey = { 9, 93, -54, 55, -33, -43, 42, 102, -89, 116, 125, -19, 120, 59, 92, -110 };
        SecretKey key = new SecretKeySpec(encodedKey, "AES");
        String text = "Hello Cipher";
        String cipherText = encrypt(text, key);
        System.out.println(cipherText);
    }
}
        

运行这个Demo程序,将得到如下输出:

B7324301EDA3890DFA73042934352254

下面来分析以下这个输出结果——长度为32的16进制串,也就是说加密的结果是16 Bytes。

需要加密的消息是"Hello Cipher"——长度为12的字符串。因为都是ASCII字符,所以每个字符占用一个字节,共计12 Bytes。

之所以加密前的数据长度和加密后的数据长度不一样,是因为AES的Block Size是16 Bytes。因为"Hello Cipher"只有12 Bytes,在进行加密的时候会把它Padding到16 Bytes,因此最终的加密结果就是16 Bytes。

Cipher解密步骤

解密的步骤和加密非常相似,唯一的区别是在init Cipher的时候需要将Cipher的状态设置为解密-DECRYPT_MODE

  1. 调用Cipher.getInstance通过Transformation请求对应的Cipher实例。
  2. 调用init方法,传入状态参数DECRYPT_MODE,以及解密时需要用到的Key。
  3. 调用update方法,将待解密的数据传入Cipher中(可选)。
  4. 调用doFinal方法获得最终解密的结果。

下面是AES解密的例子:

public class AESDemo {
    public static byte[] decrypt(byte[] cipherData, Key key) {
        try {
            Cipher cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] data = cipher.doFinal(cipherData);
            return data;
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static String decrypt(String cipherText, Key key) {
        try {
            byte[] cipherData = DatatypeConverter.parseHexBinary(cipherText);
            byte[] data = decrypt(cipherData, key);
            String text = new String(data, "UTF-8");
            return text;
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static void main(String[] args) {
        byte[] encodedKey = { 9, 93, -54, 55, -33, -43, 42, 102, -89, 116, 125, -19, 120, 59, 92, -110 };
        SecretKey key = new SecretKeySpec(encodedKey, "AES");
        String cipherText = "B7324301EDA3890DFA73042934352254";
        String text = decrypt(cipherText, key);
        System.out.println(text);
    }
}
        

运行这个程序,得到如下输出:

Hello Cipher

可以看到,已经从密文中准确的解密出了明文。

关于Mode

对于Block Cipher支持很多种Mode,每种Mode都可以解决一些安全性问题,当然每种Mode都有各自的优点和缺点。这是一个比较庞大的话题,此处暂时不进行讨论。此处仅仅讲解以下最简单的Mode——ECB(Electronic Code Book)

与其说ECB是简单的Mode,不如说ECB根本没有什么功能。它对每个Block不进行任何处理,Block被加密成密文后,直接进行输出,Block和Block之间没有任何关联。对于ECB Mode而言,最大的优点就是block之间没有关联,比如在传输或者存储的过程中,某个字节损坏了(无法进行解密),那么影响的仅仅是其中的一个block,对于其它的block依然可以进行解密,因为block之间没有关联。同样ECB的优点也是它的缺点,导致了一些安全问题,使得采用ECB Mode进行加密的密文容易被攻击(篡改,猜测,解密等...)。

运行下面一段代码:

public class ECBDemo {
    public static void main(String[] args) {
        try {
            Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            System.out.println("Block Size: " + cipher.getBlockSize());
            SecretKey key = new SecretKeySpec(new byte[]{-56, -29, 33, 117, -17, 123, 82, 108, -71, 82, 102, -63, -13, 29, 105, 26, -90, -115, 70, -125, -69, -126, -43, -86, 49, -108, 109, 64, 112, 2, -122, 79}, "AES");
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] data1 = "How are you doing. I have a question...".getBytes("UTF-8");
            byte[] data2 = "How are you doing. I was thinking...".getBytes("UTF-8");
            byte[] cipherData1 = cipher.doFinal(data1);
            byte[] cipherData2 = cipher.doFinal(data2);
            System.out.println(DatatypeConverter.printHexBinary(cipherData1));
            System.out.println(DatatypeConverter.printHexBinary(cipherData2));
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | UnsupportedEncodingException | IllegalBlockSizeException | BadPaddingException ex) {
            throw new RuntimeException(ex);
        }
    }
}
    

将会得到如下输出:

B237616BC0754F0EC16460F9A857D3D67EFB95DA272E49063324C858B2818B189053685C7A7ED49529D8CBCF1FD5552A
B237616BC0754F0EC16460F9A857D3D651FB47DC9EF1BEB43E26917883A5F8514383EF98D738EA87D34EA807F4D2958B
    

我们可以看到,因为两个字符串都是以"How are you doing. "开头,因此导致了第一个Block的加密结果相同。这样就产生了安全隐患,我们可以对这些相同的部分进行猜测,或者篡改等。

如何解决ECB存在的问题呢?需要用到其它的Mode,比如常用的CBC, PCBC, CFB等等。

关于Padding

目前Padding的操作是在Cipher内部帮助我们完成的,让我们来看看Padding的数据是什么样的。

如何来一探究竟呢?上面我们使用AES/ECB/PKCS5Padding对"Hello Cipher"进行了加密,加密结果是B7324301EDA3890DFA73042934352254。因为这个是加密的结果,而且,数据是先Padding然后再加密,所以从这个加密过的结果上是没有办法知道Padding是什么样子的。如果直接用上面的代码解密,Cipher将自动处理掉Padding的内容,导致我们依然没有办法看到Padding是什么样的。

那么想要获取Padding的数据,简单的思路是这样,在解密的时候使用这个Transformation - "AES/ECB/NoPadding"。什么意思呢?这里我们对Cipher撒了个谎“我的加密数据使用AES/ECB进行加密的,其中没有使用Padding(其实是有的,我们心里清楚),所以你不需要帮我把Padding的内容处理掉,直接返回给我解密后的结果即可”。代码如下:

public class PKCS5PaddingInspector {
    public static void main(String[] args) throws Exception {
        byte[] encodedKey = { 9, 93, -54, 55, -33, -43, 42, 102, -89, 116, 125, -19, 120, 59, 92, -110 };
        SecretKey key = new SecretKeySpec(encodedKey, "AES");
        String cipherText = "B7324301EDA3890DFA73042934352254";
        byte[] cipherData = DatatypeConverter.parseHexBinary(cipherText);

        Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key);
        byte[] data = cipher.doFinal(cipherData);
        System.out.println(Arrays.toString(data));
    }
}
    

运行上面的代码,得到如下结果:

[72, 101, 108, 108, 111, 32, 67, 105, 112, 104, 101, 114, 4, 4, 4, 4]
    

分析:

因为,"Hello Cipher"只有12 Bytes,而且,AES的Block Size是16 Bytes,因此需要Padding 4 Bytes。看到上面结果里最后四个字节了么?那就是Padding的数据。下面我们给出PKCS5Padding的规范:

  • 如果需要在末尾添加n个字节,那么添加的n个字节的值都是n
  • 如果不需要进行padding(正好是Block Size的倍数),那么在消息末尾添加值为Block Size的Block Size个字节。比如Block Size是16 Bytes,那么就需要添加16个值为16的Byte。

因此只要是使用了PKCS#5进行padding的消息,都会被添加上额外的字节。那么在解密之后,可以通过判断末尾字节的值,来确定末尾有多少个字节是Padding的,从而进行剔除。

总结

这里只讲解了Cipher的一些基础用法,对于更加深入的用法以后会慢慢说明,也可以去查找其它资料。

虽然这里仅仅以AES算法作为了例子,但是对于其它算法也是相同的操作流程,无非是使用的Transformation不一样罢了。甚至非对称加密算法也是一样的,只需要记住,加密用公钥,解密就需要用私钥,反之亦然,即可。

本文没有详细的讲解Cipher Mode,其实这一部分的内容还是比较重要的,想要真正的把Cipher应用在实际的系统中仅仅依靠ECB是不行的,因为ECB的安全级别实在是太低了,相对来说CBC比较好些,理解简单,而且也可以解决ECB中存在的安全隐患。

除了PKCS5Padding,Java的默认实现中还提供了一些其它的Padding方法:方法列表请参见 这里

Thanks for reading.

参考资料

  • 《Java Crypotography》—— Jonathan B. Knudsen
  • 《Java Security 2nd Edition》
  • 《Java 加密与解密的艺术》—— 梁栋