安全Argon2密码学Argon2密码哈希算法深度解析:原理、实现与实战应用
XRArgon2密码哈希算法深度解析:原理、实现与实战应用
引言
做后端开发这些年,密码存储一直是个让人头疼的问题。早期项目用MD5,后来知道不安全了改SHA-256,再后来听说要加盐,又搞了SHA-512+随机盐。但总感觉心里不踏实,直到遇到了Argon2。
这篇文章不是简单的API调用教程,而是要把Argon2的底层原理掰开了讲清楚,告诉你为什么它比传统哈希算法安全,以及在实际项目中怎么用。
第一部分:问题背景 - 为什么需要Argon2
1.1 传统哈希算法的根本问题
我们先看看为什么SHA-512这类算法在密码存储上有天然缺陷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import hashlib import time
def test_sha512_speed(): password = "user_password_123" salt = "random_salt_xyz" start_time = time.time() for i in range(1000000): hash_result = hashlib.sha512((password + salt).encode()).hexdigest() end_time = time.time() print(f"SHA-512计算100万次耗时: {end_time - start_time:.2f}秒") print(f"平均每次: {(end_time - start_time) * 1000000:.2f}微秒")
|
这个速度意味着什么?攻击者用一块普通显卡,每秒可以尝试数十亿个密码。即使你加了盐,只要密码本身不够复杂,被破解只是时间问题。
问题的核心在于:SHA-512被设计为快速计算哈希值,而密码验证需要的是慢速哈希。
1.2 真实世界的攻击成本分析
让我们算笔账。假设攻击者拿到了你的数据库:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| def crack_time_estimation(): gpu_speed = 2_000_000_000 passwords_by_length = { "6位数字": 10**6, "8位小写字母": 26**8, "8位大小写+数字": (26+26+10)**8, "12位大小写+数字+符号": (94)**12 } for desc, combinations in passwords_by_length.items(): avg_attempts = combinations // 2 time_seconds = avg_attempts / gpu_speed if time_seconds < 1: time_str = f"{time_seconds*1000:.1f}毫秒" elif time_seconds < 60: time_str = f"{time_seconds:.1f}秒" elif time_seconds < 3600: time_str = f"{time_seconds/60:.1f}分钟" elif time_seconds < 86400: time_str = f"{time_seconds/3600:.1f}小时" elif time_seconds < 86400*365: time_str = f"{time_seconds/86400:.1f}天" else: time_str = f"{time_seconds/(86400*365):.1f}年" print(f"{desc}: 平均破解时间 {time_str}")
|
可以看到,除非用户密码特别复杂,否则SHA-512基本上就是在裸奔。
第二部分:Argon2基础概念
2.1 Argon2的诞生背景
Argon2不是拍脑袋想出来的,它是2015年Password Hashing Competition(PHC)的获胜者。这个竞赛就是为了解决密码哈希的安全问题,全世界的密码学专家提交了24个方案,经过3年的评估,Argon2胜出。
2.2 核心设计理念
Argon2的设计哲学很简单:既然无法阻止攻击者尝试,那就让每次尝试都变得极其昂贵。
具体怎么做?三个维度:
- 内存成本 - 每次计算需要大量内存
- 时间成本 - 可以调节计算时间
- 并行限制 - 限制并行攻击的效率
2.3 三个变种对比
Argon2有三个变种:
- Argon2i - 独立于内存的访问模式,抗侧信道攻击
- Argon2d - 依赖数据的访问模式,抗时间-内存权衡攻击
- Argon2id - 混合模式,平衡两种攻击的防护
实际项目中,推荐使用Argon2id,它综合了前两者的优势。
2.4 核心参数解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def argon2_parameters_explained(): """ Argon2的三个核心参数 """ parameters = { "time_cost": { "含义": "时间成本,迭代次数", "推荐值": "3-10", "影响": "计算时间线性增长" }, "memory_cost": { "含义": "内存成本,以KB为单位", "推荐值": "65536 (64MB) - 1048576 (1GB)", "影响": "内存使用量和攻击成本" }, "parallelism": { "含义": "并行度,同时运行的线程数", "推荐值": "1-4", "影响": "CPU利用率和计算时间" } } return parameters
|
第三部分:核心工作原理
3.1 参数绑定机制:为什么不同参数无法计算出相同值
其实我刚看到 Argon2是可以选择参数来控制时间与空间成本的时候,我就有点好奇,那么使用不同参数,结果肯定不一样吧。那么怎么实现的呢?
答案:使用不同的内存成本和时间成本参数,确实无法计算出相同的哈希值。这不是缺陷,而是Argon2的关键安全特性。
参数绑定的工作原理
Argon2将所有参数都编码在最终的哈希字符串中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| def demonstrate_parameter_binding(): """ 演示Argon2参数绑定机制 """ import argon2 password = "same_password_123" hashers = { "低安全": argon2.PasswordHasher(time_cost=1, memory_cost=1024, parallelism=1), "中安全": argon2.PasswordHasher(time_cost=3, memory_cost=65536, parallelism=4), "高安全": argon2.PasswordHasher(time_cost=10, memory_cost=1048576, parallelism=8) } print("=== 相同密码,不同参数的哈希结果 ===") results = {} for level, hasher in hashers.items(): hash_result = hasher.hash(password) results[level] = hash_result print(f"\n{level}参数:") print(f"哈希值: {hash_result}") parts = hash_result.split('$') if len(parts) >= 4: params = parts[3] print(f"嵌入参数: {params}") print(f"\n=== 哈希值比较 ===") hash_values = list(results.values()) print(f"低安全 == 中安全: {hash_values[0] == hash_values[1]}") print(f"中安全 == 高安全: {hash_values[1] == hash_values[2]}") print(f"低安全 == 高安全: {hash_values[0] == hash_values[2]}") return results
|
安全意义:防止参数降级攻击
这种参数绑定机制的安全意义:
- 防止参数降级攻击 - 攻击者不能用低参数重新计算来绕过安全设置
- 强制安全策略 - 旧哈希值无法通过新参数验证,强制升级
- 审计和合规 - 哈希值本身包含了安全参数信息
- 防止时间-空间权衡 - 必须使用完全相同的参数配置
3.2 内存访问模式详解
Argon2的核心创新在于它的内存访问模式。传统哈希算法内存占用很小,而Argon2会创建一个大型的内存数组,然后在其中进行复杂的数据依赖操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| def argon2_memory_pattern_simplified(): """ 这是Argon2内存访问的简化版本,帮助理解其工作原理 实际算法要复杂得多 """ memory_size = 65536 memory_blocks = [bytearray(1024) for _ in range(memory_size)] for i in range(memory_size): if i == 0: memory_blocks[i] = initial_hash(password, salt, i) else: memory_blocks[i] = hash_function(memory_blocks[i-1], i) for round_num in range(1, time_cost): for i in range(memory_size): access_index = compute_access_index(memory_blocks[i], i, round_num) memory_blocks[i] = mix_blocks( memory_blocks[i], memory_blocks[access_index] ) return final_hash(memory_blocks)
|
3.3 为什么这样设计能提高安全性?
关键在于数据依赖性:
- 内存无法压缩 - 每个内存块的内容都不可预测,攻击者必须存储所有中间状态
- 计算无法并行 - 下一步的计算依赖于前面的结果,无法跳跃
- 时间-内存权衡无效 - 即使攻击者用更多计算时间换更少内存,效果也很有限
第四部分:算法实现细节
4.1 算法整体流程
graph TD
A["输入: 密码P, 盐S, 参数"] --> B["阶段1: 初始化"]
B --> C["阶段2: 构建内存矩阵"]
C --> D["阶段3: 内存访问轮次"]
D --> E["阶段4: 最终输出"]
subgraph "阶段1: 初始化"
B1["计算H0 = Blake2b(P||S||参数)"]
B2["生成初始块B[0][0], B[0][1]"]
end
subgraph "阶段2: 构建内存矩阵"
C1["并行填充p个线程的初始行"]
C2["每个线程: B[i][j] = G(B[i][j-1], B[i][j-2])"]
end
subgraph "阶段3: 内存访问轮次"
D1["for pass = 1 to t-1"]
D2["计算访问索引: φ(B[i][j-1])"]
D3["B[i][j] = G(B[i][j-1], B[l][z])"]
D4["l, z 基于φ函数和Argon2变种"]
end
subgraph "阶段4: 最终输出"
E1["压缩最后一列: C = B[0][m-1] ⊕ ... ⊕ B[p-1][m-1]"]
E2["输出: Blake2b(C)[0..τ-1]"]
end
4.2 数学符号定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def argon2_mathematical_notation(): """ Argon2数学符号定义 """ symbols = { "基本参数": { "P": "密码 (password)", "S": "盐 (salt)", "p": "并行度 (parallelism)", "τ": "输出长度 (tag length)", "m": "内存成本,以KB为单位", "t": "时间成本 (iterations)" }, "内存结构": { "B[i][j]": "内存矩阵,i是线程索引(0≤i<p),j是块索引(0≤j<m/p)", "m'": "实际内存块数量 = 4*m*1024/1024 = 4*m", "q": "每个线程的块数量 = m'/p" }, "函数定义": { "H": "Blake2b哈希函数", "G": "压缩函数 (compression function)", "φ": "访问索引计算函数", "⊕": "异或运算" } } return symbols
|
4.3 压缩函数G的数学定义
压缩函数G是Argon2安全性的核心,我们来看看它的数学性质:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| def compression_function_analysis(): """ 压缩函数G的数学性质分析 压缩函数G: {0,1}^2048 → {0,1}^1024 """ def compression_G(block1, block2): """ 压缩函数G(X,Y)的实现 """ R = bytearray(1024) for i in range(1024): R[i] = block1[i] ^ block2[i] Z = permutation_P(R) result = bytearray(1024) for i in range(1024): result[i] = R[i] ^ Z[i] return bytes(result) properties = { "函数签名": "G: {0,1}^2048 → {0,1}^1024", "输入": "两个1024字节的块 X, Y", "输出": "一个1024字节的块", "数学性质": { "非线性": "由于P函数的非线性,G也是非线性的", "扩散性": "输入的微小改变会导致输出的大幅改变", "混淆性": "输入和输出之间的关系复杂难以推断", "不可逆": "从G(X,Y)计算X或Y在计算上不可行" } } return compression_G, properties
|
第五部分:与传统哈希算法对比
5.1 计算成本对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import argon2 import hashlib import time import psutil
def performance_comparison(): """ Argon2 vs SHA-512 性能对比 """ password = "test_password_123" salt = b"random_salt_bytes" print("=== SHA-512性能测试 ===") start_time = time.time() start_memory = psutil.Process().memory_info().rss / 1024 / 1024 for i in range(10000): sha512_hash = hashlib.sha512(password.encode() + salt).hexdigest() end_time = time.time() end_memory = psutil.Process().memory_info().rss / 1024 / 1024 print(f"计算1万次SHA-512耗时: {end_time - start_time:.3f}秒") print(f"内存占用增加: {end_memory - start_memory:.1f}MB") print(f"平均每次: {(end_time - start_time) * 100:.3f}毫秒") print("\n=== Argon2性能测试 ===") ph = argon2.PasswordHasher( time_cost=3, memory_cost=65536, parallelism=1 ) start_time = time.time() start_memory = psutil.Process().memory_info().rss / 1024 / 1024 for i in range(10): argon2_hash = ph.hash(password) end_time = time.time() end_memory = psutil.Process().memory_info().rss / 1024 / 1024 print(f"计算10次Argon2耗时: {end_time - start_time:.3f}秒") print(f"内存占用增加: {end_memory - start_memory:.1f}MB") print(f"平均每次: {(end_time - start_time) * 100:.1f}毫秒")
|
从这个对比可以看出:Argon2单次计算的时间是SHA-512的数万倍,内存占用是数百倍。
5.2 攻击成本分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| def attack_cost_analysis(): """ 攻击成本的实际计算 """ print("=== 破解8位密码的成本对比 ===") password_space = (26 + 26 + 10) ** 8 avg_attempts = password_space // 2 sha512_speed = 2_000_000_000 sha512_time = avg_attempts / sha512_speed / 86400 sha512_gpu_cost = 1000 sha512_power_day = 5 sha512_total_cost = sha512_gpu_cost + sha512_time * sha512_power_day print(f"SHA-512破解成本:") print(f" 时间: {sha512_time:.1f}天") print(f" 硬件成本: ${sha512_gpu_cost}") print(f" 电费: ${sha512_time * sha512_power_day:.0f}") print(f" 总成本: ${sha512_total_cost:.0f}") argon2_speed = 10 argon2_memory_per_attempt = 64 argon2_time = avg_attempts / argon2_speed / 86400 parallel_attempts = 1000 total_memory_gb = argon2_memory_per_attempt * parallel_attempts / 1024 memory_cost_per_gb = 50 argon2_hardware_cost = total_memory_gb * memory_cost_per_gb argon2_power_day = 50 argon2_total_cost = argon2_hardware_cost + (argon2_time / parallel_attempts) * argon2_power_day print(f"\nArgon2破解成本:") print(f" 时间: {argon2_time / parallel_attempts:.1f}天 (并行{parallel_attempts}次)") print(f" 内存需求: {total_memory_gb:.0f}GB") print(f" 硬件成本: ${argon2_hardware_cost:.0f}") print(f" 电费: ${(argon2_time / parallel_attempts) * argon2_power_day:.0f}") print(f" 总成本: ${argon2_total_cost:.0f}") print(f"\n成本倍数: Argon2是SHA-512的 {argon2_total_cost / sha512_total_cost:.0f} 倍")
|
第六部分:实战应用指南
6.1 不同场景的参数配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| def argon2_configurations(): """ 不同场景下的Argon2参数推荐 """ configurations = { "高安全系统": { "time_cost": 10, "memory_cost": 1024 * 1024, "parallelism": 4, "适用": ["金融系统", "政府系统", "关键基础设施"], "登录时间": "2-5秒", "说明": "可以容忍较长登录时间,追求最高安全性" }, "企业应用": { "time_cost": 3, "memory_cost": 65536, "parallelism": 4, "适用": ["企业内部系统", "B2B平台", "后台管理"], "登录时间": "0.5-1秒", "说明": "平衡安全性和用户体验" }, "消费级应用": { "time_cost": 2, "memory_cost": 32768, "parallelism": 2, "适用": ["移动APP", "网站", "游戏"], "登录时间": "0.2-0.5秒", "说明": "考虑移动设备和网络延迟" }, "IoT设备": { "time_cost": 2, "memory_cost": 4096, "parallelism": 1, "适用": ["嵌入式设备", "智能硬件"], "登录时间": "0.1-0.2秒", "说明": "资源受限环境" } } return configurations
|
6.2 Python实现示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| import argon2 from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError, HashingError import time
class SecurePasswordManager: """ 基于Argon2的安全密码管理器 """ def __init__(self, time_cost=3, memory_cost=65536, parallelism=4): self.ph = PasswordHasher( time_cost=time_cost, memory_cost=memory_cost, parallelism=parallelism, hash_len=32, salt_len=16, encoding='utf-8', type=argon2.Type.ID ) def hash_password(self, password: str) -> str: """哈希密码""" try: start_time = time.time() hashed = self.ph.hash(password) end_time = time.time() print(f"密码哈希计算耗时: {(end_time - start_time) * 1000:.1f}ms") return hashed except HashingError as e: print(f"密码哈希失败: {e}") raise def verify_password(self, hashed: str, password: str) -> bool: """验证密码""" try: start_time = time.time() self.ph.verify(hashed, password) end_time = time.time() print(f"密码验证耗时: {(end_time - start_time) * 1000:.1f}ms") return True except VerifyMismatchError: return False except Exception as e: print(f"密码验证出错: {e}") return False def get_hash_info(self, hashed: str) -> dict: """解析哈希值信息""" try: parts = hashed.split('$') if len(parts) != 6: return {"error": "Invalid hash format"} algorithm = parts[1] version = parts[2] params = parts[3] salt = parts[4] hash_value = parts[5] param_dict = {} for param in params.split(','): key, value = param.split('=') param_dict[key] = int(value) return { "algorithm": algorithm, "version": version, "memory_cost": param_dict.get('m', 0), "time_cost": param_dict.get('t', 0), "parallelism": param_dict.get('p', 0), "salt_b64": salt, "hash_b64": hash_value } except Exception as e: return {"error": f"Failed to parse hash: {e}"}
def demo_usage(): """使用示例""" print("=== Argon2密码管理器演示 ===") pm = SecurePasswordManager(time_cost=3, memory_cost=65536, parallelism=4) test_password = "MySecurePassword123!" print("\n1. 哈希密码") hashed = pm.hash_password(test_password) print(f"哈希结果: {hashed}") print("\n2. 哈希信息") info = pm.get_hash_info(hashed) for key, value in info.items(): print(f" {key}: {value}") print("\n3. 验证正确密码") result = pm.verify_password(hashed, test_password) print(f"验证结果: {'通过' if result else '失败'}") print("\n4. 验证错误密码") result = pm.verify_password(hashed, "WrongPassword") print(f"验证结果: {'通过' if result else '失败'}")
if __name__ == "__main__": demo_usage()
|
6.3 参数升级和迁移策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| class ParameterMigrationManager: """ 参数迁移管理器 - 处理不同安全级别的哈希值 """ def __init__(self): self.parameter_versions = { "v1.0": {"time_cost": 2, "memory_cost": 16384, "parallelism": 1}, "v2.0": {"time_cost": 3, "memory_cost": 65536, "parallelism": 4}, "v3.0": {"time_cost": 5, "memory_cost": 262144, "parallelism": 8} } self.current_version = "v3.0" self.hashers = {} for version, params in self.parameter_versions.items(): self.hashers[version] = argon2.PasswordHasher(**params) def identify_hash_version(self, hash_string): """识别哈希值的参数版本""" try: parts = hash_string.split('$') if len(parts) >= 4: params_str = parts[3] params = {} for param in params_str.split(','): key, value = param.split('=') params[key] = int(value) for version, version_params in self.parameter_versions.items(): if (params.get('m') == version_params['memory_cost'] and params.get('t') == version_params['time_cost'] and params.get('p') == version_params['parallelism']): return version return "unknown" except: return "invalid" def verify_with_migration(self, username, password, stored_hash): """验证密码并在需要时进行迁移""" hash_version = self.identify_hash_version(stored_hash) if hash_version == "invalid": return False, None, "invalid_hash" try: if hash_version == "unknown": current_hasher = self.hashers[self.current_version] current_hasher.verify(stored_hash, password) return True, stored_hash, "verified" version_hasher = self.hashers[hash_version] version_hasher.verify(stored_hash, password) if hash_version != self.current_version: new_hasher = self.hashers[self.current_version] new_hash = new_hasher.hash(password) print(f"用户 {username} 的密码从 {hash_version} 升级到 {self.current_version}") return True, new_hash, "upgraded" else: return True, stored_hash, "verified" except argon2.exceptions.VerifyMismatchError: return False, None, "wrong_password" except Exception as e: return False, None, f"error_{str(e)}"
|
6.4 部署和维护建议
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| class Argon2DeploymentGuide: """ Argon2部署实战指南 """ def benchmark_parameters(self, target_time_ms=500): """基准测试,找到合适的参数""" import argon2 import time print(f"目标计算时间: {target_time_ms}ms") print("开始参数调优...") password = "benchmark_password" best_params = None for time_cost in [1, 2, 3, 4, 5]: for memory_cost in [16384, 32768, 65536]: ph = argon2.PasswordHasher( time_cost=time_cost, memory_cost=memory_cost, parallelism=2 ) total_time = 0 for _ in range(5): start = time.time() ph.hash(password) total_time += (time.time() - start) avg_time_ms = (total_time / 5) * 1000 print(f" time_cost={time_cost}, memory_cost={memory_cost}: {avg_time_ms:.1f}ms") if best_params is None or abs(avg_time_ms - target_time_ms) < abs(best_params['time'] - target_time_ms): best_params = { 'time_cost': time_cost, 'memory_cost': memory_cost, 'time': avg_time_ms } print(f"\n推荐参数: time_cost={best_params['time_cost']}, memory_cost={best_params['memory_cost']}") print(f"实际计算时间: {best_params['time']:.1f}ms") return best_params def production_recommendations(self): """生产环境建议""" return { "配置管理": [ "将Argon2参数放在配置文件中,便于调整", "不同环境(开发/测试/生产)使用不同参数", "定期评估和调整参数" ], "性能监控": [ "监控密码验证的响应时间", "监控服务器内存使用情况", "设置超时和重试机制" ], "安全考虑": [ "定期更新Argon2库版本", "监控是否有新的攻击方法", "考虑添加额外的安全层(如MFA)" ], "扩展性": [ "如果用户量大,考虑使用缓存减少重复计算", "可以考虑异步处理密码验证", "负载均衡时注意内存分配" ] }
|
总结
Argon2不是什么黑科技,它的核心思想很简单:让攻击者为每次密码尝试付出高昂的代价。通过巧妙的内存访问模式和可调节的参数,它在密码哈希领域确实是目前的最佳选择。
关键要点回顾
- 理解核心问题 - 传统哈希算法太快,给了攻击者机会
- 参数绑定机制 - 不同参数无法计算出相同值,防止降级攻击
- 内存依赖设计 - 数据依赖的内存访问模式让攻击成本指数增长
- 实际应用考虑 - 根据场景选择合适参数,制定升级策略
- 持续维护 - 定期评估参数,跟进安全发展
最终建议
- 新项目直接使用Argon2id - 不要再用传统哈希算法
- 老项目制定迁移计划 - 逐步升级到Argon2
- 参数选择要平衡 - 安全性和用户体验都要考虑
- 保持学习态度 - 密码学在发展,要跟上时代
安全是个系统工程,Argon2只是其中一环。密码策略、多因素验证、系统监控等都同样重要。但作为密码存储的基础,选择Argon2确实能让你在安全性上领先一大步。
参考资料
- Argon2 Specification
- Password Hashing Competition
- OWASP Password Storage Cheat Sheet
- Argon2 GitHub Repository