最新的一些 Web3 开发相关技术栈更新 && MetaMask 是怎么保存你的钱包秘钥的?
发布于 1 年前 作者 lyman 2013 次浏览 来自 分享

Web3Hacker.World 是一个集合程序员黑客、对新事物好奇的种子用户、及 vc 投资者组合的围绕 万物皆可 Web3 的理念打造的生态体系,目前已有付费会员 19 人

我是来自 Web3Hacker.World的 Bruce,22 年 5 月 All In Web3,后面连续参加了 10 多个黑客松并三个月内拿到 15 个左右的赛道奖及 Grants,累积产值 70 万 RMB 左右(含后续参与其他团队获得)。 后面又经历休息、搬家到海边(懒散了几个月)、从新学习了大量新技术。现在又开始新的一轮的 Web3 掘金。 这波主要围绕我创建的 BuidlerProtocol 打造 万物皆可 Web3 的跨链的 Web3 App Store 生态平台来制作各种不同的实际 RWA(Real World Assets) 应用场景的 DApp 参加各种不同的黑客松比赛并尝试获得 RWA 的早期种子用户。 在这个过程中,我会用 Web3 的技术开发一系列产品,同步输出开发这些产品的一些相关技术资料总结及分享。

本周开始报名了波卡黑客松比赛,这次规划做一个 NFT-Fi-Twitter 的浏览器插件,可以让 Twitter 用户去发布 NFT-Gating 的 twitter 或者评论,而要查看加密的内容的用户则需要支付 $BST 才能解锁(付费解锁时会附带获得对应的 NFT 作为支付凭证),在做的过程中延伸出了一系列的技术扩展需要研究。

TL;DR

  • NFT-Fi-Twitter: 可以注入到各种页面的 BuidlerProtocol 应用钱包
    • content inject 部分:在页面注入了内容数据列表及新建内容表单
    • options: 一整个 onboard 模块的 UI 设置好了,为了实现钱包的秘钥保存功能,翻译了 MetaMask 是怎么保存你的钱包秘钥的? 这篇文章,不过作者其实也是国人,只是用英文写的文章
  • vue3-wagmi 欢迎对开发 vue生态的 Web3 组件的同学加入报名,后续 Web3Hacker.World 会陆续推出激励机制
  • wagmi-cn 将组织翻译 wagmi 及 viem 文档中和 react 无关的部分(即通用部分)

细节版本

  • 浏览器插件开发相关技术: Plasmo / vitesse-webext
    • Plasmo
      • 这个技术去年就有关注,不过一直没有实战,这次立项 NFT-Fi-Twitter 浏览器扩展一开始也重新翻了一波他的文档及示例代码,但最终没有选择使用。
      • 因为使用 vue 技术栈,虽然里面也有 vue 的示例代码,但是实际大规模的工程化开发还需要很多的配置和模块化设计
      • 虽然开源,但是很多底层的配置我还没有看到可以自由设置,有点感觉被捆绑住了,要修改个 vite 配置等还得去翻额外文档或者代码
  • 最终使用了 vitesse-webext
  • 这个是 vitesse 的变种
  • 虽然他已有的代码逻辑并没有完全设计好大规模工程化开放的架构设计,但是我们可以比较自由的去修改底层一些设置
  • 我们本身之前的 Craft V1 及去年的 web3-framework 其实都是在其基础上进一步开发产生的
  • 目前我们的 Craft V2 即 Nuxt3 版本也是在其 Nuxt 变种基础上加上了额外设计架构成为 layer 模式继承开发的
  • 这些各种变种的设计思路都是一样的,打造统一的一致的能简化程序员开发心智的代码架构设计
  • 这一系列其实大部分套件都一样
    • Vue3, Vite, pnpm, esbuild
    • 文件基础的路由
      • 在 webext 版本中其并不支持,我们在这周也改造好了, 设计了 options/popup 的路由前缀
    • 组件自动导入
    • 通过 Pinia 进行状态管理
      • webext 版本还未使用 Pinia
    • Layout 系统
      • webext 版本中原本也不支持,我们本周也实现了
    • UnoCSS
    • 无需导入即可快速使用各种 Icon
  • 我们额外根据 Web3 开发的需求进行了进一步改造
    • 引入使用 Wagmi 4.5k star
      • 其初始定位为以太坊的 React Hooks(不过 EVM 兼容的均可)
      • 后面其抽象除了一套 ts 原生的 api,故我们可以在此基础上去封装一套 vue 的 composables api
        • 在其 awesome 库中看到已经个项目在做了
          • 一个 vagmi,但是作者竟然弃坑了
            • 估计用 Vue 开发 Web3 的比较少,用户少这个哥们受不了寂寞就弃坑了(还有他是说他也不再做 Web3 开发了)
            • 感觉在软件开源层面需要有一个非庞氏的Web3 激励措施来促进代码工具的迭代开发,但是核心还是作者本身有这个热情以及一定的后台支持才好推进,光用爱发电是无法长期维持的
          • 市面上其实还有别的 vue 的栈在做,但是不是很活跃
        • Web3Hacker.World 在上周也开坑了,不过还没有推代码: https://github.com/Web3Hacker-World/Discussions/issues/17,欢迎 vue3 技术栈同学可以到帖子里报名加入
    • ethersjs 替换为 Viem
      • Wagmi 和 Viem 为同一个团队开发的,这个团队非常活跃,Wagmmi 是其 16 个月前开始的项目 https://twitter.com/wagmi_sh/status/1655993650621317149
      • 而其这次则是在过去 16 个月的密集开发基础上,额外抽象出来 Viem, 用于取代 ethers.js
      • 我们这周也花了大量时间深入研究了一波,并会后续开坑组织翻译 Wagmmi 和 Viem 的中文文档,这些资料都会最终放到 BuidlerProtocol 上以 Web3 的 NFT-Gating 的模式提供给我们的黑客会员使用
      • 感兴趣同学欢迎报名:https://github.com/Web3Hacker-World/Discussions/issues/18

原文链接: https://www.wispwisp.com/index.php/2020/12/25/how-metamask-stores-your-wallet-secret/

作为区块链领域的安全工程师/渗透测试人员,我在研究和客户参与期间测试了许多加密钱包应用程序。在看到在不同钱包中处理秘密的不同方式后,我很好奇 MetaMask(该领域最著名的加密钱包之一)是如何做到的。好吧,如果你问我为什么不早点看 MetaMask,因为我只是假设它是安全的,而且我发现任何问题的机会非常低。

他们的扩展移动应用程序以及应用程序中使用的由 MetaMask 创建的许多其他模块都是开源的。代码库是迄今为止我见过的所有加密钱包中最大的。但它并不难理解,因为代码写得很好并且有很多注释。

这篇文章解释了应用程序中与秘钥存储相关的不同组件。在文章的最后,我包含了一个非常粗略的代码逻辑,描述了这个浏览器扩展是怎么创建一个新钱包。请注意,本文中的钱包秘钥指的是助记词(mnemonic)和私钥。

秘钥环(Keyring)

Keyring是 MetaMask 中秘密存储和账户管理系统的核心概念。KeyringController是密钥环的实现。引用自 KeyringController README:

一个用于管理以太坊账户组的模块,称为“Keyrings”,最初是为 MetaMask 的多账户类型功能定义的。

KeyringController 具有三个主要职责:

  • 初始化和使用(签名)以太坊帐户组(“密钥环”)。
  • 跟踪这些个人帐户的本地昵称。
  • 提供密码加密保存和恢复秘密信息。

这是密钥环结构的可视化参考: keyrings-ref

圆环表示用于生成公钥-私钥对的助记词。挂在环上的每把钥匙就是一个个人钱包帐户,这些私钥都是通过这个助记词生成的。种子短语和所有帐户数据捆绑在一起,使用从用户密码生成的加密密钥加密,并存储在扩展中。

KeyringController 使用 obs-store 类来存储数据。obs-store 代表 ObservableStore,它是单个值的同步内存存储。该代码还将 obs-store 引用为 Vault。让我们仔细看看它是如何实现的:在 KeyringController 构造函数中创建了两个 ObservableStore 对象,一个名为 this.store,另一个名为 this.memStore

constructor (opts) {
  super()
  const initState = opts.initState || {}
  this.keyringTypes = opts.keyringTypes ? keyringTypes.concat(opts.keyringTypes) : keyringTypes
  this.store = new ObservableStore(initState)
  this.memStore = new ObservableStore({
    isUnlocked: false,
    keyringTypes: this.keyringTypes.map((krt) => krt.type),
    keyrings: [],
  })

  this.encryptor = opts.encryptor || encryptor
  this.keyrings = []
}

this.store 存储加密后的钱包秘钥。this.store 的数据会被放入chrome扩展的本地存储中进行持久化数据存储。用户可以通过在扩展开发控制台中输入以下代码来访问数据。(似乎不行,译者注)

chrome.storage.local.get('data', result => {
    var vault = result.data.KeyringController.vault
    console.log(vault)
})

this store

this.memStore 则是存储解密后的钱包秘钥。该对象中的数据保留在内存中,不会放入浏览器的持久存储中。当您打开 MetaMask 扩展程序并输入您的密码时,您解密后的帐户私钥将存储在这个 this.memStore 对象中以备将来使用。 参考代码 1, 参考代码 2

加密器(encryptor)

在 KeyringController 类内部,加密和解密操作由加密器 对象执行。

例子 1. 加密秘钥环数据:

return this.encryptor.encrypt(this.password, serializedKeyrings)

例子 2. 解密此加密保险库

const vault = await this.encryptor.decrypt(password, encryptedVault)

加密器对象在 KeyringController 构造函数中分配

constructor (opts) {
  super()
  const initState = opts.initState || {}
  this.keyringTypes = opts.keyringTypes ? keyringTypes.concat(opts.keyringTypes) : keyringTypes
  this.store = new ObservableStore(initState)
  this.memStore = new ObservableStore({
    isUnlocked: false,
    keyringTypes: this.keyringTypes.map((krt) => krt.type),
    keyrings: [],
  })

  this.encryptor = opts.encryptor || encryptor
  this.keyrings = []
}

扩展程序和移动应用程序使用不同的加密器。该扩展使用 browser-passworder模块,其源代码可在 Github 上获得。移动应用程序有自己的 加密器 类。它们的工作原理几乎相同,除了[PBKDF2](https://en.wikipedia.org/wiki/PBKDF2)迭代和 AES 模式。

browser-passworder: 根据 password 生成 enc_key: PBKDF2, 10000 iteration AES mode: AES-GCM

Mobile app encryptor: 根据 password 生成 enc_key: PBKDF2, 5000 iteration AES mode: AES-CBC

移动应用

与扩展类似,移动应用程序采用密钥环结构来存储秘钥和管理帐户。对于持久存储,应用程序使用 persistConfig 文件中定义的 async-storage 模块存储加密数据。我能找到的唯一描述 异步存储 工作原理的官方文档是这样说的:

在 iOS 上,AsyncStorage 由本机代码支持,该代码将小值存储在序列化字典中,将较大值存储在单独的文件中。

在 Android 上,AsyncStorage 将根据可用情况使用 RocksDB 或 SQLite。

移动钱包提供 记住我使用触摸 ID/设备密码解锁 选项。用户无需每次在移动设备上打开应用程序时都输入密码。

remember-me

touchid-passcode

为实现这一点,MetaMask 移动应用程序使用 SecureKeychain 模块将用户密码存储在设备中,该模块建立在 react-native-keychain 之上。用户密码用于生成密钥来解密持久存储中的加密钱包秘密。

关于 react-native-keychain 如何在移动设备中存储数据的快速说明:

安全钥匙串(SecureKeyChain)

代码中的注释解释了 SecureKeychain 的作用:

/**
* react-native-keychain 内包裹 Keychain 的类
* 抽象 metamask 特定的功能及设置
* 同时在写入手机的 keychain 前添加额外的层
*/
class SecureKeychain {
...
}

抽象 metamask 特定的功能及设置 是指 记住我使用触摸 ID/设备密码登录 功能。查看 resetGenericPassword,getGenericPasswordsetGenericPassword 函数以了解它是如何实现的。

至于“增加一层额外的加密”,我很好奇这里使用的加密密钥是什么来执行加密?这是我找到的相关代码路径:

  1. 构造函数中的 code 用作加密密钥。(来源
  2. init(salt) 函数中,使用参数 salt 创建 SecureKeychain 对象(来源
  3. init 使用参数 props.foxCode( Source ) 调用该函数(来源

foxCode 又是什么?我在 手机钱包仓库MetaMask 组织 下的所有公共仓库中搜索 ,但没有结果。嗯🤔,应用程序二进制文件怎么样?

我用这个工具下载了 Metamask APK ,用 jadx 解包,搜索字符串 foxCode,发现了这个:

<img width=“750” alt=“foxcode” src=“https://github.com/Web3Hacker-World/Discussions/assets/109504677/1210ff50-fc8e-4c31-978d-7d305f58797c”>

等等,所以foxCode等于字符串“encrypt”?使用硬编码字符串加密某些内容有什么意义?🤔

我给 Metamask 安全团队发了一封邮件,询问为什么用硬编码字符串(foxCode)encrypt 加密的用户密码,该团队回复说:

metamask-reply

我非常同意他的看法。但我仍然想知道为什么之前会将此类代码放入代码库中。

结束

文章到此结束。我希望它能解释 MetaMask 钱包如何存储你的钱包秘密的基础知识。如果您想了解更多详细信息,可以在他们的 GitHub 存储库 https://github.com/MetaMask 中阅读其源代码。

==========================🦊附录🦊======================= ===

用于创建新钱包的非常粗略的代码路径:

  1. metamask-extension/ui/app/pages/first-time-flow/first-time-flow.component.js
const seedPhrase = await createNewAccount(password)
  1. metamask-extension/ui/app/pages/first-time-flow/first-time-flow.container.js
createNewAccount: (password) => dispatch(createNewVaultAndGetSeedPhrase(password))
  1. metamask-extension/ui/app/store/actions.js
export function createNewVaultAndGetSeedPhrase(password){
   ....
   await createNewVault(password)
   const seedWords = await verifySeedPhrase()
   ....
}
  1. metamask-extension/app/scripts/metamask-controller.js
async createNewVaultAndKeychain(password){
   vault = await this.keyringController.createNewVaultAndKeychain(password)
}
  1. KeyringController/blob/master/index.js
createNewVaultAndKeychain (password) {
   return this.persistAllKeyrings(password)
       .then(this.createFirstKeyTree.bind(this))
       .then(this.persistAllKeyrings.bind(this, password))
       .then(this.setUnlocked.bind(this))
       .then(this.fullUpdate.bind(this))
}
  1. MetaMask/KeyringController/blob/master/index.js
createFirstKeyTree () {
   ...
   return this.addNewKeyring('HD Key Tree', { numberOfAccounts: 1 })
   ...
}
  1. MetaMask/KeyringController/blob/master/index.js
addNewKeyring (type, opts) {
   ...
   const Keyring = this.getKeyringClassForType(type)
   const keyring = new Keyring(opts)
   ...
}
  1. MetaMask/eth-hd-keyring/blob/master/index.js
addAccounts (numberOfAccounts = 1) {
    ...
    this._initFromMnemonic(bip39.generateMnemonic())
    ...
}

往期回顾

1 回复

牛,我看看。

回到顶部