今年看到很多关于「抢劫加密货币」的新闻:
针对于加密货币实施的犯罪不仅存在于比特世界,在原子世界也变得越来越频繁。今天读到《精通比特币》第三版「种子和恢复码」部分,结合自己之前经历过的钱包安全事件,脑海中出现很多经典的犯罪场景,针对于这些场景,我们可以采用哪些技术手段来保证自己的资产安全呢?
💡 在此之前,你需要知道的:
种子技术是一种在加密货币钱包中用于生成私钥和公钥的技术。通过生成一个随机数作为种子,再利用该种子计算出多个私钥和公钥对,从而实现钱包的生成和恢复。
恢复码是一组用于恢复钱包的助记词,是种子的可读形式。常见的恢复码由12到24个单词组成(如 BIP39),这是通过将种子编码为单词序列得到的。用户可以手动记录这组单词,用于在钱包丢失或损坏时恢复钱包。
加入你遇到了劫匪,劫匪要求你必须交出比特币钱包的恢复码,你不想暴露自己的全部资产,但又要交出一些钱来保命。
不能让劫匪知道你到底有多少钱。
使用 BIP39 和 SLIP39 的合理否认机制。在 BIP39 和 SLIP39 恢复码方案中,用户可以设置一个「主恢复码」以及可选的口令。不同的口令会生成不同的密钥树,因此 Alice 可以设置一个包含少量资金的备用口令,用于应对被迫交出恢复码的情况。这样,歹徒即便拿到恢复码和备用口令,也只能获取少量资产。
def generate_seed(mnemonic_phrase, passphrase=""):
"""
Generate seed from mnemonic phrase and optional passphrase using PBKDF2.
"""
# BIP39 defines the salt as "mnemonic" + passphrase
salt = "mnemonic" + passphrase
# Generate the seed using PBKDF2-HMAC-SHA512 with 2048 iterations
seed = hashlib.pbkdf2_hmac("sha512", mnemonic_phrase.encode(), salt.encode(), 2048)
return seed.hex()
# 初始化 BIP39 助记词生成器
mnemo = Mnemonic("english")
# 生成助记词
mnemonic_phrase = mnemo.generate(strength=128) # 12 个助记词
print(f"助记词: {mnemonic_phrase}")
# 使用不同的口令生成种子
passphrase_1 = "main_passphrase"
passphrase_2 = "secondary_passphrase"
# 生成不同的种子
seed1 = generate_seed(mnemonic_phrase, passphrase_1)
seed2 = generate_seed(mnemonic_phrase, passphrase_2)
print(f"种子 (口令1): {seed1}")
print(f"种子 (口令2): {seed2}")
# 验证不同口令生成的种子是否相同
if seed1 != seed2:
print("不同口令生成了不同的种子。")
else:
print("不同口令生成了相同的种子(请检查代码或口令设置)。")
打印示例
助记词: dose grid token call output jungle spot monster ozone spread bunker canal
种子 (口令1): 5a7a35f7a6e08d7a7efb93f9120d94bf7ffb3a7f82e84e2334fa6310a7f216b20b6fc536...
种子 (口令2): 9e40e3d60ff15a54d37dabc8474825c92a5b830d842fbdb77e5fbe5a6b35b7123c973c6...
你全球旅行到了一个对加密货币不友好的国家。入境检查时,当地安全人员要求他上交钱包的恢复码和私钥,但你并不想这么做。
携带的私钥无法完整恢复钱包
使用 SLIP39(分布式恢复码),可以将种子恢复码分成多个分片,存放在不同位置。比如我们可以设置 2 个分片,自身携带一个,要求比如两个分片在一起时才可以恢复钱包。
# 初始化 BIP39 助记词生成器
mnemo = Mnemonic("english")
# 生成助记词作为种子
mnemonic_phrase = mnemo.generate(strength=128) # 生成 12 个助记词
print(f"原始助记词: {mnemonic_phrase}")
# 将助记词分成两个分片,设定 2 个分片才能恢复
mnemonics = generate_mnemonics(master_secret=mnemonic_phrase.encode(), group_threshold=1, groups=[(2, 2)])
print("生成的分片:")
for idx, part in enumerate(mnemonics, start=1):
print(f"分片 {idx}: {part}")
# 恢复过程,使用两个分片组合恢复原始种子
recovered_phrase = combine_mnemonics(mnemonics)
print(f"恢复的助记词: {recovered_phrase.decode()}")
# 验证恢复的助记词是否和原始助记词一致
if recovered_phrase.decode() == mnemonic_phrase:
print("成功恢复原始助记词!")
else:
print("恢复失败,助记词不匹配。")
打印示例
原始助记词: stable whisper section invite risk embrace stand educate snack banner mirror result
生成的分片:
分片 1: armed strong acquire jungle plug mutual letter mother door use field mistake
分片 2: quick trip modify island food reduce length renew warm fortune green square
恢复的助记词: stable whisper section invite risk embrace stand educate snack banner mirror result
💡 原始助记词并不是分片 1 和分片 2 的简单集成。Shamir’s Secret Sharing (SSSS) 算法背后的原理并不是将原始助记词简单地拆分成多个部分,而是通过数学方法将原始数据(种子或助记词)转换成多个唯一且独立的分片。每个分片都包含一部分恢复原始数据所需的信息,但单独一个分片本身不能直接恢复原始助记词。因此:
每个分片都包含部分信息,但单独无法还原原始助记词;
多个分片联合才能进行插值重建,进而恢复原始种子。
你和几个朋友一起开了一家公司,希望共同管理 BTC 资产。但不希望任何人单独获得恢复码。
任何一个恢复码都无法单独获得所有资产。
SLIP39 或 Codex32 的分布式密钥分片。可以将种子恢复码分成多个分片(如 8 个),并将不同的分片分给朋友,同时设定恢复门限为 5 个分片。这样,即使有部分分片丢失或泄露,攻击者仍无法获得完整的恢复码。
💡 Codex32 是一种新的、仍在开发中的恢复码方案,目前尚未有正式的 Python 库或标准实现,但我们可以根据分布式密钥分片的原理,模拟 Codex32 的工作方式。Codex32 类似于 Shamir 的秘密共享方案,但它设计为可以通过物理方式(如纸质打印和手动验证)来生成和验证密钥分片。具体逻辑可以参考 SLIP39 代码片段。
当然,目前很多 EVM 链或图灵完备的公链大多数会采用多签钱包或 AA 钱包的方式来解决此类问题。但由此可能会引申出一个新的问题 - 如果双方各掌握 50% 的权限,那么当意见出现分歧时,很容易陷入僵局。
你的手机被盗,而且手机中存放着恢复码的照片。
恢复码照片泄露导致资金被盗。
**Aezeed(口令身份验证)。**Aezeed 的口令验证可以防止用户输入错误密码,而攻击者即便拥有恢复码,若不知道正确口令,依然无法生成有效的密钥。
使用 Aezeed 方案,你的恢复码是加密的,并且包含口令身份验证。在恢复钱包时,如果输入错误的密码,会立即显示验证错误,不会生成错误的密钥树。
从上述描述中,我们可能感觉 Aezeed 和 BIP39、SLIP39 在功能上有一定相似性,特别是在使用口令生成不同密钥树方面,但 BIP39 和 SLIP39 的设计旨在生成单一恢复码或分片恢复码,通过可选口令来生成不同的密钥树,但缺少验证机制。输入错误口令时,BIP39 和 SLIP39 仍会生成有效密钥树,可能导致用户误解和丢失资金。但 Aezeed 会通过校验,告知用户输入的是错误的口令。
不仅如此 Aezeed 相比 BIP39 和 SLIP39 还有一些独特的特性,包括其设计目的是为 闪电网络提供更好的支持,并且包含更多的安全特性,如口令验证和钱包生日信息,以及多层版本控制。这些特性使得 Aezeed 更适合复杂应用场景,比如高频、实时的交易需求。
由于目前 Aezeed 还没有正式的库实现,但我们可以模拟其通过口令生成不同种子的流程,同时添加口令验证,以确保在用户输入错误口令时返回错误信息。
def generate_seed_with_aezeed(mnemonic_phrase, passphrase=""):
"""
Generate an Aezeed-style seed from mnemonic phrase and optional passphrase.
Adds basic password validation by checking a derived hash.
"""
# Aezeed 风格的种子生成函数
salt = "mnemonic" + passphrase
seed = hashlib.pbkdf2_hmac("sha512", mnemonic_phrase.encode(), salt.encode(), 2048)
# 验证哈希值的前 8 字节作为基本校验
checksum = seed[:8]
return seed.hex(), checksum
def verify_passphrase(mnemonic_phrase, passphrase, expected_checksum):
"""
Verify if the provided passphrase generates the expected checksum.
"""
_, checksum = generate_seed_with_aezeed(mnemonic_phrase, passphrase)
return checksum == expected_checksum
# 初始化 BIP39 助记词生成器
mnemo = Mnemonic("english")
# 生成助记词作为种子
mnemonic_phrase = mnemo.generate(strength=128) # 生成 12 个助记词
print(f"助记词: {mnemonic_phrase}")
# 使用主口令生成种子
main_passphrase = "correct_password"
seed, checksum = generate_seed_with_aezeed(mnemonic_phrase, main_passphrase)
print(f"主种子: {seed}")
print(f"校验码: {checksum.hex()}")
# 测试其他口令是否会产生不同种子,并进行验证
test_passphrase = "wrong_password"
is_valid = verify_passphrase(mnemonic_phrase, test_passphrase, checksum)
print(f"口令验证结果(错误口令): {'有效' if is_valid else '无效'}")
# 验证正确口令
is_valid = verify_passphrase(mnemonic_phrase, main_passphrase, checksum)
print(f"口令验证结果(正确口令): {'有效' if is_valid else '无效'}")
打印示例
助记词: stable whisper section invite risk embrace stand educate snack banner mirror result
主种子: 5a7a35f7a6e08d7a7efb93f9120d94bf7ffb3a7f82e84e2334fa6310a7f216b20b6fc536...
校验码: 5a7a35f7a6e0
口令验证结果(错误口令): 无效
口令验证结果(正确口令): 有效
你担心黑客攻击威胁到恢复码的安全,因此希望找到一种完全离线的存储方法。
离线存储且可分片(防止单一丢失)
Codex32 的手动离线分布式存储。Codex32 恢复码方案允许用户通过打印说明书、剪刀等物理方式生成分片恢复码,避免电子设备参与。Eve 可以将恢复码分片离线打印并分散存放,确保恢复码不会受到网络攻击威胁。