two women facing security camera above mounted on structure

安全和密码学算法:指南

这些算法已经过密码学家的广泛研究,开源实现也受益于严格的审查……
首页 » 博客 » 安全和密码学算法:指南

这是系列中的第二篇文章 “Python Web应用程序开发人员必须了解的关于安全性的绝对最低限度。”

应用程序安全性取决于密码学,用于加密、身份验证以及更多方面。密码学由几个核心算法组成,这些算法可以组合起来创建协议和安全系统。密码是一种加密算法。

特别感谢 Crypto 101 的作者对这些算法的详细解释。

由于这些算法本身已经过密码分析师的深入研究,并且它们的开源实现有许多人试图破解它们并使其安全(“众人拾柴火焰高”),因此我们可以确信,如果正确实施,加密是有效且安全的。

常见的密码学算法类型包括

  • 密钥交换协议
  • 哈希算法
  • 分组密码和流密码(对称密钥加密)
  • 公钥加密(非对称)
  • 签名算法
  • 伪随机数生成器

密码学库还提供了许多其他算法和工具,用于密钥管理、身份验证、双因素身份验证等。

大多数实用的加密系统都是密钥加密和公钥加密的混合体。协议或完整的安全系统(如TLS)将多种算法结合使用。

一些密码学算法,包括用于安全Web流量的公钥基础设施,理论上可能被量子计算系统破解(所谓的 量子末日)。 Shor的理论算法要破解RSA需要大约 2000万量子比特(尽管 量子图灵机以需要更长的相干时间为代价,大大减少了这个数字)。目前,最大的量子计算机拥有大约1100个量子比特。我们已经有一些 抗量子算法

a fallen tree laying on top of a river

分组密码和流密码

分组密码

分组密码是一系列算法,用于使用共享密钥以固定大小的块加密数据。分组密码是对称的,发送方和接收方都使用相同的密钥来加密和解密数据。分组密码用于保护存储或通过网络传输的数据。 

以下是分组密码的一些关键特征

  • 固定大小的块:分组密码以固定大小的块处理数据,例如64位或128位。 
  • 对称密钥:分组密码使用单个密钥进行加密和解密。 
  • 转换轮次:分组密码使用一系列转换轮次将密钥应用于输入数据。 
  • 操作模式:分组密码使用操作模式来确保相同的文本块不会以相同的方式加密。 
  • 常用用途:分组密码通常用于加密大量数据。它们经常被发现是较大密码协议的组成部分

最常见的分组密码是AES(AES取代了DES,DES由于56位密钥长度太短而过时,大约一天即可暴力破解)。AES以前称为Rijndael,目前没有已知的攻击方法。3DES是另一种现代分组密码算法。

加密多个块时使用不同的操作模式。EBC是一种朴素模式,它暴露了底层数据的结构。CBC是一种更安全的模式。

使用分组密码,我们只能发送特定长度的消息:密码的块长度或其倍数。对于长度不是块大小倍数的消息,有各种填充策略(这些策略可能存在自身的漏洞)。PKCS5和7是常见的填充策略。对于发送大小不确定的流,我们可以使用流密码。

密钥加密要求两端共享密钥。通过不安全的通道(如网络)共享密钥是通过密钥交换协议完成的。

使用CBC模式下的AES-128加密数据的示例。请注意,IV(初始化向量)是使用 os.urandom 的随机数据源,我们必须使用PCKCS7填充来填充明文,以匹配块大小

import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
BLOCK_SIZE_BYTES = algorithms.AES.block_size // 8
plaintext = b"This is a secret message."
key = b"mysupersecretkey"
iv = os.urandom(BLOCK_SIZE_BYTES )
# Create the AES cipher object in CBC mode
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor = cipher.encryptor()
# Pad the data to block size and encrypt
padder = PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(plaintext) + padder.finalize()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
# Receiver needs ciphertext, iv and key to decrypt
ciphertext = iv + ciphertext
print(f"Ciphertext: {ciphertext.hex()}")

以及解密加密数据

iv = ciphertext[:BLOCK_SIZE_BYTES]
ciphertext = ciphertext[BLOCK_SIZE_BYTES:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
decryptor = cipher.decryptor()padded_data = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = PKCS7(algorithms.AES.block_size).unpadder()
plaintext = unpadder.update(padded_data) + unpadder.finalize()
print(f"Decrypted message: {plaintext}")

您通常不会直接使用 cryptography.hazmat 库中的这些低级组件,而是使用更高级别的接口。文档中有此警告

危险: 这是一个“危险材料”模块。如果您 100% 确定自己知道自己在做什么,您才应该使用它,因为这个模块充满了地雷、恶龙和带有激光枪的恐龙。

对于实际的加密和解密,您可以使用 Fernet,它既好又直接

from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
# Encrypt
token = f.encrypt(b"my deep dark secret")
f = Fernet(key)
# Decrypt with the same key
assert f.decrypt(token) == b'my deep dark secret'

实际上,更常见的是使用基于这些组件构建的系统或更高级别的协议,而不是实现您自己的加密系统——无论如何都不鼓励这样做……

Fernet 构建于

  • CBC 模式下使用 128 位密钥的 AES 加密之上;使用 PKCS7 填充。
  • 使用 SHA256 的 HMAC 进行身份验证。
  • 初始化向量使用 os.urandom() 生成

这是一个使用 Fernet 和密码的示例,使用“密钥派生函数”将密码转换为密码学安全的密钥。为了将来从密码派生密钥,必须存储盐值;每个密码使用不同的盐值。我们稍后将讨论密码和盐值。

import base64
import os
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC

password = b"password"
salt = os.urandom(16)
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=32,
    salt=salt,
    iterations=480000,
)
key = base64.urlsafe_b64encode(kdf.derive(password))
# Encrypt the data
f = Fernet(key)
token = f.encrypt(b"Secret message!")
# Now decrypt (using the same key derived from the salt and password)
f = Fernet(key)
f.decrypt(token)
b'Secret message!'

随机数据(此处使用 os.urandom)在密码学中非常重要。我们稍后将研究随机数生成器。

AES是您可以与SQLAlchemy StringEncryptedType 一起使用的标准加密算法之一,用于数据库中“静态”数据加密。以下是在列定义中使用它的示例

import os
import sqlalchemy as sa
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy_utils import StringEncryptedType, JSONType
from sqlalchemy_utils.types.encrypted.encrypted_type import AesEngine
SECRET_KEY = os.environ["SECRET_KEY"]

class Base(DeclarativeBase):
    pass

class Example(Base):
    __tablename__ = "example"
    id = sa.Column(sa.Integer, primary_key=True)
    data = sa.Column(StringEncryptedType(JSONType(), SECRET_KEY, AesEngine, 'pkcs5'))

流密码

流密码是一种密码学算法,它以连续流的方式加密和解密数据。它是一种轻量级的对称加密技术。 

以下是流密码的一些特征

  • 随机密钥流
    流密码的安全性取决于统计随机密钥流的生成。(从用于加密和解密消息的相同共享密钥开始。)
  • 速度
    流密码通常比其他加密方法更快,例如分组密码(并且比公钥加密快得多)。
  • 串行性质
    流密码允许在信息准备就绪时发送信息,而不是等待所有信息都准备就绪

RC4是最常见的流密码,但已知存在针对RC4的攻击,并且它被认为是过时的。Salsa20和ChaCha是最先进的流密码。

以下是使用密码学库中的ChaCha20流密码加密和解密数据的示例

import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms

plaintext = b"This is a secret message."
key = os.urandom(32)  # ChaCha20 key must be 32 bytes
nonce = os.urandom(16)  # ChaCha20 nonce must be 16 bytes

# Create the ChaCha20 cipher object for encryption
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None)
encryptor = cipher.encryptor()

# Encrypt the plaintext
ciphertext = encryptor.update(plaintext)
print(f"Ciphertext: {ciphertext.hex()}")

# Create the ChaCha20 cipher object for decryption
cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None)
decryptor = cipher.decryptor()

# Decrypt the ciphertext
decrypted_plaintext = decryptor.update(ciphertext)
print(f"Decrypted plaintext: {decrypted_plaintext.decode('utf-8')}")

由于加密器和解密器尚未最终确定,您可以调用 update 并提供更多明文/密文来连续加密/解密数据流。这是对称加密,因此加密器和解密器之间必须共享相同的密钥和nonce。

哈希算法

哈希算法是单向算法,它将任意大的数据缩减为一个方向上的单个数字。从这个数字,您无法返回到原始数据。该数字的长度是固定的,称为“摘要”。密码学哈希函数 是一种哈希函数,在该函数中,找到生成相同摘要的不同数据在计算上是不可行的。

哈希算法可用于签名数据、验证文件下载等数据是否正确,以及作为身份验证加密等协议的一部分。哈希也用于密码检查;我们可以存储密码的哈希值,而不是以纯文本形式存储密码,并且可以将用户输入的哈希值与密码的哈希值进行比较。哈希算法经常出现。

内置的Python哈希函数对每种类型(例如,字符串、数字等)使用特定的哈希算法,此哈希的主要用途是用于字典(哈希表),而Python是构建在用于命名空间和对象的字典之上的。这些算法最大限度地减少了哈希表中的哈希冲突,并且作为密码学哈希算法并不有趣。

对于密码学哈希函数,我们希望使其不可能

  1. 在不更改哈希值的情况下修改消息。
  2. 生成具有给定哈希值的消息。
  3. 找到两个具有相同哈希值的不同消息。

输入中的微小更改应在输出中产生较大更改,即雪崩效应。可以通过生成输入的所有可能排列的哈希值“彩虹表”来攻击哈希值(对于有限大小的输入 - 这就是为什么长密码更安全的原因)。我们通过“加盐哈希”来防止彩虹攻击,为每个密码使用不同的盐值(也存储),因此您需要每个密码的彩虹表才能从哈希值返回到密码。 

密钥派生函数比哈希和加盐更好(KDF基于哈希并用于哈希)用于密码保护。最先进的技术会随着时间推移而变化,因此请使用为您提供安全密码管理的框架(或委托给Azure AD之类的身份提供商来为您执行此操作)。

bcrypt 是Niels Provos和David Mazières设计的现代密码哈希函数,基于Blowfish密码,并在1999年的USENIX上提出。

如果可以为哈希函数生成与另一个哈希值相同的新消息(从而可以伪造仍然与给定哈希值匹配的消息或数据),则称该哈希函数容易受到碰撞攻击。md5以这种方式被破解。

Python中的用于哈希的hashlib

hashlib为标准哈希算法(如md5(已过时但仍常见)、sha256、PKCS#5、blake2等)提供了Python接口。不同的哈希算法具有不同的用途,包括不同的性能和安全性。如果消息包含已签名的哈希值,那么您就知道是谁发送的,并且可以验证内容是否已被篡改。从网络接收的数据可以使用哈希值进行验证。

使用hashlib验证下载文件的SHA 256的示例代码

import hashlib

with open(file_path, 'rb') as file:
    file_hash = hashlib.sha256()
    while chunk := file.read(8192):
        file_hash.update(chunk)

# Convert the calculated hash to hexadecimal
calculated_hash = file_hash.hexdigest()

# Compare the calculated hash with the expected hash
assert calculated_hash.lower() == expected_hash.lower()

分发哈希值仍然必须通过安全机制执行,该机制不受可能修改正文的同一攻击者的修改。

Python中的hmac

仅检查哈希值并不能证明该文件不是恶意的,任何可以植入恶意下载的人也可以修改预期的哈希值。消息认证码(MAC)比仅哈希值更能验证数据,因为它们还通过签名算法提供身份验证。HMAC是一种常用算法

这是一个使用hmac库为“消息”生成签名的示例,使用共享密钥和SHA-3(现代)哈希算法

import base64
import hmac
import hashlib

message = b'some secret message'
key = b'secret-shared-key-goes-here'
hash = hmac.new(
    key,
    message,
    hashlib.sha3_512,
)

signature = hash.digest()
print(base64.encodebytes(signature))

相同的代码可用于生成和验证签名,尽管有一个辅助函数可以比较摘要,该函数可以抵御定时攻击

hmac.compare_digest(signature, digest_of_received_data)

这是一个使用 cryptography 库的示例

from cryptography.hazmat.primitives import hashes, hmac

key = b'A real key should use os.urandom or TRNG to generate'
message = b"message to hash" 
h = hmac.HMAC(key, hashes.SHA3_512())
h.update(message)
signature = h.finalize()
# b'k\xd9\xb29\xefS\xf8\xcf\xec\xed\xbf...'

# Now verify the same message using the signature
h = hmac.HMAC(key, hashes.SHA3_512())
h.update(message)
# This will raise a cryptography.exceptions.InvalidSignature
# exception if the signature does not match.
h.verify(signature)
a bunch of keys that are on a yellow background
照片由 Alp Duran 拍摄于 Unsplash

密钥交换协议

两个人如何在不安全的通道上通信?  

互联网,实际上任何网络,从根本上来说都是不安全的,并且可以在任何时候被监听。传输中加密解决了这个问题,但对于对称加密算法,两个连接方需要能够拥有可以通过不安全连接传输的共享密钥(密钥)。

Diffie-Hellman 和椭圆曲线 Diffie-Hellman 是密钥交换协议,允许双方以窃听者无法拦截的方式传递共享密钥。

密钥交换协议在使用对称密钥交换(密钥加密)的加密系统中或作为更复杂的协议(如TLS)的一部分找到。

您几乎肯定不应该直接实现或使用 Diffie-Hellman,而是应该使用包含它的其他协议。有很多方法可以弄错。如果您想查看示例代码以了解其工作原理,请阅读 Crypto 101 中的章节或参考此文档页面

公钥加密

公钥加密算法起源于20世纪70年代,在那之前被认为是不可行的。最常见的算法是RSA,它也是最初的算法之一。RSA没有被破解,但存在部分攻击。OAEP(最佳非对称加密填充)是一种最先进的算法。

公钥加密是非对称的。密钥以公钥和私钥对生成。数据可以使用公钥加密,并且只能使用私钥解密。数据可以专门为接收者加密,并且没有私钥的第三方无法解密。密钥交换协议是公钥加密的一个示例。您可能用于通过git(可能)安全访问代码的公钥/私钥对是公钥加密和身份验证的另一个示例。

公钥加密也用于签名算法。在最广泛使用的公钥算法下,我们有一个令人高兴的属性,即用私钥加密的数据只能用相应的公钥解密。如果数据可以用某人的公钥解密,您就知道它是他们加密的(或至少是用他们的私钥加密的)。这是签名算法的基础。您不仅可以确信通道是安全的(加密),而且可以确信您正在与您认为与之交谈的人交谈(签名证书)。这种组合是TLS的基础。

公钥加密比密钥加密慢得多,在输入大小上呈指数级减慢,有时慢数千倍。

这是一个使用RSA公钥加密生成密钥,然后签名和哈希消息的示例。接收者可以使用发送者的公钥验证哈希和签名消息。这检查了它是否由声称发送它的人发送,并且内容与发送的内容相同

import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import (
  padding, rsa, utils
)

message = b"A message I want to sign"

# Generate public and private keys
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048,
)
public_key = private_key.public_key()

# The message is hashed and signed with the private key
prehashed_msg = hashlib.sha256(message).digest()
signature = private_key.sign(
    prehashed_msg,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    utils.Prehashed(hashes.SHA256())
)

# The hashed and signed message can then be authenticated
# and verified with the public key.
public_key.verify(
    signature,
    prehashed_msg,
    padding.PSS(
        mgf=padding.MGF1(hashes.SHA256()),
        salt_length=padding.PSS.MAX_LENGTH
    ),
    utils.Prehashed(hashes.SHA256())
)

签名算法

签名算法是MAC的公钥等效项。MAC是对称的,使用共享密钥。签名算法是非对称的。私钥用于生成消息(在本例中为签名),公钥用于验证/解释它,这与公钥加密相反。如果可以使用公钥解释消息,则可以确定该消息是使用私钥创建的,因此我们使用它来验证身份(身份验证)。

签名算法可以构建在其他加密算法之上,并由以下部分组成

  1. 密钥生成算法,可以与其他公钥算法共享
  2. 签名生成算法
  3. 签名验证算法

RSA和DSA用于签名算法,但很容易错误地实现或配置它们,从而消除所有安全优势。Ed25519是一种使用 EdDSA Curve25519 的椭圆曲线签名算法。如果您没有遗留互操作性方面的顾虑,那么您应该强烈考虑使用此签名算法

>>> from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
>>> message = b"my authenticated message"
>>>
>>> private_key = Ed25519PrivateKey.generate()
>>> signature = private_key.sign(message)
>>> public_key = private_key.public_key()
>>>
>>> # Raises InvalidSignature if verification fails
>>> public_key.verify(signature, message)

GPG(GNU Privacy Guard) 是最广泛用于加密和签名数据的系统之一,但以复杂和令人困惑而闻名。它建立在公钥加密之上。 OpenPGP 是使用公钥加密加密电子邮件的兼容标准。TLS(安全通信的基础)将公钥加密用于握手和证书。

伪随机数生成器

任何考虑使用算术方法生成随机数字的人,当然都处于罪恶的状态。– 约翰·冯·诺依曼

许多密码学算法都依赖于随机数据。世界上有许多真正的随机性来源(算术算法不在此列)。系统中可用的真实随机性量称为“熵”量。所有现代操作系统都提供密码学安全的随机性来源,可以通过Python中的 os.urandom 或通过 secrets.token_bytes 访问,后者是相同事物的更现代的接口。

我们有时还需要可重复的随机分布数据流,例如用于密钥流。伪随机数生成器可以从起始种子生成数字,并且从相同的种子开始将生成相同的数字流。os.urandom是不可重复的,并且不采用种子。

对于PRNG中的密码学安全性,必须不可能预测未来的输出或恢复过去的输出(给定当前的输出)。

Python random 模块提供了基于梅森旋转算法的伪随机数生成器。它旨在在数学上有用(“专为建模和仿真而设计”),并且在密码学上不安全。请改用 secrets 模块

secrets 模块中最有用的功能可能是生成安全令牌,适用于密码重置、难以猜测的URL和类似应用程序。令牌函数有字节、十六进制和URL安全版本

import secrets 
token1 = secrets.token_bytes()
token2 = secrets.token_bytes(10)
token3 = secrets.token_hex(16)
token4 = secrets.token_urlsafe()

secrets 模块还具有诸如 choice 之类的函数,您可以将其代替random.choice使用,以及 randbelow,您可以将其代替random.randint使用。 

分组密码将伪随机生成器(PRNG)用于IV(初始化向量)和块密钥流。

这是系列的一部分:“Python Web应用程序开发人员必须了解的关于安全性的绝对最低限度。” 下一篇:TLS 和网络

作者

  • Michael Foord

    我是Python培训师和承包商。我专注于Python教学和系统的端到端自动化测试。我的热情在于代码的简洁明了、高效的轻量级流程以及设计良好的系统。作为Python核心开发人员,我编写了unittest的一部分,并创建了mock库,该库后来成为unittest.mock。

    查看所有文章

如果您喜欢这篇文章,您可能也会喜欢这些