MetaMask Snaps 是让 MetaMask 成为全球可扩展性最强钱包的发展路线图。作为开发者,您可以通过全新的方式将自己的功能和 API 带入 MetaMask。Web3 开发者是这一增长的核心,本系列旨在展示当今正在构建的创新 MetaMask Snaps。
Invisible Keys Snap
您为什么要构建它?
外部拥有账户(EOA)是以太坊生态系统中几乎所有 dapp 或服务的核心。然而,设计并实现一个既安全又易用的钱包应用绝非易事。通常,为了给用户提供完全的控制权和高度的安全性,我们最终不得不依赖复杂且不实用的机制。另一方面,要实现简单愉快的用户体验,往往只能要求用户放弃对其密钥的控制权。
借助 InvisibleKeys,我们在 MetaMask 卓越用户体验的基础上,提出了一种在用户体验与安全性之间的新平衡方案,使 MetaMask 能够连接到一个创新的外部多云钱包。
如何实现?InvisibleKeys 的多云钱包将用户的私钥存储在两个或多个云存储服务中(例如 Google Drive、Dropbox 等),即使其中一个服务遭到攻击,密钥也永远不会泄露。 这种多云方案遵循一项已获专利的工作流程(专利号 US20190095628),在确保极高安全级别的同时,支持多设备同步。
要从云端导入账户,MetaMask 会读取您 Google Drive(或任何其他存储服务)中的一个 JSON 文件,其中包含您公钥的数组。然后,在签署交易时,您的私钥(PK)会在外部 Web 应用中临时且本地地重建,该应用接收 MetaMask 的签名请求,对交易进行签名,并将已签名的交易返回给 MetaMask。密钥不会被持久化存储,仅临时存在于内存中。此外,为保障用户的完全控制权,MetaMask 扩展程序永远无法访问私钥。即使钱包处于解锁状态或遭到攻击,私钥也无法被窃取。
InvisibleKeys 让用户可以在世界任何地方访问其钱包,因为所有内容都存储在云端,这与传统 MetaMask 方案形成互补,同时保持了 MetaMask 在可用性和用户界面方面的高标准。
架构
InvisibleKeys 由两个主要组件构成:MetaMask 钱包和外部 Web 应用。
由于我们打算在钱包外部完成交易签名,我们充分利用了代码中硬件钱包连接/签名的部分。我们没有将待签名的交易发送到特定硬件,而是弹出一个包含我们 Web 应用的窗口,在其中执行签名操作。
在 Web 应用中,用户使用云端凭据进行身份验证,检索私钥,对交易进行签名,然后从内存中清除密钥。最后,弹出窗口关闭,用户回到处于等待状态的 MetaMask 钱包,等待发送已签名的交易。
两个组件通过浏览器的 window web API 进行通信。在此场景下,这已足够,因为两者之间从不交换任何私密信息。
界面变更
首先,我们对 MetaMask 的界面进行了一些调整,以添加我们的 InvisibleKeys 连接策略。其添加方式与向应用添加新支持的硬件钱包相同。
以下是对代码所做的部分更改:添加了新的设备和类型,以及界面的新按钮。
app/scripts/metamask-controller.js
keyringName = InvisibleKeyring.type;
break;
ui/pages/create-account/connect-hardware/select-hardware.js
renderConnectToInvisibleButton() {
return (
<button
className={classnames('hw-connect__btn', {
selected: this.state.selectedDevice === 'invisible',
})}
onClick={(_) => this.setState({ selectedDevice: 'invisible' })}
>
<img
className="hw-connect__btn__img"
src="images/invisible-logo.png"
alt="Invisible"
/>
</button>
);
}
Invisible Keys Keyring
为了连接不同的硬件钱包,Keyring 类充当钱包与设备之间的通信接口。导入账户和签署交易所需的主要方法为:addAccounts()(返回地址数组)和 signTransaction()(返回已签名的交易)。
我们在 InvisibleKeyring 上实现了这些方法。第一个方法打开弹出窗口并等待包含地址数组的消息。第二个方法打开弹出窗口,并在消息中编码未签名的交易,然后等待编码后的已签名交易,该交易将被返回并由主控制器发送。在等待弹出窗口关闭期间,MetaMask 处于加载状态。
这些方法会调用另一个类 InvisibleConnect,该类负责 Web 应用的连接与通信。
./InvisibleConnect.js
export default class InvisibleConnect {
{...}
const appUrl = 'http://url-to-web-app';
async getAccountsCloud() {
let newAccounts = []
window.addEventListener(
'message',
event => {
if (event.data.event_id === 'imported_accounts') {
newAccounts = event.data.accounts
}
},
false,
)
return new Promise(async resolve => {
const child = window.open(appUrl + '/import')
const interval = setInterval(() => {
if (child.closed) {
clearInterval(interval)
this.accounts = newAccounts
resolve(newAccounts)
}
}, 1000)
})
}
async signTxCloud(transaction) {
let signedTx = {}
window.addEventListener(
'message',
event => {
if (event.data.event_id === 'signedTx') {
signedTx = {
v: event.data.v,
r: event.data.r,
s: event.data.s,
}
}
},
false,
)
return new Promise(async resolve => {
const child = window.open(appUrl + '/sign')
child.postMessage(
{
event_id: 'unsignedTx',
data: {
tx: transaction.serialize().toString('hex'),
},
},
'*',
)
const interval = setInterval(() => {
if (child.closed) {
clearInterval(interval)
resolve(signedTx)
}
}, 1000)
})
}
}
./eth-invisible-keyring.js
import InvisibleConnect from './InvisibleConnect'
class InvisibleKeyring extends EventEmitter {
{...}
addAccounts(n = 1) {
return new Promise((resolve, reject) => {
this.unlock()
.then(async _ => {
const from = this.unlockedAccount
const to = from + n
this.accounts = []
for (let i = from; i < to; i++) {
const address = InvisibleConnect.getAccounts(i).address
this.accounts.push(address)
this.accountIndexes[ethUtil.toChecksumAddress(address)] = i
this.page = 0
}
resolve(this.accounts)
})
.catch(e => {
reject(e)
})
})
}
signTransaction(address, tx) {
return new Promise(async (resolve, reject) => {
try {
const signedTx = await InvisibleConnect.signTxCloud(tx)
const txData = tx.toJSON()
txData.v = ethUtil.addHexPrefix(signedTx.v)
txData.r = ethUtil.addHexPrefix(signedTx.r)
txData.s = ethUtil.addHexPrefix(signedTx.s)
const common = tx.common
const freeze = Object.isFrozen(tx)
const feeMarketTransaction = FeeMarketEIP1559Transaction.fromTxData(
txData,
{common, freeze},
)
resolve(feeMarketTransaction)
} catch (err) {
reject(new Error(err))
}
})
}
{...}
}
外部应用
该应用包含两个页面:一个用于导入账户,另一个用于签署交易。
在第一个页面中,用户向 Google Drive 进行身份验证,其中存储了一个包含公钥的文件,路径为 metamask/public.json。点击导入按钮后,系统会获取该文件,读取密钥,并通过 window web API 发送至 MetaMask 钱包。
const sendAccountsToMetamask = (publicKeys: Address[]) => {
window.opener.postMessage(
{
event_id: 'importAccounts',
data: {
accounts: publicKeys,
},
},
'*'
);
};
const getPublicKeys = async (): void => {
const publicKeys: Address[] = await getGoogleDrivePublicKeys();
sendAccountsToMetamask(publicKeys);
return;
};
在第二个页面中,用户需要同时向 Google Drive 和 Dropbox 进行身份验证,这两个服务是我们在本示例中使用的。该方案兼容任何存储服务,要求是这些服务相互独立、互不勾连。身份验证完成后,从这些服务中检索各部分信息,并使用 decrypt() 方法重建私钥,该方法按照上述专利中描述的机制实现,然后对交易进行签名。签名完成后,私钥立即从内存中清除。最后,已签名的交易被发送回 MetaMask 钱包。
以下是逐步展示的代码,仅显示相关部分,所有与界面状态或 API 通信相关的内容均已省略。
const sendTxToMetamask = (v, r, s) => {
window.opener.postMessage(
{
event_id: 'signedTx',
data: {
v: v,
r: r,
s: s,
},
},
'*'
);
};
const signTransaction = async (): void => {
const trs = TransactionFactory.fromSerializedData(Buffer.from(tx, 'hex'));
const googleDrivePart = await getGoogleDrivePart();
const dropboxPart = await getDropboxPart();
const piecesArray = [
Buffer.from(googleDrivePart, 'hex'),
Buffer.from(dropboxPart, 'hex')
];
const decrypted = decrypt({ data: piecesArray });
let plainTextPK = Buffer.from(
new TextDecoder().decode(decrypted.data),
'hex'
);
if (!ethUtil.isValidPrivate(plainTextPK)) {
alert('Failed to decrypt Private Key');
return;
}
const signedTx = trs.sign(plainTextPK);
plainTextPK = null;
const v = ethUtil.stripHexPrefix(ethUtil.bufferToHex(signed_tx.v));
const r = ethUtil.stripHexPrefix(ethUtil.bufferToHex(signed_tx.r));
const s = ethUtil.stripHexPrefix(ethUtil.bufferToHex(signed_tx.s));
if (signed_tx.verifySignature()) {
sendTxToMetamask(v, r, s);
} else {
alert('Signature failed');
}
return;
}
能否介绍一下您自己和您的团队?
我们是一支充满热情的软件工程师团队,总部位于葡萄牙布拉加,在分布式系统领域拥有深厚的技术背景。我们创立了 Invisible Lab,这是一家专注于解决区块链和 Web3 领域难题的初创公司。
您是什么时候第一次接触 MetaMask Snaps 的,体验如何?
我们第一次接触 MetaMask Snaps 是在 SDK 发布之时。自那以后,这项技术已有了显著的进步,并仍在持续发展。
MetaMask Snaps 与其他钱包有何不同?
MetaMask Snaps 钱包提供了在其基础上构建功能的能力,同时保留了数百万用户每天都在使用的熟悉设计。
请谈谈您和您的团队使用 MetaMask 构建 Snaps 的体验?
我们使用了 MetaMask Snaps 的早期版本,该版本并不支持本项目所需的所有功能。但我们相信,在不久的将来,我们构建的一切都将能够以更简单、更流畅的方式通过 Snaps 实现。
MetaMask Snaps 对您意味着什么?
它意味着定制化、可升级性,以及在 MetaMask 钱包之上构建创新的无限可能。
您认为 MetaMask Snaps 为 Web3 领域带来了哪些机遇?
我们认为一个令人兴奋的机遇是构建额外的安全策略——根据每位用户的经验和愿意承担的风险级别,创建不同层次的安全保障。这可能对降低用户面临安全问题和技术黑客攻击的风险产生重大影响,而这些问题在该领域不幸已屡见不鲜。缓解这些问题对于 Web3 的大规模普及至关重要。
对于有意尝试 MetaMask Snaps 的开发者,您有什么建议?
大胆探索吧!MetaMask Snaps 能让您实现的成果会令您惊喜不已。无论是集成新的区块链、功能,还是安全方案,都大有可为!
使用 MetaMask Snaps 进行构建
开始使用 MetaMask Snaps:
查阅开发者文档
安装 MetaMask Flask
查看 MetaMask Snaps 指南
在 Twitter、GitHub 讨论区和 Discord 上与我们保持联系
请留意我们团队在您附近举办的下一场黑客松!祝 BUIDL 愉快 ⚒️
免责声明:MetaMask Snaps 通常由 Consensys Software 以外的第三方开发。使用第三方开发的 MetaMask Snaps 须由您自行决定并承担风险,且您同意对因此类活动导致的任何损失或损害承担全部责任。Consensys 对第三方开发的 MetaMask Snaps 不作任何明示或暗示的保证,并对第三方开发的 MetaMask Snaps 免除一切责任。使用区块链相关软件存在风险,使用 MetaMask Snaps 即表示您完全承担上述风险。