惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
I
InfoQ
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
P
Privacy International News Feed
T
Threatpost
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Vulnerabilities – Threatpost
NISL@THU
NISL@THU
aimingoo的专栏
aimingoo的专栏
S
Schneier on Security
C
Cisco Blogs
T
The Blog of Author Tim Ferriss
Simon Willison's Weblog
Simon Willison's Weblog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Jina AI
Jina AI
雷峰网
雷峰网
Know Your Adversary
Know Your Adversary
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
博客园 - Franky
博客园 - 【当耐特】
Hugging Face - Blog
Hugging Face - Blog
The Hacker News
The Hacker News
K
Kaspersky official blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tailwind CSS Blog
Project Zero
Project Zero
T
Tor Project blog
B
Blog RSS Feed
Recorded Future
Recorded Future
Scott Helme
Scott Helme
美团技术团队
V
V2EX
V
Visual Studio Blog
L
Lohrmann on Cybersecurity
P
Proofpoint News Feed
D
DataBreaches.Net
The Register - Security
The Register - Security
M
MIT News - Artificial intelligence
L
LangChain Blog
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Cyber Attacks, Cyber Crime and Cyber Security
博客园_首页
P
Privacy & Cybersecurity Law Blog

Scrypt - 标签 - This Cute World

暂无文章

写给开发人员的实用密码学(三)—— MAC 与密钥派生函数 KDF
於清樂 · 2022-03-01 · via Scrypt - 标签 - This Cute World

本文主要翻译自 Practical-Cryptography-for-Developers-Book,笔者补充了 HMAC 的 Python 实现以及 scrypt 使用示例。

MAC 消息认证码,即 Message Authentication Code,是用于验证消息的一小段附加数据。换句话说, 能用它确认消息的真实性——消息来自指定的发件人并且没有被篡改。

MAC 值通过允许拥有密钥的验证者检测消息内容的任何更改来保护消息的数据完整性及其真实性。

一个安全的 MAC 函数,跟加密哈希函数非常类似,也拥有如下特性:

  • 快速:计算速度要足够快
  • 确定性:对同样的消息跟密钥,应该总是产生同样的输出
  • 难以分析:对消息或密钥的任何微小改动,都应该使输出完全发生变化
  • 不可逆:从 MAC 值逆向演算出消息跟密钥应该是不可行的。
  • 无碰撞:找到具有相同哈希的两条不同消息应该非常困难(或几乎不可能)

但是 MAC 算法比加密哈希函数多一个输入值:密钥,因此也被称为 keyed hash functions,即「加密钥的哈希函数」。

如下 Python 代码使用 key 跟消息计算出对应的 HMAC-SHA256 值:

import hashlib, hmac, binascii

key = b"key"
msg = b"some msg"

mac = hmac.new(key, msg, hashlib.sha256).digest()

print(f"HMAC-SHA256({key}, {msg})", binascii.hexlify(mac).decode('utf8'))

# => HMAC-SHA256(b'key', b'some msg') = 32885b49c8a1009e6d66662f8462e7dd5df769a7b725d1d546574e6d5d6e76ad

HMAC 的算法实际上非常简单,我参考 wiki/HMAC 给出的伪码,编写了下面这个 Python 实现,没几行代码,但是完全 work:

import hashlib, binascii

def xor_bytes(b1, b2):
  return bytes(a ^ c for a, c in zip(b1, b2))

def my_hmac(key, msg, hash_name):
  # hash => (block_size, output_size)
  # 单位是 bytes,数据来源于 https://en.wikipedia.org/wiki/HMAC
  hash_size_dict = {
    "md5": (64, 16),
    "sha1": (64, 20),
    "sha224": (64, 28),
    "sha256": (64, 32),
    # "sha512/224": (128, 28),  # 这俩算法暂时不清楚在 hashlib 里叫啥名
    # "sha512/256": (128, 32),
    "sha_384": (128, 48),
    "sha_512": (128, 64),
    "sha3_224": (144, 28),
    "sha3_256": (136, 32),
    "sha3_384": (104, 48),
    "sha3_512": (72, 64),
  }
  if hash_name not in hash_size_dict:
    raise ValueError("unknown hash_name")

  block_size, output_size = hash_size_dict[hash_name]
  hash_ = getattr(hashlib, hash_name)

  # 确保 key 的长度为 block_size
  block_sized_key = key
  if len(key) > block_size:
    block_sized_key = hash_(key).digest()  # 用 hash 函数进行压缩
  if len(key) < block_size:
    block_sized_key += b'\x00' * (block_size - len(key))  # 末尾补 0

  o_key_pad = xor_bytes(block_sized_key, (b"\x5c" * block_size))  # Outer padded key
  i_key_pad = xor_bytes(block_sized_key, (b"\x36" * block_size))  # Inner padded key

  return hash_(o_key_pad + hash_(i_key_pad + msg).digest()).digest()


# 下面验证下
key = b"key"
msg = b"some msg"

mac_ = my_hmac(key, msg, "sha256")
print(f"HMAC-SHA256({key}, {msg})", binascii.hexlify(mac_).decode('utf8'))

# 输出跟标准库完全一致:
# => HMAC-SHA256(b'key', b'some msg') = 32885b49c8a1009e6d66662f8462e7dd5df769a7b725d1d546574e6d5d6e76ad

上一篇文章提到过,哈希函数只负责生成哈希值,不负责哈希值的可靠传递。

而数字签名呢,跟 MAC 非常相似,但是数字签名使用的是密钥对(非对称加密系统),原理更复杂,计算速度也更慢。

MAC 的功能跟数字签名一致,都是验证消息的真实性(authenticity)、完整性(integrity)、不可否认性(non-repudiation),但是 MAC 使用哈希函数与预共享密钥(对称密码系统)来做这件事情,速度要更快,算法也更简单。

这是最简单的一个应用场景,在通信双向都持有一个预共享密钥的前提下,通信时都附带上消息的 MAC 码。接收方也使用「收到的消息+预共享密钥」计算出 MAC 码,如果跟收到的一致,就说明消息真实无误。

注意这种应用场景中,消息是不保密的!

常用的加密方法只能保证数据的保密性,并不能保证数据的完整性。

而这里介绍的 MAC 算法,或者还未介绍的基于非对称加密的数字签名,都只能保证数据的真实性、完整性,不能保证数据被安全传输。

而认证加密,就是将加密算法与 MAC 算法结合使用的一种加密方案。

在确保 MAC 码「强不可伪造」的前提下,首先对数据进行加密,然后计算密文的 MAC 码,再同时传输密文与 MAC 码,就能同时保证数据的保密性、完整性、真实性,这种方法叫 Encrypt-then-MAC, 缩写做 EtM. 接收方在解密前先计算密文的 MAC 码与收到的对比,就能验证密文的完整性与真实性。

AE 有一种更安全的变体——带有关联数据的认证加密 (authenticated encryption with associated data,AEAD)。AEAD 将「关联数据(Associated Data, AD)」——也称为「附加验证数据 (Additional Authenticated Data, AAD)」——绑定到密文和它应该出现的上下文,以便可以检测和拒绝将有效密文「剪切并粘贴」到不同上下文的尝试。 AEAD 用于加密和未加密数据一起使用的场景(例如,在加密的网络协议中),并确保整个数据流经过身份验证和完整性保护。换句话说,AEAD 增加了检查某些内容的完整性和真实性的能力。

我们会在第六章「对称加密算法」中看到如何通过 Python 使用 AEAD 加密方案 AES-256-GCM.

MAC 码的另一个用途就是伪随机数生成函数,相比直接使用熵+哈希函数的进行伪随机数计算,MAC 码因为多引入了一个变量 key,理论上它会更安全。

这种场景下,我们称 MAC 使用的密钥为 salt,即盐。

next_seed = MAC(salt, seed)

我们都更喜欢使用密码来保护自己的数据而不是二进制的密钥,因为相比之下二进制密钥太难记忆了, 字符形式的密码才是符合人类思维习惯的东西。

可对计算机而言就刚好相反了,现代密码学的很多算法都要求输入是一个大的数字,二进制的密钥就是这样一个大的数字。因此显然我们需要一个将字符密码(Password)转换成密钥(Key)的函数,这就是密钥派生函数 Key Derivation Function.

KDF 的名字中包含了「Derivation(派生)」,顾名思义,它也可以从一个字符密码中生成多个密钥,一个常见的生成多个密钥的用法就是 BTC 的「分层确定性钱包(Hierarchical Deterministic Wallet)」。

直接使用 SHA256 之类的加密哈希函数来生成密钥是不安全的,因为为了方便记忆,通常密码并不会很长,绝大多数人的密码长度估计都不超过 15 位。甚至很多人都在使用非常常见的弱密码,如 123456 admin 生日等等。这就导致如果直接使用 SHA256 之类的算法,许多密码将很容易被暴力破解、字典攻击、彩虹表攻击等手段猜测出来!

也有些如 HKDF(HMAC-based KDF) 之类的 KDF 算法用于生成密钥,但它们不适合用于用户密码。HKDF 的特点是计算速度快,因此它只适合用于输入熵比较高的场景,比如将 DHKE 的共享密钥转换成对称加密算法的密钥。如果用 HKDF 来从用户密码生成密钥,那么密码的熵就太低了,将很容易被暴力破解!

KDF 目前主要从如下三个维度提升 hash 碰撞难度:

  1. 时间复杂度:对应 CPU/GPU 计算资源
  2. 空间复杂度:对应 Memory 内存资源
  3. 并行维度:使用无法分解的算法,锁定只允许单线程运算

主要手段是加盐,以及多次迭代。这种设计方法被称为「密钥拉伸 Key stretching」。

KDF 的工作示意图如下:

因为相比其他加密哈希算法,KDF 具有一个独特属性——计算速度很慢,而且从设计上就使其计算速度难以提升,所以 KDF 也被称作「慢哈希算法」。

KDF 计算速度的「慢」是相对而言的,对于 SSH 等本地应用而言,KDF 通常只需要在登录时被执行一次,因此慢这么一点点完全可以接受,而且用户也完全有足够的资源执行这个 KDF 函数。对于 APP 登录设计而言,用户登录完成后后续就会使用 cookie 进行验证,单次登录时多花费点资源并不会造成大量的资源消耗。但是如果一个黑客获得了你的加密 SSH Key 或者拖库拿到了 APP 的密码数据库,想要通过 Hash 碰撞来猜测出用户的密码,那它就必须执行海量的 KDF 计算,这个时候 KDF 的威力就显现出来了——黑客将需要提供海量的 CPU/GPU 计算资源、海量的内存资源才能完成目标,而这显然得不偿失,这样 KDF 就确保了用户密码的安全性。

目前比较著名的 KDF 算法主要有如下几个:

  1. PBKDF2:这是一个非常简单的加密 KDF 算法,目前已经不推荐使用。
  2. Bcrypt:安全性在下降,用得越来越少了。不建议使用。
    • 根据 Wikipedia 说明,在内存小于 4MB 时 bcrypt 要强于 scrypt,在运行时间小于 1 秒时其强度要高于 argon2. 因此在一些极端资源场景下,仍旧可以考虑 bcrypt 算法。
    • 另外需要注意的一点是,Wikipedia 说 bcrypt 并非典型的 KDF 算法,它的输出不适合直接用做加解密或签名用的 Key, 因此 OpenSSH 的实现是将 Bcrypt 的输出再用 BPKDF2 加盐处理一遍输出最终用于验证的二进制 Key,也即它的 bcrypt_pbkdf 算法。
  3. Scrypt:可以灵活地设定使用的内存大小,在 argon2 不可用时,可使用它。
    • Scrypt 是目前社区使用的主流算法,知名案例如:age,rclone,restic
  4. Argon2:目前最强的密码 Hash 算法,在 2015 年赢得了密码 Hash 竞赛。
    • Linux 的硬盘加密模块 LUKS2 就支持使用 argon2id 作为它的 KDF 算法。

如果你正在开发一个新的程序,需要使用到 KDF,建议选用 argon2/scrypt.

Python 中最流行的密码学库是cryptographyrequests 的底层曾经就使用了它(新版本已经换成使用标准库 ssl 了),下面我们使用这个库来演示下 Scrypt 算法的使用:

# pip install cryptography==36.0.1
import os
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt

salt = os.urandom(16)

# derive
kdf = Scrypt(
    salt=salt,
    length=32,
    n=2**14,
    r=8,
    p=1,
)
key = kdf.derive(b"my great password")

# verify
kdf = Scrypt(
    salt=salt,
    length=32,
    n=2**14,
    r=8,
    p=1,
)
kdf.verify(b"my great password", key)