Showing Posts From

密码学

BIP39 助记词会不会重复:数学上不会,现实中被坑的另有其人

有人问过我一个纠结的问题:用钱包 APP 生成助记词,会不会跟别人重复导致钱包被盗? 数学上算一下就知道,重复的概率低到根本不用担心;真正被盗的原因,几乎都是别的坑。 BIP39 生成流程 12 词助记词的生成,标准流程只有三步: 1. 从系统 CSPRNG 取 128 位熵 import secrets entropy = secrets.token_bytes(16) # 16 字节 = 128 位secrets 底层用的是操作系统提供的密码学安全随机源:Windows:BCryptGenRandom Linux:/dev/urandom macOS:SecRandomCopyBytes2. 拼上 4 位校验和 对 128 位熵做 SHA-256,取前 4 位挂到熵尾部,凑成 132 位: 128 位熵 + 4 位校验和 = 132 位3. 每 11 位查一次词表 BIP39 词表有 2^11 = 2048 个词。132 位 / 11 = 12 个词。 10010101100 → abandon 11100101010 → ability ...碰撞概率有多低 12 词的熵是 128 位,等于 3.4 × 10³⁸ 种可能。用生日悖论算一下极端情况: 假设全球 100 亿人,每人生成 100 万个钱包,一共 10¹⁶ 个: 碰撞概率 ≈ N² / (2 × 2¹²⁸) ≈ 10³² / 6.8 × 10³⁸ ≈ 1.5 × 10⁻⁷约 0.000015%,还是极端假设。真实世界远远达不到这个量级。24 词是 256 位熵,那更是天文数字。 只要用正规钱包 + 系统 CSPRNG 生成,重复被盗几乎不可能。 真被盗的常见姿势 1. 随机数不安全 自己造轮子最容易出问题: import random random.seed(time.time()) # 危险Python 的 random 是伪随机 + 可预测种子。攻击者知道大概时间戳,就能枚举出你所有可能的助记词。 永远用 secrets 或 os.urandom,不用 random。 2. "幸运数字"当种子 有人觉得自己的生日、手机号能当种子更好记: seed = "5201314" entropy = sha256(seed)这种熵严重不足。攻击者跑一遍 0 到 9,999,999,999 就把你的钱包翻出来了。任何"人可以记住"的种子都不够安全。 3. 脑钱包 想一句话当助记词: iloveyou123彩虹表早就把常见短语算光了,这种钱包上链一分钟就被扫走。 4. 假钱包 APP 流程: APP 生成助记词 → 悄悄上传服务器 → 等你存币 → 转走来路不明的浏览器插件、"新币空投工具"是重灾区。装钱包只从官网或 App Store。 5. 助记词泄露截屏保存到手机相册(云同步就完了) 记在便笺、微信自己的对话 输入到"辅助工具"网页正确做法:只离线纸质备份,多份异地存放。 安全生成的一行代码 用 bip_utils: from bip_utils import Bip39MnemonicGenerator, Bip39WordsNummnemonic = Bip39MnemonicGenerator().FromWordsNumber(Bip39WordsNum.WORDS_NUM_12) print(mnemonic)内部就是 CSPRNG + 128 位熵 + SHA-256 校验,标准 BIP39 流程。 一句话总结 担心助记词重复是想多了;担心自己不用 CSPRNG、拿脑钱包和假钱包 APP 是应该的。 只要用正规钱包生成 + 离线纸质备份,安全性远高于随机重复这种理论问题。

BIP39 助记词生成原理与 Python 实现

BIP39 定义了一套将随机数编码为可记忆单词的标准,大多数钱包(MetaMask、Phantom、imToken)都支持。 BIP39 原理生成 128/160/192/224/256 位随机熵 取熵的 SHA-256 哈希前 len/32 位作为校验位,拼接到熵末尾 每 11 位映射一个 BIP39 词表单词(共 2048 个词) 最终得到 12/15/18/21/24 个单词熵长度 校验位 总位数 单词数128 bit 4 bit 132 bit 12256 bit 8 bit 264 bit 24Python 实现(bip-utils) pip install bip-utils从助记词派生以太坊私钥和地址 from bip_utils import ( Bip39SeedGenerator, Bip39MnemonicValidator, Bip44, Bip44Coins, Bip44Changes, )mnemonic = "word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12"# 可选:验证助记词是否合法 if not Bip39MnemonicValidator().IsValid(mnemonic): raise ValueError("助记词无效")# 助记词 → 种子(可选 passphrase) seed = Bip39SeedGenerator(mnemonic).Generate(passphrase="")# 种子 → BIP44 派生路径 m/44'/60'/0'/0/0 ctx = Bip44.FromSeed(seed, Bip44Coins.ETHEREUM) addr_ctx = ( ctx.Purpose() .Coin() .Account(0) .Change(Bip44Changes.CHAIN_EXT) .AddressIndex(0) )private_key_hex = addr_ctx.PrivateKey().Raw().ToHex() address = addr_ctx.PublicKey().ToAddress()print(f"私钥: {private_key_hex}") print(f"地址: {address}")派生多个地址 for i in range(5): addr_ctx = ( ctx.Purpose() .Coin() .Account(0) .Change(Bip44Changes.CHAIN_EXT) .AddressIndex(i) ) print(f"[{i}] {addr_ctx.PublicKey().ToAddress()}")Solana(BIP44 Coin 501) from bip_utils import Bip44Coinsctx = Bip44.FromSeed(seed, Bip44Coins.SOLANA) sol_ctx = ctx.Purpose().Coin().Account(0).Change(Bip44Changes.CHAIN_EXT).AddressIndex(0) print(f"Solana 地址: {sol_ctx.PublicKey().ToAddress()}")手动验证 BIP39 校验和(纯 Python) import hashlibdef validate_bip39(mnemonic: str, wordlist_path: str) -> bool: with open(wordlist_path) as f: words = f.read().splitlines() word_index = {w: i for i, w in enumerate(words)} mnemonic_words = mnemonic.strip().split() bits = "" for word in mnemonic_words: if word not in word_index: return False bits += format(word_index[word], "011b") # 最后 len/33 位是校验位 cs_len = len(bits) // 33 entropy_bits = bits[:-cs_len] checksum_bits = bits[-cs_len:] # 还原熵字节 entropy = int(entropy_bits, 2).to_bytes(len(entropy_bits) // 8, "big") # SHA256 前 cs_len 位 h = hashlib.sha256(entropy).digest() expected_cs = format(h[0], "08b")[:cs_len] return checksum_bits == expected_cs注意事项助记词等同于钱包全部资产的控制权,不能在任何联网代码中明文存储 生产环境派生私钥只能在离线机器或 HSM 上操作 仅用于工具开发/验证时,建议使用测试网地址,不要导入真实资产

Python 实现 PBKDF2 + AES-256-GCM 加密存储

PBKDF2 + AES-GCM 是本地加密存储(钱包 vault、配置文件保护)的标准方案:密码经 PBKDF2 派生出密钥,AES-GCM 提供加密和完整性验证。 加密流程 Password ↓ PBKDF2-HMAC-SHA512(iterations=600000, salt=随机16字节) ↓ 32 字节 AES Key ↓ AES-256-GCM(nonce=随机16字节) ↓ Ciphertext + Tag安装依赖 pip install pycryptodome完整实现 import json import os import base64 from hashlib import pbkdf2_hmac from Crypto.Cipher import AESPBKDF2_ITERATIONS = 600_000 # NIST 2023 推荐最低值def derive_key(password: str, salt: bytes) -> bytes: return pbkdf2_hmac( "sha512", password.encode("utf-8"), salt, PBKDF2_ITERATIONS, dklen=32, # AES-256 )def encrypt(password: str, plaintext: str) -> dict: salt = os.urandom(16) nonce = os.urandom(16) key = derive_key(password, salt) cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) ciphertext, tag = cipher.encrypt_and_digest(plaintext.encode("utf-8")) return { "salt": base64.b64encode(salt).decode(), "nonce": base64.b64encode(nonce).decode(), "ciphertext": base64.b64encode(ciphertext).decode(), "tag": base64.b64encode(tag).decode(), }def decrypt(password: str, vault: dict) -> str: salt = base64.b64decode(vault["salt"]) nonce = base64.b64decode(vault["nonce"]) ciphertext = base64.b64decode(vault["ciphertext"]) tag = base64.b64decode(vault["tag"]) key = derive_key(password, salt) cipher = AES.new(key, AES.MODE_GCM, nonce=nonce) plaintext = cipher.decrypt_and_verify(ciphertext, tag) return plaintext.decode("utf-8")使用示例 # 加密 secret = '{"privateKey": "0xabcd..."}' vault = encrypt("my_strong_password", secret)# 存储为 JSON with open("vault.json", "w") as f: json.dump(vault, f, indent=2)# 解密 with open("vault.json") as f: vault = json.load(f)plaintext = decrypt("my_strong_password", vault) print(plaintext)vault.json 格式: { "salt": "base64...", "nonce": "base64...", "ciphertext": "base64...", "tag": "base64..." }为什么选 AES-GCM 而非 CBC模式 加密 完整性验证 推荐AES-CBC ✅ ❌(需额外 HMAC) 不推荐新项目AES-GCM ✅ ✅(内置 Tag) ✅ 推���AES-CTR ✅ ❌ 需配合 HMACGCM 模式同时提供加密和认证,decrypt_and_verify 会在解密前验证 Tag,密文被篡改时抛出 ValueError。 关键参数说明 PBKDF2 迭代次数:越高越安全,NIST 2023 建议 SHA-512 至少 210,000 次,MetaMask 等钱包用 600,000 次。代价是派生速度变慢(约 0.5~2 秒),对正常用户不感知,但让暴力破解成本提高数十万倍。 salt 唯一性:每次加密生成新的随机 salt,防止相同密码产生相同密钥(彩虹表攻击)。salt 不需要保密,公开存储即可。 nonce 唯一性:GCM 的 nonce 绝对不能重用于同一密钥,否则 GCM 的安全性完全崩溃。每次加密随机生成是最安全的做法。 密码错误时的行为 try: plaintext = decrypt("wrong_password", vault) except ValueError: print("密码错误或数据被篡改")decrypt_and_verify 验证 Tag 失败时抛出 ValueError,不会泄漏任何明文信息。

Python str.translate 自定义 Base64:字符替换编解码原理

在逆向接口参数时,经常遇到这类代码: b64 = base64.b64encode(data.encode("utf-8")).decode("ascii") return b64.translate(TRANS)b64.translate(TRANS) 是 Python 字符串的内置 translate() 方法,不是某个库的 API。 translate() 做了什么 先 Base64 编码,再按照 TRANS 映射表替换字符。 translate() 一次可以替换多个字符,比链式 replace() 更高效: TRANS = str.maketrans({ "+": "-", "/": "_", })"a+b/c=".translate(TRANS) # → "a-b_c="TRANS 的常见形式 替换少量字符 TRANS = str.maketrans({ "+": "-", "/": "_", })这是标准的 URL Safe Base64 变体(RFC 4648),把 +/ 换成 -_,避免 URL 编码问题。 整体字符映射(自定义 Base64) src = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" dst = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm0123456789-_"TRANS = str.maketrans(src, dst)这是在对整个 Base64 字符集做轮换映射,常用于接口参数混淆。 解码:把映射反过来 编码是 src → dst,解码就是 dst → src: REVERSE_TRANS = str.maketrans(dst, src)完整编解码实现 import base64src = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" dst = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm0123456789-_"TRANS = str.maketrans(src, dst) REVERSE_TRANS = str.maketrans(dst, src)def encode(data: str) -> str: b64 = base64.b64encode(data.encode()).decode() return b64.translate(TRANS)def decode(data: str) -> str: raw = data.translate(REVERSE_TRANS) return base64.b64decode(raw).decode()x = encode("hello world") print(x) # 输出混淆后的字符串 print(decode(x)) # → hello world简单的 URL Safe Base64 变体 如果 TRANS 只替换 +/=,编解码更简单: import base64def url_safe_encode(data: str) -> str: b64 = base64.b64encode(data.encode()).decode() return b64.translate(str.maketrans({"+": "-", "/": "_", "=": ""}))def url_safe_decode(data: str) -> str: # 补齐 padding padded = data + "=" * (-len(data) % 4) raw = padded.translate(str.maketrans({"-": "+", "_": "/"})) return base64.b64decode(raw).decode()逆向时如何确认 TRANS 内容 遇到 b64.translate(TRANS) 时,找到 TRANS 的定义:如果是 str.maketrans(src, dst):把 dst 作为新的 src,src 作为新的 dst,即可反向还原 如果是 str.maketrans({...}):把字典的键值互换即可很多登录接口、签名参数都会用这种方式,目的是避免 +/= 在 URL 中被转义,或者简单混淆防止直接调用。