Skip to main content

BIP-38: 口令保护的私钥

号外:今天在“刘教链Pro”发表了《内参:Uniswap V4的源代码协议之争论》,欢迎关注公众号“刘教链Pro”并阅读。


比特币改进提案#38号是关于口令保护的私钥。

该提案主要解决两个问题: 一、使用任意口令加密既有私钥 二、共享私钥生成方案,使得实物比特币制造商不能触及原始的、可提取资金的口令

以下内容主要摘译自BIP-38提案[1]

摘要

(该提案)提出了一种以 58 个字符的 Base58Check 编码可打印字符串的形式来加密和编码受口令保护的比特币私钥记录的方法。 加密的私钥记录旨在用于纸钱包和实物比特币。 每个记录字符串都包含重构私钥所需的所有信息(口令除外),该方法使用加盐以及scrypt加密来抵御暴力攻击。

该方法提供了两种编码方法——一种允许使用任意口令加密任何已知的私钥,另一种允许共享私钥生成方案,其中生成最终密钥字符串及其相关的比特币地址的一方(例如实物比特币制造商)只知道从原始口令派生的一个字符串,而需要原始口令才能实际赎回发送到相关比特币地址的资金。

生成的比特币地址的 32 位哈希值在每个加密密钥中以明文编码,因此不知道口令的人可以以合理的概率将其与比特币地址相关联。完整的比特币地址可以通过成功解密密钥记录得到。

动机

提出这个建议的动机源于对实物比特币和纸钱包使用方式的观察。

实物比特币的发行者必须值得信赖和信任。 即使值得信赖,用户也有理由怀疑第三方理论上可以拿走他们的资金。 不能被其发行者破坏的实物比特币总是比可以破坏的更具内在价值。

双因子实物比特币解决方案对于希望安全地拥有比特币而不想遭受任何电子盗窃风险也不想克服产生这种(安全)环境所需的技术学习曲线的个人和组织非常有用。 双因子实物比特币允许将安全存储解决方案放入盒子中并在公开市场上出售,从而大大增加了能够安全存储比特币的人数。

用于创建双因子实物比特币的现有方法是有限且繁琐的。 在提出本提案时,用户可以创建自己的私钥,将公钥提交给实物比特币发行者,然后收到一个实物比特币,该比特币必须与用户生成的私钥的某种记录保存在一起, 最后,必须通过工具来赎回。 实物比特币必须与用户生成的私钥保存在一起这一事实抵消了实物比特币的大部分好处 - 用户也可以打印和维护私钥。

标准化的口令保护私钥格式使用户更容易获取和兑换双因子实物比特币。 用户可以自己选择密码,而不是维护无法记忆的私钥。 密码短语可能比典型私钥的长度短得多,短到他们可以在收到实物比特币之后,使用标签或雕刻机将口令永久地记录到他们的实物比特币上。 通过采用一种标准的方式来加密私钥,我们最大限度地提高了他们能够在他们选择的地方赎回资金的可能性,而不是依赖于他们可能不希望下载的可执行程序作为赎回工具。

受密码和口令保护的私钥支持新的实用用例,用于在人与人之间发送比特币。 想要通过邮政邮件发送比特币的人可以发送一个受密码保护的纸钱包,并通过电话或电子邮件向收件人提供口令,从而使传输安全,不会被任何一种渠道拦截。 纸钱包或比特币纸币式凭证(“现金”)的用户可以携带资金加密的私钥,同时在家中留下一份副本,作为防止意外丢失或被盗的一种保护措施。 将比特币留在银行金库或保险箱中的纸钱包用户可以将密码留在家中或与可信赖的同事分享,以防止银行的某人获得纸钱包的访问权限并从中支出。 受密码保护的私钥的可预见和不可预见的用例很多。

版权

此提案特此置于公共领域。

原因

用户故事:作为使用纸钱包的比特币用户,我希望能够添加加密功能,这样我的比特币纸质存储可以是双因子的:我拥有的东西和我知道的东西。

用户故事:作为比特币用户,我想用私钥向个人或公司付款,我不想担心通信路径的任何部分可能导致密钥被拦截和我的资金被盗。 我更愿意提供一个加密的私钥,然后使用不同的通信渠道(例如电话或短信)跟进(提供)密码。

用户故事:(EC相乘密钥)作为实物比特币的用户,我希望第三方能够为我创建受密码保护的比特币私钥,而无需他们知道密码,这样我就可以从发行人无法访问私钥的物理比特币中受益。我希望能够选择一个密码,其最小长度和要求的格式不妨碍我记住它或将其刻在我的实物比特币上,而不会使我面临密码破解和/或被商品制造商盗窃的不当风险。

用户故事:(EC相乘密钥)作为纸钱包的用户,我希望能够生成大量受相同密码保护的比特币地址,同时享受高度的安全性(计算量非常大的 scrypt 参数),但没有必要生成每个地址都需要 scrypt 延迟。

规格

该提案使用了以下函数和定义:

AES256Encrypt、AES256Decrypt:著名的 AES 块密码的简单形式,不考虑初始化向量或块链接。 这些函数中的每一个都采用一个 256 位密钥和 16 个字节的输入,并确定性地产生 16 个字节的输出。

SHA256,一种著名的哈希算法,它以任意数量的字节作为输入并确定性地产生 32 字节的哈希。

scrypt:一种众所周知的密钥派生算法。 它采用以下参数:(string) password、(string) salt、(int) n、(int) r、(int) p、(int) length,并确定性地生成长度等于length参数的字节数组。

ECMultiply:椭圆曲线点乘以相对于 secp256k1 椭圆曲线的标量整数。

G、N:定义为 secp256k1 椭圆曲线一部分的常数。 G为椭圆曲线点,N为大正整数。

Base58Check:一种使用比特币生态系统中常用的 58 个字母数字字符对字节数组进行编码的方法。

前言

建议生成的 Base58Check 编码字符串以“6”开头。 从用户的角度来看,数字“6”旨在表示“需要其他东西才能使用的私钥” —— 一个伞式定义,将来可以理解为包括参与多重签名交易的密钥,并且这么选以致敬钱包导入格式中最常见的、表示未加密私钥的现有前缀“5”。

建议第二个字符应该给出关于需要什么作为第二个因素的提示,并且对于需要口令的加密密钥,建议使用大写字母 P。

为了减小加密密钥的大小,AES 加密中不使用初始化向量 (IV)。 相反,适合类似 IV 使用的值是使用 scrypt 从密码短语和使用生成的比特币地址的 32 位哈希值作为盐来派生的。

提议规格

  • 对象标识符前缀:0x0142(非 EC 相乘)或 0x0143(EC 相乘)。 这些是常量字节,出现在 Base58Check 编码记录的开头,它们的存在导致生成的字符串具有可预测的前缀。
  • 用户看到什么:58 个字符,始终以“6P”开头
    • 视觉提示出现在第三个字符中,用于视觉识别 EC 相乘和压缩标志。
  • 有效负载字节数(超出前缀):37
    • 1 字节(标志字节):
      • 最重要的两位设置如下,以保留前缀中压缩标志的可见性,以及将有效负载保持在保持“6P”前缀完整的允许值范围内。 对于非 EC 相乘密钥,位为 11。对于 EC 相乘密钥,位为 00。
      • 设置时值为 0x20 的位表示应使用 DER 压缩公钥格式将密钥转换为 base58check 编码的 P2PKH 比特币地址。 未设置时,它应该是使用 DER 未压缩公钥格式的 base58check 编码的 P2PKH 比特币地址。
      • 值为 0x10 和 0x08 的位保留用于未来的规范,该规范考虑使用多重签名作为组合因素的方式,以便拥有单独因素的各方可以独立签署提议的交易,而无需任何一方同时拥有这两个因素。 这些位必须为 0 才能符合此版本的规范。
      • 值为 0x04 的位指示批号和序列号是否被编码到第一个因素中,并激活将它们包含在解密过程中的特殊行为。 这仅适用于 EC 相乘密钥。 对于非 EC 相乘密钥,必须为 0。
      • 其余位保留供将来使用,并且必须全部为 0 以符合此版本的规范。
    • 4 个字节:SHA256(SHA256(expected_bitcoin_address))[0...3],既用于拼写错误检查又用作盐
    • 16字节:内容取决于是否使用EC相乘。
    • 16字节:lasthalf:一条AES加密的密钥材料记录(内容取决于是否使用EC相乘)
  • 未压缩的非 EC 相乘密钥的 base58check 编码范围(前缀 6PR):
    • 最小值:6PRHv1jg1ytiE4kT2QtrUz8gEjMQghZDWg1FuxjdYDzjUkcJeGdFj9q9Vi(基于01 42 C0加三十六个00)
    • 最大值:6PRWdmoT1ZursVcr5NiD14p5bHrKVGPG7yeEoEeRb8FVaqYSHnZTLEbYsU(基于01 42 C0加上三十六个FF)
  • 带压缩的非 EC 相乘密钥的 base58check 编码范围(前缀 6PY):
    • 最小值:6PYJxKpVnkXUsnZAfD2B5ZsZafJYNp4ezQQeCjs39494qUUXLnXijLx6LG(基于01 42 E0加三十六个00)
    • 最大值:6PYXg5tGnLYdXDRZiAqXbeYxwDoTBNthbi3d61mqBxPpwZQezJTvQHsCnk(基于 01 42 E0 加上三十六个 FF)
  • 无压缩的 EC 相乘密钥的 base58check 编码范围(前缀 6Pf):
    • 最小值:6PfKzduKZXAFXWMtJ19Vg9cSvbFg4va6U8p2VWzSjtHQCCLk3JSBpUvfpf(基于 01 43 00 加上 36 个 00)
    • 最大值:6PfYiPy6Z7BQAwEHLxxrCEHrH9kasVQ95ST1NnuEnnYAJHGsgpNPQ9dTHc(基于01 43 00加上三十六个FF)
  • 带压缩的 EC 相乘密钥的 base58check 编码范围(前缀 6Pn):
    • 最小值:6PnM2wz9LHo2BEAbvoGpGjMLGXCom35XwsDQnJ7rLiRjYvCxjpLenmoBsR(基于01 43 20加三十六个00)
    • 最大值:6PnZki3vKspApf2zym6Anp2jd5hiZbuaZArPfa2ePcgVf196PLGrQNyVUh(基于01 43 20加上三十六个FF)

不使用 EC 相乘标志时的加密

在没有 EC 相乘的情况下加密私钥提供了可以加密任何已知私钥的优势。 执行加密的一方必须知道密码。

加密步骤:

  1. 计算比特币地址 (ASCII),并取其 SHA256(SHA256()) 的前四个字节。 让我们称之为“addresshash”。
  2. 使用 scrypt 从口令派生密钥
    • 参数:口令是以 UTF-8 编码并使用 Unicode Normalization Form C (NFC) 规范化的密码本身。 salt 是前面步骤的 addresshash,n=16384,r=8,p=8,length=64(n,r,p 是临时的,需要达成共识)
    • 让我们将生成的 64 个字节分成两半,并将它们称为 derivedhalf1 和 derivedhalf2。
  3. 做AES256Encrypt(block = bitcoinprivkey[0...15] xor derivedhalf1[0...15], key = derivedhalf2),调用16字节结果encryptedhalf1
  4. 做AES256Encrypt(block = bitcoinprivkey[16...31] xor derivedhalf1[16...31], key = derivedhalf2),调用16字节结果encryptedhalf2

加密的私钥是以下内容的 Base58Check 编码串联,总共 39 个字节,没有 Base58 校验和:

  • 0x01 0x42 + flagbyte + salt + encryptedhalf1 + encryptedhalf2

解密步骤:

  1. 从用户那里收集加密的私钥和口令。
  2. 通过将密码和地址哈希值传递到 scrypt 函数来派生 derivedhalf1 和 derivedhalf2。
  3. 使用AES256Decrypt对encryptedhalf1和encryptedhalf2进行解密,合并形成加密私钥。
  4. 将该私钥转换为比特币地址,遵守在加密密钥记录的标志字节中指定的压缩首选项。
  5. 哈希比特币地址,并验证来自加密私钥记录的地址哈希是否与哈希匹配。 如果不是,请报告口令条目不正确。

使用 EC 相乘模式时的加密

使用 EC 相乘加密私钥使某人能够生成加密密钥,只知道从原始密码派生的 EC 点和口令所有者生成的一些盐,而不知道口令本身。 只有知道原始口令的人才能解密私钥。 称为中间代码的代码在不知道口令的情况下传达生成此类密钥所需的信息。

这种方法不提供加密已知私钥的能力——这意味着创建加密密钥的过程也是生成新地址的过程。 另一方面,这为拥有以这种方式生成的地址的人提供了安全好处:如果可以通过使用口令解密其私钥来重新创建地址,并且这是一个可以确定只有他自己知道的强口令,那么他可以安全地得出结论,没有人可以知道该地址的私钥。

知道口令并且是私钥的预期受益人的人称为所有者。 他将生成一个或多个“中间代码”,这是双因子赎回系统的第一个因子,并将它们交给我们称为打印者的其他人,后者生成带有中间代码的密钥对,其可以知道地址和加密的私钥,但由于没有原始口令故而无法解密私钥。

为了便利用户,中间代码应该(但不是必须)嵌入可打印的“批号”和“序列号”。 该提案强制将这些批号和序列号包含在从它们生成的任何有效私钥中。 应用程序会建议已请求为其生成多个私钥的所有者确保每个私钥都具有与他生成的中间代码一致的唯一批次和序列号。 这些主要有助于保护所有者免受打印者可能造成的潜在错误和/或攻击。

“批号”和“序列号”组合成一个 32 位数。 20位用于批号,12位用于序号,批号可以是0到1048575之间的任意十进制数,序号可以是0到4095之间的任意十进制数。对于为所有者生成批次中间代码的程序,建议批号在100000-999999范围内随机选择,并分配从1开始的序列号。

如果包含批号和序列号,所有者执行的生成单个中间代码的步骤:

  1. 生成 4 个随机字节,称它们为 ownersalt。
  2. 将批号和序列号编码为 4 字节数量(大端字节序):lotnumber * 4096 + sequencenumber。 将这四个字节称为 lotsequence。
  3. 连接 ownersalt + lotsequence 并将此称为 ownerentropy。
  4. 使用 scrypt 从口令派生密钥
    • 参数:口令是以 UTF-8 编码并使用 Unicode Normalization Form C (NFC) 规范化的密码本身。 盐是 ownersalt。 n=16384,r=8,p=8,长度=32。
    • 把生成的 32 字节数据称为 prefactor。
    • 计算 SHA256(SHA256(prefactor + ownerentropy)) 并将结果称为 passfactor。 “+”运算符是连接。
  5. 计算椭圆曲线点 G * passfactor,并将结果转换为压缩符号(33 字节)。 将此称为 passpoint。 压缩符号用于此目的,无论其目的是创建带有或不带有压缩公钥的比特币地址。
  6. 将 ownersalt 和 passpoint 连同校验和(checksum)一起传送给生成密钥的一方以确保完整性。
    • 为此,建议使用以下 Base58Check 编码格式:魔术字节“2C E9 B3 E1 FF 39 E2 51”,后跟 ownerentropy,然后是 passpoint。 由于常量字节,生成的字符串将以单词“passphrase”开头,长度为 72 个字符,编码 49 个字节(8 个字节常量 + 8 个字节所有者熵 + 33 个字节 passpoint)。 校验和在 Base58Check 编码中处理。 生成的字符串称为 intermediate_passphrase_string。

如果未包括批号和序列号,则按照相同的程序进行以下更改:

  • ownersalt 是 8 个随机字节而不是 4 个,并且省略了 lotsequence。 ownerentropy 成为 ownersalt 的别名。
  • prefactor 到 passfactor 的 SHA256 转换被省略了。 相反,scrypt 的输出直接用作 passfactor。
  • 魔术字节是“2C E9 B3 E1 FF 39 E2 53”(最后一个字节是 0x53 而不是 0x51)。

从所有者给定中间密码字符串创建新的加密私钥的步骤(所以我们有所有者熵和密码,但我们没有密码或密码):

  1. 设置 flagbyte。
    • 如果比特币地址将通过哈希压缩公钥形成,则打开位 0x20(可选,节省空间,但许多比特币实现与其不兼容)
    • 如果 ownerentropy 包含 lotsequence 的值,则打开位 0x04。 (虽然它对密钥对生成过程没有影响,但解密过程需要这个标志来知道如何处理 ownerentropy)
  2. 生成 24 个随机字节,称之为 seedb。 取 SHA256(SHA256(seedb)) 产生 32 个字节,称之为 factorb。
  3. 将 passpoint 和 factorb 进行EC相乘。 使用生成的 EC 点作为公钥,并使用压缩或未压缩的公钥方法(指定在 flagbyte 中使用哪种方法)将其哈希成比特币地址。 这是生成的比特币地址,称之为generatedaddress。
  4. 取 SHA256(SHA256(generatedaddress)) 的前四个字节并将其称为 addresshash。
  5. 现在我们将加密 seedb。 使用 scrypt 从 passpoint 派生第二个密钥
    • 参数:passphrase为甲方提供的passpoint(二进制表示为33字节)。 salt 是 addresshash + ownerentropy,n=1024,r=1,p=1,length=64。 “+”运算符是连接。
    • 将结果分成两个 32 字节的一半,并将它们称为 derivedhalf1 和 derivedhalf2。
  6. 做AES256Encrypt(block = (seedb[0...15] xor derivedhalf1[0...15]), key = derivedhalf2),调用16字节结果encryptedpart1
  7. 执行 AES256Encrypt(block = ((encryptedpart1[8...15] + seedb[16...23]) xor derivedhalf1[16...31]), key = derivedhalf2),调用 16 字节结果 encryptedpart2。 “+”运算符是连接。

加密的私钥是以下内容的 Base58Check 编码串联,总共 39 个字节,没有 Base58 校验和:

  • 0x01 0x43 + flagbyte + addresshash + ownerentropy + encryptedpart1[0...7] + encryptedpart2
确认码

生成比特币地址的一方可以选择向所有者返回确认码,允许所有者独立验证他是否已获得实际上取决于他的口令的比特币地址,并确认批号和序列号(如果适用) . 这可以保护所有者不会被另一方提供与密钥推导无关并且可能被另一方花费的比特币地址。 如果提供给所有者的比特币地址可以通过确认过程成功重新生成,则所有者可以合理地确信没有口令的任何支出都是不可行的。 此确认码为 75 个字符,以“cfrm38”开头。

要生成它,我们需要来自原始加密操作的 flagbyte、ownerentropy、factorb、derivedhalf1 和 derivedhalf2。

  1. 将b和G进行EC相乘,称结果为pointb。 结果是 33 个字节。
  2. 第一个字节是 0x02 或 0x03。 将它与 (derivedhalf2[31] & 0x01) 进行异或,把结果称为 pointbprefix。
  3. 执行 AES256Encrypt(block = (pointb[1...16] xor derivedhalf1[0...15]), key = derivedhalf2) 并把结果称为 pointbx1。
  4. 执行 AES256Encrypt(block = (pointb[17...32] xor derivedhalf1[16...31]), key = derivedhalf2) 并把结果称为 pointbx2。
  5. 连接 pointbprefix + pointbx1 + pointbx2(共 33 个字节)并将结果称为 encryptedpointb。

结果是以下内容的 Base58Check 编码串联:

  • 0x64 0x3B 0xF6 0xA8 0x9A + flagbyte + addresshash + ownerentropy + encryptedpointb

给定密码和确认码,使用确认工具就可以重新计算地址,验证地址哈希,然后断言:“已确认比特币地址地址依赖于此口令”。 如果适用:“批号为 lotnumber,序列号为 sequencenumber。”

要重新计算地址:

  1. 使用带有 ownerentropy 和用户口令的 scrypt 导出 passfactor,并使用它重新计算 passpoint
  2. 使用带有 passpoint、addresshash 和 ownerentropy 的 scrypt 导出 pointb 的解密密钥
  3. 解密 encryptedpointb 以产生 pointb
  4. 将 pointb 与 passfactor 进行EC相乘。 使用生成的 EC 点作为公钥,并使用压缩或未压缩的公钥方法将其哈希成地址,如 flagbyte 中指定的那样。
解密
  1. 从用户那里收集加密私钥和口令。
  2. 使用带有 ownersalt 和用户口令的 scrypt 导出 passfactor,并使用它重新计算 passpoint
  3. 使用带有 passpoint、addresshash 和 ownerentropy 的 scrypt 为 seedb 导出解密密钥
  4. 使用 AES256Decrypt 解密 encryptedpart2 以生成 seedb 的最后 8 个字节和 encryptedpart1 的最后 8 个字节。
  5. 解密 encryptedpart1 以产生 seedb 的剩余部分。
  6. 使用 seedb 计算 factorb。
  7. 将 passfactor 乘以 factorb mod N 以产生与 generatedaddress 关联的私钥。
  8. 将该私钥转换为比特币地址,遵循加密密钥中指定的压缩首选项。
  9. 哈希比特币地址,并验证来自加密私钥记录的地址哈希是否与哈希匹配。 如果不是,请报告密码条目不正确。

向后兼容性

向后兼容性是最低限度适用的,因为这最多是一个扩展电子钱包导入格式的新标准。 假定私钥数据的入口点也可以接受现有格式的私钥(例如十六进制和钱包导入格式); 该草案使用一种不会被误认为任何现有格式的密钥格式,并保留了自动检测功能。

对山寨链提案实施者的建议

如果该提案被山寨链接受,则要求未使用的标志字节不用于表示密钥属于山寨链。

山寨链实施者应该为此目的利用地址哈希。 由于此提案中的每个操作都涉及对硬币地址的文本表示进行哈希处理,该地址(对于比特币)包括前导“1”,因此可以通过使用山寨链的首选格式来表示地址以轻松地表示山寨链。 山寨链实施者还可以更改前缀,使加密地址不以“6P”开头。

讨论项:scrypt参数

该提案将 scrypt 参数搁置,悬而未决。 提议考虑以下事项:

scrypt 的主要目标是降低暴力攻击的可行性。 必须假设攻击者将能够使用 scrypt 的高效实现。 这些参数应该强制 scrypt 的高效实现等待相当长的时间来减缓攻击。

另一方面,scrypt 不可避免地可能被实施的地方是使用缓慢的解释语言,例如 javascript。 高效的 scrypt 实现可能需要几毫秒,而在 javascript 中可能需要几秒钟。

然而,据信,使用 javascript 实现的人可能正在手工处理代码,一次处理一个代码,而不是生成或处理大批量的代码。 因此,用户可以接受几秒的等待时间。

强制服务器消耗几秒 CPU 时间的私钥赎回过程会阻碍服务器所有者的实施,因为他们会通过邀请用户多次尝试调用赎回过程来打开拒绝服务的途径。 但是,服务器所有者也可以通过用户的浏览器完成解密,从他自己的服务器卸载任务的方式来实现他的兑换过程(并提供另一个原因,为什么选择的 scrypt 参数应该容忍基于javascript的解密器)。

希望 16384、8 和 8 的初始值提供以下属性:

  • javascript中的加密/解密每次操作需要几秒钟
  • 在并发线程可用的环境中,并行化参数的使用为加速提供了适度的机会——此类环境将被选择用于必须处理大量加密/解密操作的进程。 一个操作的估计时间是几十或几百毫秒。

参考资料: