password

11 天前
31
2

password

Rust与Python密码哈希兼容性调试:小白也能看懂的完整过程

🎯 项目背景:我要解决什么问题?

密码哈希是什么?(用大白话解释)

想象你有个密码"123456",直接存到数据库里不安全,黑客一看就知道了。所以我们要把它变成这样:

pbkdf2:sha256:260000$RandomSalt123$a1b2c3d4e5f6...一堆乱码

这个过程叫"哈希",就是把简单密码变成复杂字符串。好处是:

  • 安全:黑客看到乱码,很难猜出原密码
  • 验证:用户登录时,把输入的密码也变成乱码,如果和存储的一样就说明密码对了

为什么要兼容两种语言?

我们公司有两套系统:

  • 老系统:用Python写的,用Werkzeug库处理密码
  • 新系统:用Rust重写,更快更安全

问题来了:用户在老系统设的密码,在新系统应该还能用!

这就像两把不同的锁,但要用同一把钥匙能开。我的任务就是让Rust生成的"钥匙"(密码哈希)和Python的完全一样。

🕵️ 调试过程:一步步找出问题

第1步:发现不对劲 - "咦,怎么长得不一样?"

我先运行了两个版本,对比输出:

Python版本输出:

pbkdf2:sha256:260000$HehzXdgPCaIV50Iz$57bd6863f405f2001199a90ad9df238a8a2df722d45b32dee8aaf3383fe02bb9

Rust版本输出:

pbkdf2:sha256:260000$BS3zWkNu7sWZ42VS24r0Bg==$7DNjGGzjCgCOFavqpycM//U8YWhOmt5kHGRS0U6djtA=

一眼就看出问题了!

  • Python的盐值(第二部分):HehzXdgPCaIV50Iz (16个字符)
  • Rust的盐值:BS3zWkNu7sWZ42VS24r0Bg== (24个字符,还有等号)
  • Python的哈希值(第三部分):64个字符
  • Rust的哈希值:44个字符,还有等号

初步判断:Rust用的是Base64编码,Python用的可能是其他编码方式。

第2步:深入分析 - "让我看看这些字符到底是什么"

我写了个Python脚本来分析Werkzeug的输出格式:

# 分析Python生成的哈希
hash_result = "pbkdf2:sha256:260000$HehzXdgPCaIV50Iz$57bd6863f405f2001199a90ad9df238a8a2df722d45b32dee8aaf3383fe02bb9"
parts = hash_result.split('$')

salt = parts[1]      # HehzXdgPCaIV50Iz
hash_val = parts[2]  # 57bd6863f405f2001199a90ad9df238a8a2df722d45b32dee8aaf3383fe02bb9

print("盐值长度:", len(salt))        # 16个字符
print("哈希值长度:", len(hash_val))   # 64个字符

然后我尝试解码:

import base64
# 试试盐值是不是Base64
try:
    salt_decoded = base64.b64decode(salt + '==')  # 加padding试试
    print("盐值解码后字节数:", len(salt_decoded))  # 输出: 12
    print("盐值是Base64编码!")
except:
    print("盐值不是Base64")

# 试试哈希值是不是十六进制
try:
    hash_bytes = bytes.fromhex(hash_val)
    print("哈希值解码后字节数:", len(hash_bytes))  # 输出: 32
    print("哈希值是十六进制编码!")
except:
    print("哈希值不是十六进制")

发现

  • 盐值:Base64编码,解码后12字节
  • 哈希值:十六进制编码,64个字符 = 32字节

第3步:第一次尝试修复 - "按发现的规律改代码"

基于分析结果,我修改了Rust代码:

// 修改盐值长度
const SALT_LENGTH: usize = 12;  // 从16改成12字节

// 修改编码方式
let salt_b64 = base64_engine.encode(&salt);  // 盐值用Base64
let hash_hex = hex::encode(&hash);           // 哈希值用十六进制
let result = format!("pbkdf2:sha256:{}${}${}", iterations, salt_b64, hash_hex);

测试结果

Rust生成的哈希: pbkdf2:sha256:260000$s9Naf6jDcdJPmfe6$13c8367c8e9389ff4948e3cd9921c912e1269f574247d7f557ecc9efbcdd33bd

看起来格式对了!但是...

验证Python哈希时全部失败!

Rust验证Python哈希1: ✗ 失败
Rust验证Python哈希2: ✗ 失败
Rust验证Python哈希3: ✗ 失败

问题:格式看起来对了,但实际算法还是不匹配。

第4步:当侦探 - "我要看看Python到底怎么做的"

既然猜测不行,我决定直接看Python Werkzeug的源代码:

# 查看Werkzeug源码
import inspect
from werkzeug.security import generate_password_hash, _hash_internal

print(inspect.getsource(generate_password_hash))
print(inspect.getsource(_hash_internal))

重要发现

  1. Werkzeug用gen_salt()生成盐值
  2. 查看gen_salt()源码发现,盐值来自这个字符集: python SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
  3. 盐值是直接从这62个字符中随机选择组成的字符串!

等等,这不是Base64!这是BASE62编码!

第5步:真相大白 - "原来我一开始就理解错了!"

我写了个更详细的调试程序:

password = "test123"
werkzeug_hash = generate_password_hash(password)
parts = werkzeug_hash.split('$')

salt_str = parts[1]      # 这是个字符串,不是Base64!
hash_hex = parts[2]      # 这个是十六进制,这个没错

print(f"盐值字符串: {salt_str}")
print(f"盐值字符数: {len(salt_str)}")  # 16个字符

# 关键:盐值在PBKDF2中作为UTF-8字节使用
salt_bytes = salt_str.encode('utf-8')
print(f"盐值UTF-8字节数: {len(salt_bytes)}")  # 16字节(不是12字节!)

# 手动计算PBKDF2验证
import hashlib
manual_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt_bytes, 260000)
stored_hash = bytes.fromhex(hash_hex)

print(f"手动计算是否匹配: {manual_hash == stored_hash}")  # True!

真相终于大白

  1. 盐值不是Base64编码,而是直接的BASE62字符串
  2. 盐值长度是16个字符(我之前以为是12字节)
  3. 在PBKDF2计算中,盐值直接作为UTF-8字节使用

之前错在哪里? 我看到盐值能被Base64解码,就以为它是Base64编码的。实际上是巧合!BASE62字符集是Base64字符集的子集,所以BASE62字符串往往能被Base64解码,但这不代表它就是Base64编码的。

第6步:最终修复 - "按真相重写代码"

现在我知道真相了,重写Rust代码:

// 正确的常量
const SALT_LENGTH: usize = 16;  // 16个字符,不是字节
const SALT_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

// 正确的盐值生成(从BASE62字符集随机选择)
let mut salt_string = String::with_capacity(SALT_LENGTH);
let mut rng = OsRng;
for _ in 0..SALT_LENGTH {
    let idx = (rng.next_u32() as usize) % SALT_CHARS.len();
    salt_string.push(SALT_CHARS[idx] as char);
}

// 正确的PBKDF2计算(盐值作为UTF-8字节)
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt_string.as_bytes(), iterations, &mut hash);

// 正确的格式组装
let result = format!("pbkdf2:sha256:{}${}${}", iterations, salt_string, hex::encode(&hash));

验证逻辑也要改

// 之前(错误):尝试Base64解码盐值
let salt = base64_engine.decode(&parsed.salt).map_err(...)?;

// 现在(正确):盐值直接作为UTF-8字节
let salt = parsed.salt.as_bytes();

第7步:最终测试 - "大功告成!"

Rust测试结果

=== 端对端兼容性测试 ===
Rust生成的哈希: pbkdf2:sha256:260000$I52CAuWHjkWCsRda$1d29e8a5f1cec88bf866430ace3e1d6613183d1f88f433a024000eaebc17d97c
Rust验证自己的哈希: ✓ 成功
Rust验证Python哈希1: ✓ 成功
Rust验证Python哈希2: ✓ 成功
Rust验证Python哈希3: ✓ 成功

Python测试Rust哈希

rust_hash = "pbkdf2:sha256:260000$I52CAuWHjkWCsRda$1d29e8a5f1cec88bf866430ace3e1d6613183d1f88f433a024000eaebc17d97c"
result = check_password_hash(rust_hash, "test123")
print(f"Python验证Rust哈希: {'✓ 成功' if result else '✗ 失败'}")
# 输出: Python验证Rust哈希: ✓ 成功

🎉 完全成功!

📋 改动总结:我具体改了什么

1. 常量定义的修正

// ❌ 之前(基于错误理解)
const SALT_LENGTH: usize = 12;  // 以为是12字节

// ✅ 现在(基于正确理解)
const SALT_LENGTH: usize = 16;  // 16个字符
const SALT_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

2. 盐值生成逻辑的重写

// ❌ 之前(错误方法)
let mut salt = [0u8; SALT_LENGTH];
OsRng.fill_bytes(&mut salt);                    // 生成随机字节
let salt_b64 = base64_engine.encode(&salt);     // Base64编码

// ✅ 现在(正确方法)
let mut salt_string = String::with_capacity(SALT_LENGTH);
let mut rng = OsRng;
for _ in 0..SALT_LENGTH {
    let idx = (rng.next_u32() as usize) % SALT_CHARS.len();
    salt_string.push(SALT_CHARS[idx] as char);  // 从BASE62字符集选择
}

3. PBKDF2计算的修正

// ❌ 之前
pbkdf2_hmac::<Sha256>(password.as_bytes(), &salt, iterations, &mut hash);
//                                         ^^^^^ salt是字节数组

// ✅ 现在  
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt_string.as_bytes(), iterations, &mut hash);
//                                         ^^^^^^^^^^^^^^^^^^^ salt是字符串的UTF-8字节

4. 验证逻辑的修正

// ❌ 之前
let salt = base64_engine.decode(&parsed.salt).map_err(...)?;

// ✅ 现在
let salt = parsed.salt.as_bytes();  // 直接当作UTF-8字节使用

5. 依赖项添加

[dependencies]
hex = "0.4"  # 新增:用于十六进制编码/解码

🎓 我学到的调试技巧

1. 对比大法

  • 分解对比:不要只看整体,把哈希字符串按$分割,逐部分对比
  • 格式分析:分析每部分的长度、字符类型、可能的编码方式
  • 逐步验证:每次只修改一个假设,立即测试结果

2. 源码是真理

  • 不要猜测:当文档不清楚时,直接看源代码
  • Python神技:用inspect.getsource()查看函数源码
  • 理解本质:不要只看表面现象,要理解背后的实现逻辑

3. 手动验证

  • 模拟算法:用Python手动实现相同的步骤
  • 中间结果:打印每一步的中间结果进行对比
  • 交叉验证:用A验证B生成的结果,用B验证A生成的结果

4. 小步快跑

  • 单一修改:每次只改一个地方
  • 立即测试:改完马上测试,不要攒一堆修改再测
  • 记录过程:记下每次修改和结果,避免重复犯错

🤔 我犯的错误和教训

错误1:基于假设编程

我以为:盐值是Base64编码,因为它能被Base64解码
实际:它是BASE62字符串,只是恰好能被Base64解码
教训:不要因为某种解码方法有效就认为数据就是这样编码的

错误2:忽视字符集差异

我以为:Base64和BASE62差不多
实际:BASE62不包含+/=这些字符,字符集完全不同
教训:看似相近的编码方式可能有本质差异

错误3:混淆字节和字符

我以为:16个字符的UTF-8编码还是16字节
实际:ASCII字符的UTF-8编码确实是1字节1字符,但概念要区分清楚
教训:在处理编码时要明确区分字符数和字节数

错误4:相信第一印象

我看到:Rust输出有==,Python没有,就认为编码方式不同
实际:这确实说明编码不同,但我对Python的编码方式判断错了
教训:第一印象可能指出问题方向,但具体分析要更深入

🏆 最终成果验证

格式完全匹配

Python格式pbkdf2:sha256:260000$[16个BASE62字符]$[64个十六进制字符]
Rust格式pbkdf2:sha256:260000$[16个BASE62字符]$[64个十六进制字符]
完全一致

双向兼容测试

  • ✅ Rust验证自己生成的哈希:成功
  • ✅ Rust验证Python生成的哈希:成功
  • ✅ Python验证Rust生成的哈希:成功
  • ✅ 多轮交叉测试:全部通过

性能和安全性

  • ✅ 相同的PBKDF2-HMAC-SHA256算法
  • ✅ 相同的260,000次迭代
  • ✅ 相同的盐值长度和随机性
  • ✅ 相同的安全强度

💡 给小白的建议

  1. 遇到兼容性问题别慌:这很正常,特别是涉及密码学的时候
  2. 细节决定成败:密码学容不得一点马虎,每个字节都很重要
  3. 源码比文档靠谱:当文档模糊时,源码永远不会撒谎
  4. 测试要全面:不仅要测试自己的实现,还要测试与目标系统的兼容性
  5. 记录调试过程:好记性不如烂笔头,记录能帮你避免重复踩坑

📁 项目文件结构

密码哈希兼容性项目/
├── Cargo.toml                    # Rust项目配置文件
├── src/
│   ├── main.rs                  # 主测试程序
│   └── password.rs              # 密码哈希核心实现
├── password.py                  # Python参考实现
├── debug_werkzeug_fixed.py      # 调试分析程序
├── final_test.py                # 最终验证程序
└── debugging_experience.md      # 这份调试总结(你正在看的文件)

这次调试让我深刻体会到:程序员不仅要会写代码,更要会调试代码。遇到问题时,保持好奇心,一步步深入分析,总能找到真相!

Rust与Python Werkzeug密码哈希兼容性调试经验总结

项目背景

任务: 让Rust和Python能够生成相同格式的密码哈希,互相验证对方的结果。
目标: 实现端对端兼容 - Python生成的哈希,Rust能验证通过;Rust生成的哈希,Python也能验证通过。
起点: 有一个Rust版本的密码哈希实现,但与Python Werkzeug库不兼容。

详细调试过程 - 像侦探破案一样

第一步:发现问题 - "这两个输出怎么长得不一样?"

当我第一次同时运行Python和Rust版本时,立马发现了问题:

Python输出(Werkzeug库):

pbkdf2:sha256:260000$HehzXdgPCaIV50Iz$57bd6863f405f2001199a90ad9df238a8a2df722d45b32dee8aaf3383fe02bb9

Rust输出(我的实现):

pbkdf2:sha256:260000$BS3zWkNu7sWZ42VS24r0Bg==$7DNjGGzjCgCOFavqpycM//U8YWhOmt5kHGRS0U6djtA=

我的第一反应:"卧槽,这俩格式完全不一样啊!"

仔细观察发现

  1. 前面的算法部分一样:pbkdf2:sha256:260000
  2. 但盐值和哈希值部分完全不同:
    • Python的盐值:HehzXdgPCaIV50Iz(16个字符,看起来像Base64)
    • Rust的盐值:BS3zWkNu7sWZ42VS24r0Bg==(24个字符,明显是Base64)
    • Python的哈希值:57bd6863f405f200...(64个字符,看起来像十六进制)
    • Rust的哈希值:7DNjGGzjCgCOFa...(44个字符,明显是Base64)

初步判断:"看起来编码格式不对,Python用的不是Base64?"

第二步:分析编码格式 - "让我看看你们到底是什么编码"

为了搞清楚Python到底用的什么编码,我写了一个专门的分析程序:

# check_encoding.py - 我的第一个调试程序
from werkzeug.security import generate_password_hash
import base64

h = generate_password_hash('test123')
parts = h.split('$')
salt = parts[1]      # 盐值部分
hash_val = parts[2]  # 哈希值部分

print('完整哈希:', h)
print('盐值:', salt)
print('哈希值:', hash_val)

# 检查盐值是不是Base64
print('\n=== 盐值编码检查 ===')
print('盐值长度:', len(salt))
try:
    salt_decoded = base64.b64decode(salt + '==')  # 加padding试试
    print('盐值base64解码长度:', len(salt_decoded))
    print('盐值base64解码成功')
except:
    print('盐值不是base64格式')

# 检查哈希值是不是十六进制
print('\n=== 哈希值编码检查 ===')  
print('哈希值长度:', len(hash_val))
try:
    hash_bytes = bytes.fromhex(hash_val)
    print('哈希值hex解码长度:', len(hash_bytes))
    print('哈希值是十六进制格式')
except:
    print('哈希值不是十六进制格式')

运行结果让我震惊了

完整哈希: pbkdf2:sha256:260000$9MmsCROi0ec7yUxo$3c1211611b360f607cfd9acacc360b0a00e76755c26972d0a1c4502187bda77d
盐值: 9MmsCROi0ec7yUxo
哈希值: 3c1211611b360f607cfd9acacc360b0a00e76755c26972d0a1c4502187bda77d

=== 盐值编码检查 ===
盐值长度: 16
盐值base64解码长度: 12
盐值base64解码成功

=== 哈希值编码检查 ===
哈希值长度: 64
哈希值hex解码长度: 32
哈希值是十六进制格式

我的发现

  • 盐值确实是Base64编码,但解码后只有12字节(不是我以为的16字节)
  • 哈希值确实是十六进制编码,64个字符 = 32字节

我当时想:"好,那我知道怎么修改Rust了!"

3. 第一次修正尝试

基于上述发现,修改Rust代码:

// 错误的假设:盐值是Base64编码的12字节
const SALT_LENGTH: usize = 12;  // 从16改为12

// 修改哈希生成逻辑
let salt_b64 = base64_engine.encode(&salt);  // Base64盐值
let hash_hex = hex::encode(&hash);           // 十六进制哈希

结果: 格式看起来对了,但验证Python哈希时全部失败。

4. 深入源码分析阶段

使用Python内省功能查看Werkzeug源码:

import inspect
print(inspect.getsource(generate_password_hash))
print(inspect.getsource(_hash_internal))

关键发现:

  1. Werkzeug使用gen_salt()生成盐值
  2. 盐值字符集是BASE62: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
  3. 盐值在PBKDF2中直接作为UTF-8字节使用

5. 真相大白时刻

创建了修正版调试程序:

# debug_werkzeug_fixed.py
salt_str = parts[1]  # 盐值是字符串,不是Base64!
salt_bytes = salt_str.encode('utf-8')  # 直接UTF-8编码

# 手动计算PBKDF2
manual_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt_bytes, 260000)
print(f"是否匹配: {manual_hash == hash_bytes}")  # True!

真相:

  • 盐值不是Base64编码,而是直接的BASE62字符串
  • 长度是16个字符(不是12字节)
  • 在PBKDF2计算中作为UTF-8字节使用

6. 最终修正

基于真相,完全重写Rust实现:

// 正确的常量定义
const SALT_LENGTH: usize = 16;  // 16个字符
const SALT_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

// 正确的盐值生成
let mut salt_string = String::with_capacity(SALT_LENGTH);
let mut rng = OsRng;
for _ in 0..SALT_LENGTH {
    let idx = (rng.next_u32() as usize) % SALT_CHARS.len();
    salt_string.push(SALT_CHARS[idx] as char);
}

// 正确的PBKDF2计算
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt_string.as_bytes(), iterations, &mut hash);

// 正确的格式组装
let result = format!("pbkdf2:sha256:{}${}${}", iterations, salt_string, hash_hex);

主要改动总结

1. 常量修正

// 之前(错误)
const SALT_LENGTH: usize = 12;  // 字节数

// 之后(正确)  
const SALT_LENGTH: usize = 16;  // 字符数
const SALT_CHARS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

2. 盐值生成逻辑

// 之前(错误):生成随机字节,然后Base64编码
let mut salt = [0u8; SALT_LENGTH];
OsRng.fill_bytes(&mut salt);
let salt_b64 = base64_engine.encode(&salt);

// 之后(正确):从BASE62字符集生成字符串
let mut salt_string = String::with_capacity(SALT_LENGTH);
for _ in 0..SALT_LENGTH {
    let idx = (rng.next_u32() as usize) % SALT_CHARS.len();
    salt_string.push(SALT_CHARS[idx] as char);
}

3. 哈希验证逻辑

// 之前(错误):尝试Base64解码盐值
let salt = base64_engine.decode(&parsed.salt).map_err(...)?;

// 之后(正确):盐值直接作为UTF-8字节
let salt = parsed.salt.as_bytes();

4. 依赖项添加

[dependencies]
hex = "0.4"  # 新增:用于十六进制编码/解码

调试技巧和经验

1. 逐步对比法

  • 不要一次性修改太多
  • 每次修改后立即测试
  • 对比Python和Rust的每个组件

2. 深入源码

  • 当文档不清楚时,直接查看源代码
  • 使用Python的inspect模块查看源码
  • 理解实现细节而不是依赖假设

3. 创建详细的调试程序

  • 分解每个步骤进行验证
  • 打印中间结果进行对比
  • 手动重现算法逻辑

4. 交叉验证

  • 用Python验证Rust生成的哈希
  • 用Rust验证Python生成的哈希
  • 确保双向兼容

最终结果验证

成功指标

✅ Rust验证自己的哈希: 成功
✅ Rust验证Python哈希: 全部成功
✅ Python验证Rust哈希: 成功
✅ 格式完全匹配: pbkdf2:sha256:260000$[16个BASE62字符]$[64个十六进制字符]

性能对比

  • 两个实现使用相同的PBKDF2-HMAC-SHA256算法
  • 相同的迭代次数(260,000次)
  • 相同的盐值长度和格式
  • 完全兼容的哈希输出

关键启示

  1. 不要假设编码格式: 总是通过实验验证编码方式
  2. 源码是最权威的文档: 当遇到不明确的地方,查看源码
  3. 逐步调试: 复杂问题要分解成小步骤逐一解决
  4. 双向测试: 兼容性需要双向验证
  5. 保持耐心: 密码学相关的调试往往需要多次尝试

文件结构

├── Cargo.toml              # Rust项目配置
├── src/
│   ├── main.rs            # 主测试程序
│   └── password.rs        # 密码哈希实现
├── password.py            # Python参考实现
├── debug_werkzeug_fixed.py # 调试程序
├── final_test.py          # 最终验证程序
└── debugging_experience.md # 本文档

这次调试经历证明了仔细分析、逐步验证和源码研究在解决兼容性问题中的重要性。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...