# 逃出生天：不同威胁下的钱包恢复策略

By [Simon 写字的地方](https://paragraph.com/@simon-2) · 2024-10-30

---

前言
==

今年看到很多关于「抢劫加密货币」的新闻：

*   [乌克兰男出门遇害埋尸森林，钱包 3 枚BTC被匪徒抢走](https://www.blocktempo.com/ukrainian-bitcoin-robbery-case/)
    
*   [暴力集团锁定加密货币持有者，全美多地爆「入室抢劫」比特币](https://www.blocktempo.com/florida-man-convicted-in-btc-theft-home-invasions/)
    
*   [新加坡「币圈聚会打牌」遭持枪抢劫！ 11人被迫转出加密钱包，损失超过1亿台币](https://www.blocktempo.com/crypto-meeting-turn-into-robbery/)
    
*   …
    

针对于加密货币实施的犯罪不仅存在于比特世界，在原子世界也变得越来越频繁。今天读到《精通比特币》第三版「种子和恢复码」部分，结合自己之前经历过的钱包安全事件，脑海中出现很多经典的犯罪场景，针对于这些场景，我们可以采用哪些技术手段来保证自己的资产安全呢？

> 💡 在此之前，你需要知道的：
> 
> *   **种子技术**是一种在加密货币钱包中用于生成私钥和公钥的技术。通过生成一个随机数作为种子，再利用该种子计算出多个私钥和公钥对，从而实现钱包的生成和恢复。
>     
> *   **恢复码**是一组用于恢复钱包的助记词，是种子的可读形式。常见的恢复码由12到24个单词组成（如 BIP39），这是通过将种子编码为单词序列得到的。用户可以手动记录这组单词，用于在钱包丢失或损坏时恢复钱包。
>     

假设场景
====

场景 1：劫持威胁
---------

### 情景描述

加入你遇到了劫匪，劫匪要求你必须交出比特币钱包的恢复码，你不想暴露自己的全部资产，但又要交出一些钱来保命。

### 困点

不能让劫匪知道你到底有多少钱。

### 解决方案

使用 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...
    

场景 2：跨国旅行遭遇检查
-------------

### 情景描述

你全球旅行到了一个对加密货币不友好的国家。入境检查时，当地安全人员要求他上交钱包的恢复码和私钥，但你并不想这么做。

### 困点

携带的私钥无法完整恢复钱包

### 解决方案

使用 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) 算法背后的原理并不是将原始助记词简单地拆分成多个部分，而是通过数学方法将原始数据（种子或助记词）转换成多个**唯一且独立的分片**。每个分片都包含一部分恢复原始数据所需的信息，但单独一个分片本身不能直接恢复原始助记词。因此：
> 
> *   **每个分片都包含部分信息，但单独无法还原原始助记词；**
>     
> *   **多个分片联合才能进行插值重建**，进而恢复原始种子。
>     

场景 3：监守自盗
---------

### 情景描述

你和几个朋友一起开了一家公司，希望共同管理 BTC 资产。但不希望任何人单独获得恢复码。

### 困点

任何一个恢复码都无法单独获得所有资产。

### 解决方案

SLIP39 或 Codex32 的分布式密钥分片。可以将种子恢复码分成多个分片（如 8 个），并将不同的分片分给朋友，同时设定恢复门限为 5 个分片。这样，即使有部分分片丢失或泄露，攻击者仍无法获得完整的恢复码。

> 💡 Codex32 是一种新的、仍在开发中的恢复码方案，目前尚未有正式的 Python 库或标准实现，但我们可以根据分布式密钥分片的原理，模拟 Codex32 的工作方式。Codex32 类似于 Shamir 的秘密共享方案，但它设计为可以通过物理方式（如纸质打印和手动验证）来生成和验证密钥分片。具体逻辑可以参考 SLIP39 代码片段。

当然，目前很多 EVM 链或图灵完备的公链大多数会采用多签钱包或 AA 钱包的方式来解决此类问题。但由此可能会引申出一个新的问题 - 如果双方各掌握 50% 的权限，那么当意见出现分歧时，很容易陷入僵局。

场景 4：设备丢失或被盗
------------

### 情景描述

你的手机被盗，而且手机中存放着恢复码的照片。

### 困点

恢复码照片泄露导致资金被盗。

### 解决方案

\*\*Aezeed（口令身份验证）。\*\*Aezeed 的口令验证可以防止用户输入错误密码，而攻击者即便拥有恢复码，若不知道正确口令，依然无法生成有效的密钥。

使用 Aezeed 方案，你的恢复码是加密的，并且包含口令身份验证。在恢复钱包时，如果输入错误的密码，会立即显示验证错误，不会生成错误的密钥树。

### 技术特点

从上述描述中，我们可能感觉 **Aezeed** 和 **BIP39**、**SLIP39** 在功能上有一定相似性，特别是在使用口令生成不同密钥树方面，但 **BIP39 和 SLIP39 的设计**旨在生成单一恢复码或分片恢复码，通过可选口令来生成不同的密钥树，但缺少验证机制。输入错误口令时，BIP39 和 SLIP39 仍会生成有效密钥树，可能导致用户误解和丢失资金。但 Aezeed 会通过校验，告知用户输入的是错误的口令。

不仅如此 Aezeed 相比 BIP39 和 SLIP39 还有一些独特的特性，包括其设计目的是为 **闪电网络**提供更好的支持，并且包含更多的安全特性，如**口令验证**和**钱包生日信息**，以及多层版本控制。这些特性使得 Aezeed 更适合复杂应用场景，比如高频、实时的交易需求。

![](https://storage.googleapis.com/papyrus_images/6c0230f805d8ea1f214d92436b213c39f803a0ffbebd2d9c00d4c6484d84ae35.png)

由于目前 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
    口令验证结果（错误口令）: 无效
    口令验证结果（正确口令）: 有效
    

场景 5：网络**黑客攻击**
---------------

### 情景描述

你担心黑客攻击威胁到恢复码的安全，因此希望找到一种完全离线的存储方法。

### 困点

离线存储且可分片（防止单一丢失）

### 解决方案

Codex32 的手动离线分布式存储。Codex32 恢复码方案允许用户通过打印说明书、剪刀等物理方式生成分片恢复码，避免电子设备参与。Eve 可以将恢复码分片离线打印并分散存放，确保恢复码不会受到网络攻击威胁。

总结
==

![](https://storage.googleapis.com/papyrus_images/da465d75a3e8a703c4d910a1bb00be1dfe3964649aab76d8f2965bc3497231dd.png)

---

*Originally published on [Simon 写字的地方](https://paragraph.com/@simon-2/IiFBkoS5ZKtrkWLHrXtb)*
