密码学编程》读书笔记(1)"/>
《Python 密码学编程》读书笔记(1)
《Python 密码学编程 James/Christopher》读书笔记(1)
- 书籍代码地址
- cryptography文档
- 第 1 章 密码学:不仅仅是保密
- 1.1 设置 python 环境
- 1.2 凯撒的移位密码
- 第 2 章 哈希
- 2.1 使用hashlib 自由哈希
- 2.2 进行一次哈希教育
- 2.4 哈希密码
- scrypt算法
- 2.6 工作量证明
- 第 3 章 对称加密:两端使用同一个密钥
- 3.3 AES:对称块密码
- DES和3DES
- bytes的hex和fromhex函数
- 3.5 想要的,自发的独立
- 3.5.1 不是区块链
- 分组密码的填充
- 3.5.2 流密码
- 3.6 密钥和IV管理
- 3.8 弱密码,糟糕的管理
这本书外文翻译的非常拗口,难懂,我只能跳跃着看,建议想学习的找找国内的课程书籍
书籍代码地址
cryptography文档
第 1 章 密码学:不仅仅是保密
1.1 设置 python 环境
搭建好虚拟环境,并在环境中安装下面的包
pip install cryptography
还需要安装gmpy2
模块,少数几个案例会用到
apt install libmpfr-dev libmpc-dev libgmp-dev python3-gmpy2
pip install gmpy2
1.2 凯撒的移位密码
import stringdef creat_hift_substitutions(n):encoding={}decoding={}alphabet_size = len(string.ascii_uppercase)for i in range(alphabet_size):letter = string.ascii_uppercase[i]subst_letter = string.ascii_uppercase[(i+n)%alphabet_size]encoding[letter] = subst_letterdecoding[subst_letter] = letterreturn encoding,decodingdef encode(message,subst):return "".join(subst.get(x,x) for x in message )def printable_substitution(subst):mapping = sorted(subst.items())alphet_line = "".join(letter for letter,_ in mapping )cipher_line = "".join(subst_letter for _,subst_letter in mapping)return f"{alphet_line}\n{cipher_line}"if __name__=="__main__":n=1encoding,decoding = creat_hift_substitutions(n)while True:print("\n Shift Encoder Decoder")print(f"\tCurrent Shift:{n}")print("\t1,print encoding/decoding tables.")print("\t2,encode message.")print("\t3,decode message.")print("\t4,change shift.")print("\t5,quit.\n")choice = input(">>")print()if choice =="1":print("encoding table:")print(printable_substitution(encoding))print("decoding table:")print(printable_substitution(decoding))elif choice =="2":message = input("\nMessage to encode: ")print(f"encoded message: {encode(message.upper(),encoding)}")elif choice=="3":message = input("\nMessage to decode: ")print(f"decoded message: {encode(message.upper(),decoding)}")elif choice =="4":new_shift = input(f"\nNew shift (currently {n})")try:new_shift = int(new_shift)if new_shift<1:raise Exception("Shift must be greater than 0")else:n = new_shiftencoding,decoding = creat_hift_substitutions(n)except ValueError:print(f"shift {n} is not a valid number. ")elif choice =="5":print("terminating. this program will self destruct in 5 seconds.\n")breakelse:print(f"unknown options {choice}")
第 2 章 哈希
2.1 使用hashlib 自由哈希
MD5 不好,已被弃用,不应该用于任何安全敏感的操作
MD5中的MD 代表消息摘要
import hashlibmd5hasher = hashlib.md5()
md5hasher.hexdigest()
hashlib.md5(b"hello").hexdigest()
对于几乎所有的密码使用场合,都必须使用字节
下面的操作输出与上一条一致
md5hasher2 = hashlib.md5()
md5hasher2.update(b'h')
md5hasher2.update(b'e')
md5hasher2.update(b'l')
md5hasher2.update(b'l')
md5hasher2.update(b'o')
md5hasher2.hexdigest()
2.2 进行一次哈希教育
哈希函数基本上是视图将大量(甚至无限)的东西映射到一个较小的集合上
哈希函数的属性
- 原像抗性
- 第二原像抗性
- 抗碰撞性
– 雪崩属性:无论输入变化多么小,都会在输出中造成巨大的,不可预测的更改。
也不要使用SHA-1
抗碰撞性也被破坏了,建议使用SHA-256
hashlib.sha256(b'alice').hexdigest()
2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90
MD5
输出16字节(128位)
SHA-1
输出20字节(160位)
SHA-256
输出32字节(256位)
2.4 哈希密码
Web网站上创建一个账户时,他们几乎不存储密码,通常存储密码的哈希。
输入密码时,服务器验证H(x1)=H(x2)
,哈希后的值是否相同
slat
的概念,salt
是一个公开的值,在哈希之前与用户的密码混合在一起,通过混合一个salt
值,用户的密码就不会被立即识别出来。
salt
需要是唯一的并且足够长。一种方法是使用os.urandom
和base64.b64encode
生成随机salt
使用scrypt
生成密码
import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.backends import default_backendsalt = os.urandom(16)
kdf = Scrypt(salt=salt,length=32,n=2**14,r=8,p=1,backend=default_backend())
key = kdf.derive(b"20196735")
os.urandom
函数用来获取一个指定长度的随机bytes对象,python的这个函数实际上是在读取OS操作系统提供的随机源,在Linux系统中,就是读/dev/urandom
这个设备来获得随机bytes
使用scrypt
验证密码
如果正确,没有输出。错误会输出错误信息
kdf 只能使用一次,所以每次使用必须初始化
kdf = Scrypt(salt=salt,length=32,n=2**14,r=8,p=1,backend=default_backend())## kdf 初始化
kdf.verify(b"20196735",key)
cryptography
可以使用OpenSSL
作为backend
,这样速度更快而且安全,但是本书始终使用default_backend()
,
scrypt算法
scrypt算法介绍
scrypt是一种密码衍生算法,它是由Colin Percival创建的。使用scrypt算法来生成衍生key,需要用到大量的内存。scrypt算法在2016年作为RFC 7914标准发布。
密码衍生算法主要作用就是根据初始化的主密码来生成系列的衍生密码。这种算法主要是为了抵御暴力破解的攻击。通过增加密码生成的复杂度,同时也增加了暴力破解的难度。
参数
salt: bytes,
length: int, 输出密钥的长度,上述示例中被处理为32字节的输出
n: int, CPU/memory cost 参数,必须是2的指数,对于交互式登录,建议使用2^14,随意更敏感的文件,最好是2^20
r: int, blocksize 参数
p: int, 并行参数
backend=None
2.6 工作量证明
广泛使用哈希的另一个领域是区块链技术中所谓的“工作量证明”
矿工找到一个特殊的SHA-256
哈希值,小于某个阈值,降低这个阈值会减少有效哈希值的数量,需要更多工作来找到合适的值,这就是比特币调整难度的方式。
第 3 章 对称加密:两端使用同一个密钥
AES
,高级加密标准(英语:Advanced Encryption Standard,缩写:AES),又称Rijndael
加密法
AES五种加密模式(CBC、ECB、CTR、OCF、CFB)
1.电码本模式(Electronic Codebook Book (ECB)
这种模式是将整个明文分成若干段相同的小段,然后对每一小段进行加密。
2.密码分组链接模式(Cipher Block Chaining (CBC))
这种模式是先将明文切分成若干小段,然后每一小段与初始块或者上一段的密文段进行异或运算后,再与密钥进行加密。
3.计算器模式(Counter (CTR))
计算器模式不常见,在CTR模式中, 有一个自增的算子,这个算子用密钥加密之后的输出和明文异或的结果得到密文,相当于一次一密。这种加密方式简单快速,安全可靠,而且可以并行加密,但是在计算器不能维持很长的情况下,密钥只能使用一次。
4.密码反馈模式(Cipher FeedBack (CFB))
这种模式较复杂。
5.输出反馈模式(Output FeedBack (OFB))
这种模式较复杂。
ECB模式很糟糕,不建议使用
import os
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backendkey = os.urandom(16)
aesCipher = Cipher(algorithms.AES(key),modes.ECB(),backend=default_backend())
aesEncryptor = aesCipher.encryptor()
aesDecryptor = aesCipher.decryptor()
加密和解密的update函数总是一次处理16个字节,小于16个字节的信息调用不会产生结果,直到累积到16个字节。
3.3 AES:对称块密码
对称加密算法通常分为块密码和流密码。块密码处理的是数据块,必须提供一定量的数据,较大的数据被分解成块(每个块必须是满的)。流密码可以一次加密一个字节的数据
AES
本质是一个块密码,但是用起来可以像流密码那样
DES和3DES
DES全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法,1977年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级政府通信中使用,随后该算法在国际上广泛流传开来。需要注意的是,在某些文献中,作为算法的DES称为数据加密算法(Data Encryption Algorithm,DSA),已与作为标准的DES区分开来
DES以64位分组长度对数据加密,其中包括了8位奇偶校验位,所以实际的密文长度为56位
3DES是针对DES算法密钥过短、存在安全性的问题而改进的一个措施,被称为“3DES”。其实只是通过简单的执行3次DES来达到增加密钥长度和安全而已。
bytes的hex和fromhex函数
>>> bytes([0,1,2,3,4,5]).hex()
'000102030405'
>>> bytes.fromhex('000102030405')
b'\x00\x01\x02\x03\x04\x05'
>>> b'abcde'.hex()
'6162636465'
>>> a = bytes.fromhex('6162636465')
>>> a
b'abcde'
3.5 想要的,自发的独立
3.5.1 不是区块链
良好的哈希算法因该具有雪崩特性(之前提到),在ECB 模式中。雪崩的影响只限于块大小,如果明文的长度是10个块,那么第一个位的改变只会改变第一个块的输出位,其余9个块将保持不变
一个块的密文的改变会影响后续所有块,怎么做到?
事实上很容易,在加密时,可将一个块的加密输出与下一个块的未加密输入进行XOR
操作(相同为假,相异为真),这称为密码快链(CBC)模式
python 中使用
^
代表XOR
的操作符
CBC模式的配置:生成一个密钥
,然后采用额外步骤生成一个初始化向量(IV)
,AES-CBC IV
总是128
位长,密钥
可以是很多位。
第一个明文块在AES
之前与IV
执行XOR
操作,而在解密中,密文首先经过AES
,然后与IV
进行XOR
操作
分组密码的填充
参考链接
在分组密码中,当数据长度不符合分组长度时,需要按一定的方式,将尾部明文分组进行填充,这种将尾部分组数据填满的方法称为填充(Padding)。
No Padding
即不填充,要求明文的长度,必须是加密算法分组长度的整数倍。
... | DD DD DD DD DD DD DD DD | DD DD DD DD DD DD DD DD |
ANSI X.923
在填充字节序列中,最后一个字节填充为需要填充的字节长度,其余字节填充0。
... | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 04 |
ISO 10126
在填充字节序列中,最后一个字节填充为需要填充的字节长度,其余字节填充随机数。
... | DD DD DD DD DD DD DD DD | DD DD DD DD 81 A6 23 04 |
PKCS#5和PKCS#7
在填充字节序列中,每个字节填充为需要填充的字节长度。
... | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |
ISO/IEC 7816-4
在填充字节序列中,第一个字节填充固定值80,其余字节填充0。若只需填充一个字节,则直接填充80。
... | DD DD DD DD DD DD DD DD | DD DD DD DD 80 00 00 00 |... | DD DD DD DD DD DD DD DD | DD DD DD DD DD DD DD 80 |
Zero Padding
在填充字节序列中,每个字节填充为0。
... | DD DD DD DD DD DD DD DD | DD DD DD DD 00 00 00 00 |
代码
import os
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import paddingkey = os.urandom(32)
iv = os.urandom(16)
aesCipher = Cipher(algorithms.AES(key),modes.CBC(iv),backend=default_backend())
aesEncryptor = aesCipher.encryptor()
aesDecryptor = aesCipher.decryptor()
padder = padding.PKCS7(128).padder()
unpadder = padding.PKCS7(128).unpadder()plaintexts = [
b"SHORT",
b"MEDIUM MEDIUM MEDIUM",
b"LONG LONG LONG LONG LONG LONG"]ciphertexts = []
for m in plaintexts:padded_message = padder.update(m)ciphertexts.append(aesEncryptor.update(padded_message))ciphertexts.append(aesEncryptor.update(padder.finalize()))for c in ciphertexts:padded_message = aesDecryptor.update(c)print("recovered",unpadder.update(padded_message))print("recovered",unpadder.finalize())
输出
recovered b''
recovered b''
recovered b'SHORTMEDIUM MEDIUM MEDIUMLONG LO'
recovered b'NG LONG LONG LON'
recovered b'G LONG'
输出为什么不是输入的plaintexts
中的内容?
攒够16个字节才才被加密,
更改后的代码
import os
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import paddingclass EncryptionManger:def __init__(self):self.key = os.urandom(32)self.iv = os.urandom(16)def encrypt_message(self,message):encryptor = Cipher(algorithms.AES(self.key),modes.CBC(self.iv),backend=default_backend()).encryptor()padder = padding.PKCS7(128).padder()padded_message = padder.update(message)padded_message+=padder.finalize()ciphertext = encryptor.update(padded_message)ciphertext+=encryptor.finalize()return ciphertextdef decrypt_message(self,ciphertext):decryptor = Cipher(algorithms.AES(self.key),modes.CBC(self.iv),backend=default_backend()).decryptor()unpadder = padding.PKCS7(128).unpadder()padded_message = decryptor.update(ciphertext)padded_message+=decryptor.finalize()message = unpadder.update(padded_message)message+=unpadder.finalize()return messagemanger = EncryptionManger()
plaintexts = [b"SHORT",b"MEDIUM MEDIUM MEDIUM",b"LONG LONG LONG LONG LONG LONG"]
ciphertexts = []
for m in plaintexts:ciphertexts.append(manger.encrypt_message(m))for c in ciphertexts:print("recovered",manger.decrypt_message(c))
输出
recovered b'SHORT'
recovered b'MEDIUM MEDIUM MEDIUM'
recovered b'LONG LONG LONG LONG LONG LONG'
上述代码的问题:对不同的消息重用相同的密钥
和IV
import os
from cryptography.hazmat.primitives.ciphers import Cipher,algorithms,modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import paddingclass EncryptionManger:def __init__(self):self.key = os.urandom(32)self.iv = os.urandom(16)aesContext = Cipher(algorithms.AES(key),modes.CBC(iv),backend=default_backend())self.encryptor = aesContext.encryptor()self.decryptor = aesContext.decryptor()self.padder = padding.PKCS7(128).padder()self.unpadder = padding.PKCS7(128).unpadder()def update_encryptor(self,plaintext):return self.encryptor.update(self.padder.update(plaintext))def finalize_encryptor(self):return self.encryptor.update(self.padder.finalize())+self.encryptor.finalize()def update_decryptor(self,ciphertext):return self.unpadder.update(self.decryptor.update(ciphertext))def finalize_decryptor(self):return self.unpadder.update(self.decryptor.finalize())+self.unpadder.finalize()manager = EncryptionManger()
plaintexts = [b"SHORT",b"MEDIUM MEDIUM MEDIUM",b"LONG LONG LONG LONG LONG LONG"]
ciphertexts = []
for m in plaintexts:ciphertexts.append(manager.update_encryptor(m))
ciphertexts.append(manager.finalize_encryptor())for c in ciphertexts:print("Recovered",manager.update_decryptor(c))
print("Recovered",manager.finalize_decryptor())
3.5.2 流密码
计算器模式(Counter (CTR))
计算器模式不常见,在CTR模式中, 有一个自增的算子,这个算子用密钥加密之后的输出和明文异或的结果得到密文,相当于一次一密。这种加密方式简单快速,安全可靠,而且可以并行加密,但是在计算器不能维持很长的情况下,密钥只能使用一次。
流密码不需要填充
代码:。
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import osclass EncryptionManager:def __init__(self):key = os.urandom(32)nonce = os.urandom(16)aes_context = Cipher(algorithms.AES(key),modes.CTR(nonce),backend=default_backend())self.encryptor = aes_context.encryptor()self.decryptor = aes_context.decryptor()def updateEncryptor(self, plaintext):return self.encryptor.update(plaintext)def finalizeEncryptor(self):return self.encryptor.finalize()def updateDecryptor(self, ciphertext):return self.decryptor.update(ciphertext)def finalizeDecryptor(self):return self.decryptor.finalize()# Auto generate key/IV for encryption
manager = EncryptionManager()plaintexts = [b"SHORT",b"MEDIUM MEDIUM MEDIUM",b"LONG LONG LONG LONG LONG LONG"
]ciphertexts = []for m in plaintexts:ciphertexts.append(manager.updateEncryptor(m))
ciphertexts.append(manager.finalizeEncryptor())for c in ciphertexts:print("Recovered", manager.updateDecryptor(c))
print("Recovered", manager.finalizeDecryptor())
因为不需要填充,finalize()
方法实际上是不需要的。
如何选择CTR 和CBC模式?
在几乎所有的情况下,建议使用CTR模式,不仅更容易,而且某些情况下更安全。不仅如此,计数器模式也更容易并行化,因为密钥流中的密钥是更具索引计算的。
3.6 密钥和IV管理
永远不要重复使用密钥和IV对,加密任何内容时,总是使用新的密钥/IV对
永远不要重复使用密钥和IV对,加密任何内容时,总是使用新的密钥/IV对
永远不要重复使用密钥和IV对,加密任何内容时,总是使用新的密钥/IV对
3.8 弱密码,糟糕的管理
密码必须有良好的随机来源,下面的代码时错误的
import random
key=random.getrandbits(16,"big")#返回指定大小的整数(以位为单位)
random
是一个伪随机数生成包,不是一个好的生成器。生成的数字在人看来时随机的,但是在给定随机种子的情况下总是相同的。默认种子时基于系统时间的。
从os.urandom()
或者secrets.SystemRandom()
足够安全
从密码中派生密钥也是安全的,前提是密码必须足够安全,第二章中使用了scrypt
,还有更好的方法如 bcrypt
和Argon2
更多推荐
《Python 密码学编程》读书笔记(1)
发布评论