AES算法预研

835 阅读15分钟

demo:github.com/thy48528086…

AES全称Advanced Encryption Standard,是DES算法的替代者,也是当今最流行的对称加密算法之一。

对比:

  • 与摘要加密算法(MD5、SHA)对比

    • AES算法是属于真正意义上的解密算法,不同于MD5、SHA这些不可逆的摘要加密算法。

      区别:

      • 摘要加密算法是不可逆的,主要作用是对信息一致性和完整性的校验。
      • 而对称加密算法是可逆的,主要作用是保证私密信息不被泄露。
  • 与非对称加密算法(RSA)对比:

    • 对称加密算法

      • 特性:加密和解密采用相同密钥 优点:算法公开、计算量小、加密速度快、加密效率高 缺点:密钥的管理(密文的传输无论是速率还是安全性都不是问题,但攻击者一旦获得密钥便可由对称加密算法的优点:算法公开、计算量小解密获得明文,导致每次传输必须使用其他人无法获取唯一密钥,这会使得发收信双方所拥有的密钥数量呈几何级数增长,密钥的数量和密钥的传输是密钥的管理的两大痛点)
    • 非对称

      • 特性:公钥和密钥的加密解密的映射性 优点:安全性高、不可抵赖性、简洁性(体现在摘要上,这里略去) 缺点:加密解密的速度慢、密钥对的获取难度、非绝对安全(公钥的可冒充性:验证签名的公钥必须是真正属于发送者,第三方的冒充公钥会使得通信产生难以预估的安全威胁)

注意

  • AES加解密必须使用的是同一种填充方式和工作模式
  • AES密钥长度越长,安全性越高,性能越低
  • 我们在调用封装好的AES算法时,表面上使用的Key并不是真正用于AES加密解密的密钥,而是用于生成真正密钥的“种子”。
  • 填充明文时,如果明文长度原本就是16字节的整数倍,那么除了NoPadding以外,其他的填充方式都会填充一组额外的16字节明文块。

基本概念

密钥

密钥是AES算法实现加密和解密的根本。对称加密算法之所以对称,是因为这类算法对明文的加密和解密需要使用同一个密钥(而非对称加密比如RSA,加解密使用的是两个不同的密钥)。

AES支持三种长度的密钥:128位,192位,256位

平时所说的AES128,AES192,AES256,实际上就是指的AES算法对不同长度密钥的使用。

AES方案密钥长度(32位比特字)分组长度(32位比特字)加密轮数
AES-1284410
AES-1926412
AES-2568414

区别:密钥越长,需要加密处理的轮数越多,安全性越高,性能越低。

填充

要想了解填充的概念,我们先要了解AES的分组加密特性。

什么是分组加密呢?我们来看看下面这张图:

1.png

AES算法在对明文加密的时候,并不是把整个明文一股脑加密成一整段密文,而是把明文拆分成一个个独立的明文块,每一个明文块长度128bit。 这些明文块经过AES加密器的复杂处理,生成一个个独立的密文块,这些密文块拼接在一起,就是最终的AES加密结果。

但是这里涉及到一个问题: 假如一段明文长度是192bit,如果按每128bit一个明文块来拆分的话,第二个明文块只有64bit,不足128bit。这时候怎么办呢?就需要对明文块进行填充(Padding)。

典型的填充方式:

  • NoPadding:不做任何填充,但是要求明文必须是16字节的整数倍。

  • PKCS5Padding(默认):如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字符,且每个字节的值等于缺少的字符数。

    比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则补全为{1,2,3,4,5,a,b,c,d,e,6,6,6,6,6,6}

  • PKCS7Padding:缺n个,则填充n个二进制数n。对于AES来说PKCS5Padding和PKCS7Padding是完全一样的,不同在于PKCS5限定了块大小为8bytes而PKCS7没有限定。因此对于AES来说两者完全相同,但是对于Rijndael就不一样了。AES是Rijndael在块大小为8bytes时的特例,对于使用其他信息块大小的Rijndael算法只能使用PKCS7。(在AES加密当中严格来说是不能使用pkcs5的,因为AES的块大小是16bytes而pkcs5只能用于8bytes,通常我们在AES加密中所说的pkcs5指的就是pkcs7){1,2,3,4,5,a,b,c,d,e,3,3,3}

  • ISO10126Padding:如果明文块少于16个字节(128bit),在明文块末尾补足相应数量的字节,最后一个字符值等于缺少的字符数,其他字符填充随机数。

    比如明文:{1,2,3,4,5,a,b,c,d,e},缺少6个字节,则可能补全为{1,2,3,4,5,a,b,c,d,e,5,c,3,G,$,6}

  • ZerosPadding:全部填充0x00,无论缺多少全部填充0x00,已经是128bits倍数仍要填充。

注意:AES加密的时候使用了某一种填充方式,解密的时候必须也采用同样的填充方式。

模式

注意:OCB是迄今为止最好的模式,因为它允许在一次通过中进行加密和验证。但是在美国有专利,所以不在此介绍。

AES有多种工作模式,主要有以下模式:

ECB

  • 电码本模式(Electronic Codebook Book (ECB))

  • ECB是最简单的工作模式,在该模式下,每一个明文块的加密都是完全独立,互不干涉的。

    img

  • 优缺点

    • 好处:简单、有利于并行计算
    • 缺点:相同的明文块经过加密会变成相同的密文块,因此安全性较差。

CBC

  • 密码分组链接模式(Cipher Block Chaining (CBC))

  • CBC模式(Cipher Block Chaining)引入了一个新的概念:初始向量IV(Initialization Vector)。

    它的作用和MD5的“加盐”有些类似,目的是防止同样的明文块始终加密成同样的密文块。

  • 流程图:

    img

    从图中可以看出,CBC模式在每一个明文块加密前会让明文块和一个值先做异或操作。IV作为初始化变量,参与第一个明文块的异或,后续的每一个明文块和它前一个明文块所加密出的密文块相异或。

    这种模式是先将明文切分成若干小段,然后每一小段与初始块IV或者上一段的密文段进行异或运算后,再与密钥进行加密。这样以来,相同的明文块加密出的密文块显然是不一样的。

  • 优缺点:

    • 好处:安全性更高
    • 坏处:无法并行计算,性能上不如ECB;引入初始化向量IV,增加复杂度。

CFB

  • 密码反馈模式(Cipher FeedBack (CFB));

  • 这种模式较复杂。

    8.jpg

  • 优缺点

    • 优点:

      1.隐藏了明文模式;

      2.分组密码转化为流模式;

      3.可以及时加密传送小于分组的数据;

    • 缺点:

      1.不利于并行计算;

      2.误差传送:一个明文单元损坏影响多个单元;

      3.唯一的IV;

OFB

  • 输出反馈模式(Output FeedBack (OFB))

  • 这种模式较复杂。

    7.jpg

  • 优缺点:

    • 优点:

      1.隐藏了明文模式;

      2.分组密码转化为流模式;

      3.可以及时加密传送小于分组的数据;

    • 缺点:

      1.不利于并行计算;

      2.对明文的主动攻击是可能的;

      3.误差传送:一个明文单元损坏影响多个单元;

CTR

  • 计算器模式(Counter (CTR))

  • 计算器模式不常见,在CTR模式中, 有一个自增的算子,这个算子用密钥加密之后的输出和明文异或的结果得到密文,相当于一次一密。这种加密方式简单快速,安全可靠,而且可以并行加密,但是在计算器不能维持很长的情况下,密钥只能使用一次。CTR的示意图如下所示:

    img

  • 如果使用CTR,则必须为每条消息使用不同的IV,否则最终攻击者可以获取两个密文并获得未加密的明文组合。原因是CTR模式实质上将块密码转换为流密码,流密码的第一个规则是永远不要使用相同的Key + IV两次。

GCM

  • CTR

OBC

  • 有专利限制,不展开

注意:所有工作模式差别体现在宏观上,即明文块与明文块之间的关联。AES加密器的内部处理流程都是一样的。

AES加密的时候使用了某一种工作模式,解密的时候必须也采用同样的工作模式。

具体加密流程

AES加密大体流程图:

1.png

解释:

  1. 把明文按照128bit拆分成若干个明文块
  2. 按照所选填充方式来填充最后一个明文块(如果明文长度原本就是16字节的整数倍,那么除了NoPadding以外,其他的填充方式都会填充一组额外的16字节明文块。)
  3. 每一个明文块利用AES加密器和密钥,加密成密文块
  4. 拼接所有密文块,成为最终的密文结果

AES加密器具体详情图:

2.jpg

具体名词会在后面用到的地方作详细解释。

AES加密不是一次把明文变成密文,而是先后经过很多轮加密。具体分为:

初始轮1次
普通轮N次AES128:9轮,AES192:11轮,AES256:13轮
最终轮1次

不同阶段的Round有不同的处理步骤。

  1. 初始轮只有一个步骤:

    • 加轮密钥(AddRoundKey)
  2. 普通轮有四个步骤:

    • 字节代替(SubBytes)
    • 行移位(ShiftRows)
    • 列混淆(MixColumns)
    • 加轮密钥(AddRoundKey)
  3. 最终轮有三个步骤:

    • 字节代替(SubBytes)
    • 行移位(ShiftRows)
    • 加轮密钥(AddRoundKey)

字节代替(SubBytes)

简单叙述: 把明文块相关位置的内容用S盒相关位置的内容替代

3.jpg

前面说AES会将明文分成若干个16字节的明文块,划分后的明文块在每个步骤中都被排列成一个4X4的二维数组。

所谓字节替代,就是把明文块的每一个字节都替代成另外一个字节。替代的依据是一个被称为S盒(Subtitution Box)的16X16大小的二维常量数组。

S盒:

4.png 假设明文块当中a[2,2] = 5B(一个字节是两位16进制),那么输出值b[2,2] = S[5][B] = 39(解密的时候同样可以通过找到S盒中39所在位置,其下标为5B,即为初始值)

行移位(ShiftRows)

简单叙述: 将字节替换后的明文块的第i行循环左移i位(0 <= i <= 3)

5.png

这一步很简单,就像图中所描述的:

  • 第一行不变
  • 第二行循环左移1个字节
  • 第三行循环左移2个字节
  • 第四行循环左移3个字节

列混淆(MixColumns)

简单叙述: 将修补矩阵与行移位后的明文块相乘得到混淆后的状态矩阵。(注意:矩阵元素的乘法和加法都是定义在基于GF(2^8)上的二元运算,并不是通常意义上的乘法和加法。

6.jpg

这一步,输入数组的每一列要和一个名为修补矩阵(fixed matrix)的二维常量数组做矩阵相乘,得到对应的输出列。【举例:b[0][1] = (a[0][0] * M[0][1]) xor (a[0][1] * M[1][1]) xor (a[0][2] * M[2][1]) xor (a[0][3] * M[3][1]) ,M为修补矩阵,xor为异或操作】

修补矩阵: 10.png

计算公式: 11.png

计算方式详情:AES加密算法之列混合变换白马长枪儒雅将的博客-CSDN博客aes列混合

加轮密钥(AddRoundKey)

简单叙述: 轮密钥加操作是将128位轮密钥Ki同状态矩阵(行移位或者列混淆的结果矩阵)中的数据进行逐位异或操作。

9.jpg

这一步是唯一利用到密钥的一步,128bit的密钥也同样被排列成4X4的矩阵。

让输入数组的每一个字节a[i,j]与密钥对应位置的字节k[i,j]异或一次,就生成了输出值b[i,j]。

需要补充一点,加密的每一轮所用到的密钥并不是相同的。这里涉及到一个概念:扩展密钥(KeyExpansions)。

扩展密钥(KeyExpansions):

AES源代码中用长度 4 * 4 *(10+1) 字节的数组W来存储所有轮的密钥。W{0-15}的值等同于原始密钥的值,用于为初始轮做处理。

后续每一个元素W[i]都是由W[i-4]和W[i-1]计算而来,直到数组W的所有元素都赋值完成。

具体流程图:

12.png

AES首先将初始密钥输入到一个4*4的状态矩阵中,如上图所示。

这个4*4矩阵的每一列的4个字节组成一个字,矩阵4列的4个字依次命名为W[0]、W[1]、W[2]和W[3],它们构成一个以字为单位的数组W。例如,设密钥K为”abcdefghijklmnop”,则K0 = ‘a’,K1 = ‘b’, K2 = ‘c’,K3 = ‘d’,W[0] = “abcd”。 接着,对W数组扩充40个新列,构成总共44列的扩展密钥数组。新列以如下的递归方式产生:

  1. 如果i不是4的倍数,那么第i列由如下等式确定: W[i]=W[i-4]⨁W[i-1]

  2. 如果i是4的倍数,那么第i列由如下等式确定: W[i]=W[i-4]⨁T(W[i-1]) 其中,T是一个有点复杂的函数。 函数T由3部分组成:字循环、字节代换和轮常量异或,这3部分的作用分别如下。

    • 字循环:将1个字中的4个字节循环左移1个字节。即将输入字[b0, b1, b2, b3]变换成[b1,b2,b3,b0]。

    • 字节代换:对字循环的结果使用S盒进行字节代换。

    • 轮常量异或:将前两步的结果同轮常量Rcon[j]进行异或,其中j表示轮数。 轮常量Rcon[j]是一个字,其值见下表。

      j12345
      Rcon[j]0100000002000000040000000800000010000000
      j678910
      Rcon[j]2000000040000000800000001B00000036000000

示例:

设初始的128位密钥为:
3C A1 0B 21 57 F0 19 16 90 2E 13 80 AC C1 07 BD
那么4个初始值为:
W[0] = 3C A1 0B 21
W[1] = 57 F0 19 16
W[2] = 90 2E 13 80
W[3] = AC C1 07 BD
下面求扩展的第1轮的子密钥(W[4],W[5],W[6],W[7])。
由于4是4的倍数,所以:
W[4] = W[0] ⨁ T(W[3])
T(W[3])的计算步骤如下:
​
循环地将W[3]的元素移位:AC C1 07 BD变成C1 07 BD AC;
将 C1 07 BD AC 作为S盒的输入,输出为78 C5 7A 91;
将78 C5 7A 91与第一轮轮常量Rcon[1]进行异或运算,将得到79 C5 7A 91,因此,T(W[3])=79 C5 7A 91,故
W[4] = 3C A1 0B 21 ⨁ 79 C5 7A 91 = 45 64 71 B0
其余的3个子密钥段的计算如下:
W[5] = W[1] ⨁ W[4] = 57 F0 19 16 ⨁ 45 64 71 B0 = 12 94 68 A6
W[6] = W[2] ⨁ W[5] =90 2E 13 80 ⨁ 12 94 68 A6 = 82 BA 7B 26
W[7] = W[3] ⨁ W[6] = AC C1 07 BD ⨁ 82 BA 7B 26 = 2E 7B 7C 9B
所以,第一轮的密钥为 45 64 71 B0 12 94 68 A6 82 BA 7B 26 2E 7B 7C 9B

W数组当中,W{0-15}用于初始轮的处理,W{16-31}用于第1轮的处理,W{32-47}用于第2轮的处理 ……一直到W{160-175}用于最终轮(第10轮)的处理。

解密流程

加密流程倒置过来,顺序变为 最终轮—>普通轮—>初始轮。扩展密钥的使用顺序也和加密相反。

13.png

逆字节代换

逆字节代换就是查逆S盒来变换,逆S盒如下:

14.png

逆行移位

逆行移位即行移位的逆变换。 行移位的逆变换是将状态矩阵中的每一行执行相反的移位操作,例如AES-128中,状态矩阵的第0行右移0字节,第1行右移1字节,第2行右移2字节,第3行右移3字节。

逆列混合

逆列混合即列混合逆运算。公式:

15.png

加解密概要!

16.jpg

测试

基于pycryptodome密码库AES模块,利用timeit模块对ECB、OFB、CFB、CBC、GCM五种模式在不同密钥长度下进行性能测试。 加密数据data = 'secret datasadjasdoapjf wafj awjfwa么v哦怕v吗v到屏幕'.encode() 相关测试函数执行次数count=100000 测试结果数据:单位(秒)

  • 不含base64编码解码性能损耗:

    test_aes128_ECB 3.3816415
    test_aes192_ECB 3.3811248999999997
    test_aes256_ECB 3.135126799999999
    test_aes128_OFB 3.8089359
    test_aes192_OFB 4.018084600000002
    test_aes256_OFB 3.997190400000001
    test_aes128_CFB 4.436713000000001
    test_aes192_CFB 4.482157100000002
    test_aes256_CFB 4.7518030999999965
    test_aes128_CBC 4.1842319
    test_aes192_CBC 4.153839099999999
    test_aes256_CBC 4.0824912
    test_aes128_GCM 16.680618000000003
    test_aes192_GCM 16.57688379999999
    test_aes256_GCM 17.128724899999995
    
  • 包含base64编码解码:

    test_aes128_ECB 3.817687
    test_aes192_ECB 3.5313921
    test_aes256_ECB 3.4777353
    test_aes128_OFB 4.327306
    test_aes192_OFB 4.428335499999999
    test_aes256_OFB 4.413377799999999
    test_aes128_CFB 5.015321400000001
    test_aes192_CFB 5.014899999999997
    test_aes256_CFB 5.144274800000005
    test_aes128_CBC 4.2879145000000065
    test_aes192_CBC 4.250506799999997
    test_aes256_CBC 4.360604699999996
    test_aes128_GCM 17.045861699999996
    test_aes192_GCM 17.04790270000001
    test_aes256_GCM 17.686628900000002
    

根据结果可以发现:

  • base64有性能损耗,在ECB模式下大概有10%的性能损耗,在其他模式下只有百分之几的性能损耗,所以用不用影响不大,用了安全性可能更好一点点。
  • ECB性能最好,但不安全,遂不考虑。
  • GCM是基于CTR模式的改进,但测试结果性能太低,也不考虑。
  • 由CFB工作原理可知,它不是直接对明文加密,而是将iv加密然后将结果与明文进行异或操作得到密文
  • OFB同上,OFB与CFB的区别仅仅在于密码算法的输入不一样。
  • 综上,用CBC,并且CBC是对明文直接加密。

Python密码库pycryptodome源码AES部分解读

加密模式

AES模块提供了11种模式

MODE_ECB = 1
MODE_CBC = 2
MODE_CFB = 3
MODE_OFB = 5
MODE_CTR = 6
MODE_OPENPGP = 7
MODE_CCM = 8
MODE_EAX = 9
MODE_SIV = 10
MODE_GCM = 11
MODE_OCB = 12
# 每种模式对应一种创建函数
_modes = { 1:_create_ecb_cipher,
           2:_create_cbc_cipher,
           3:_create_cfb_cipher,
           5:_create_ofb_cipher,
           6:_create_ctr_cipher,
           7:_create_openpgp_cipher,
           9:_create_eax_cipher
           }
​
_extra_modes = { 8:_create_ccm_cipher,
                10:_create_siv_cipher,
                11:_create_gcm_cipher,
                12:_create_ocb_cipher
                }

填充方法

加密时的填充方法pad源码:

def pad(data_to_pad, block_size, style='pkcs7'):
    """
    data_to_pad:bytes类型的明文数据
    block_size:将明文数据划分后的明文块的长度,传AES.block_size就行了,或者16
    style:填充方式,一般使用默认的即可。他这里只支持pkcs7、iso7816、x923三种填充方式。pkcs7上面有解释,其他两种未了解。
    """
    padding_len = block_size-len(data_to_pad)%block_size  # 计算需要的填充长度
    if style == 'pkcs7':
        padding = bchr(padding_len)*padding_len  # 计算填充部分
    elif style == 'x923':
        padding = bchr(0)*(padding_len-1) + bchr(padding_len)
    elif style == 'iso7816':
        padding = bchr(128) + bchr(0)*(padding_len-1)
    else:
        raise ValueError("Unknown padding style")
    return data_to_pad + padding  # 返回填充后内容# 以pkcs7为例
padding = bchr(padding_len)*padding_len
bchr将传入的数字n转为bytes类型,然后填充n个

解密所需的方法unpad,用于移除填充的内容:

def unpad(padded_data, block_size, style='pkcs7'):
    pdata_len = len(padded_data)
    if pdata_len == 0:
        raise ValueError("Zero-length input cannot be unpadded")
    if pdata_len % block_size:
        raise ValueError("Input data is not padded")
    if style in ('pkcs7', 'x923'):
        padding_len = bord(padded_data[-1])
        if padding_len<1 or padding_len>min(block_size, pdata_len):
            raise ValueError("Padding is incorrect.")
        if style == 'pkcs7':
            if padded_data[-padding_len:]!=bchr(padding_len)*padding_len:
                raise ValueError("PKCS#7 padding is incorrect.")
        else:
            if padded_data[-padding_len:-1]!=bchr(0)*(padding_len-1):
                raise ValueError("ANSI X.923 padding is incorrect.")
    elif style == 'iso7816':
        padding_len = pdata_len - padded_data.rfind(bchr(128))
        if padding_len<1 or padding_len>min(block_size, pdata_len):
            raise ValueError("Padding is incorrect.")
        if padding_len>1 and padded_data[1-padding_len:]!=bchr(0)*(padding_len-1):
            raise ValueError("ISO 7816-4 padding is incorrect.")
    else:
        raise ValueError("Unknown padding style")
    return padded_data[:-padding_len]

生成密钥

AES模块提供了生成密钥的函数方法:from Crypto.Random import get_random_bytes,需要传入需要生成的密钥长度。密钥长度只支持16、24、32,单位:字节。同时如果该模式需要IV但没传入,也会通过这个函数生成一个长度为block_size字节的IV。

将明文按照16字节划分成明文块;密钥长度支持16、24、32字节

# Size of a data block (in bytes)
block_size = 16
# Size of a key (in bytes)
key_size = (16, 24, 32)  # 长度不在这里面会抛出异常

创建对象

通过new函数创建一个AES对象,需要传入密钥和模式

def new(key, mode, *args, **kwargs):
    kwargs["add_aes_modes"] = True
    return _create_cipher(sys.modules[__name__], key, mode, *args, **kwargs)

def _create_cipher(factory, key, mode, *args, **kwargs):
    kwargs["key"] = key
    modes = dict(_modes)
    if kwargs.pop("add_aes_modes", False):  # 由上面的new函数可知恒为True
        modes.update(_extra_modes)  # 即把8-12四种模式也添加进来
    if not mode in modes:
        raise ValueError("Mode not supported")

    if args:
        if mode in (8, 9, 10, 11, 12):
            if len(args) > 1:
                raise TypeError("Too many arguments for this mode")
            kwargs["nonce"] = args[0]
        elif mode in (2, 3, 5, 7):
            if len(args) > 1:
                raise TypeError("Too many arguments for this mode")
            kwargs["IV"] = args[0]
        elif mode == 6:
            if len(args) > 0:
                raise TypeError("Too many arguments for this mode")
        elif mode == 1:
            # 只有ECB模式不需要额外变量
            raise TypeError("IV is not meaningful for the ECB mode")

    return modes[mode](factory, **kwargs)

# 以CBC为例,会返回一个CbcMode对象,其提供了加密和解密方法
def _create_cbc_cipher(factory, **kwargs):
    """Instantiate a cipher object that performs CBC encryption/decryption.

    :Parameters:
      factory : module
        The underlying block cipher, a module from ``Crypto.Cipher``.

    :Keywords:
      iv : bytes/bytearray/memoryview
        The IV to use for CBC.

      IV : bytes/bytearray/memoryview
        Alias for ``iv``.

    Any other keyword will be passed to the underlying block cipher.
    See the relevant documentation for details (at least ``key`` will need
    to be present).
    """

    cipher_state = factory._create_base_cipher(kwargs)
    iv = kwargs.pop("IV", None)
    IV = kwargs.pop("iv", None)

    if (None, None) == (iv, IV):
        iv = get_random_bytes(factory.block_size)
    if iv is not None:
        if IV is not None:
            raise TypeError("You must either use 'iv' or 'IV', not both")
    else:
        iv = IV

    if len(iv) != factory.block_size:
        raise ValueError("Incorrect IV length (it must be %d bytes long)" %
                factory.block_size)

    if kwargs:
        raise TypeError("Unknown parameters for CBC: %s" % str(kwargs))

    return CbcMode(cipher_state, iv)

class CbcMode(object):
    def __init__(self, block_cipher, iv):
        self._state = VoidPointer()
        result = raw_cbc_lib.CBC_start_operation(block_cipher.get(),
                                                 c_uint8_ptr(iv),
                                                 c_size_t(len(iv)),
                                                 self._state.address_of())
        if result:
            raise ValueError("Error %d while instantiating the CBC mode"
                             % result)
        self._state = SmartPointer(self._state.get(),
                                   raw_cbc_lib.CBC_stop_operation)
        block_cipher.release()
        self.block_size = len(iv)
        self.iv = _copy_bytes(None, None, iv)
        self.IV = self.iv
        self._next = [ self.encrypt, self.decrypt ]

    def encrypt(self, plaintext, output=None):
        if self.encrypt not in self._next:
            raise TypeError("encrypt() cannot be called after decrypt()")
        self._next = [ self.encrypt ]
        
        if output is None:
            ciphertext = create_string_buffer(len(plaintext))
        else:
            ciphertext = output
            
            if not is_writeable_buffer(output):
                raise TypeError("output must be a bytearray or a writeable memoryview")
        
            if len(plaintext) != len(output):
                raise ValueError("output must have the same length as the input"
                                 "  (%d bytes)" % len(plaintext))

        result = raw_cbc_lib.CBC_encrypt(self._state.get(),
                                         c_uint8_ptr(plaintext),
                                         c_uint8_ptr(ciphertext),
                                         c_size_t(len(plaintext)))
        if result:
            if result == 3:
                raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
            raise ValueError("Error %d while encrypting in CBC mode" % result)

        if output is None:
            return get_raw_buffer(ciphertext)
        else:
            return None

    def decrypt(self, ciphertext, output=None):
        if self.decrypt not in self._next:
            raise TypeError("decrypt() cannot be called after encrypt()")
        self._next = [ self.decrypt ]
        
        if output is None:
            plaintext = create_string_buffer(len(ciphertext))
        else:
            plaintext = output

            if not is_writeable_buffer(output):
                raise TypeError("output must be a bytearray or a writeable memoryview")
            
            if len(ciphertext) != len(output):
                raise ValueError("output must have the same length as the input"
                                 "  (%d bytes)" % len(plaintext))

        result = raw_cbc_lib.CBC_decrypt(self._state.get(),
                                         c_uint8_ptr(ciphertext),
                                         c_uint8_ptr(plaintext),
                                         c_size_t(len(ciphertext)))
        if result:
            if result == 3:
                raise ValueError("Data must be padded to %d byte boundary in CBC mode" % self.block_size)
            raise ValueError("Error %d while decrypting in CBC mode" % result)

        if output is None:
            return get_raw_buffer(plaintext)
        else:
            return None