MetaMask Snaps 是讓 MetaMask 成為全球最具擴展性錢包的發展藍圖。身為開發者,您可以透過全新的方式將您的功能與 API 帶入 MetaMask。Web3 開發者是這項成長的核心,本系列旨在展示當今正在建構的創新 MetaMask Snaps。
Invisible Keys Snap
您為什麼要建構它?
外部擁有帳戶(Externally-owned accounts)幾乎是 Ethereum 生態系中每個 dapp 或服務的核心。然而,要設計並實作一個既安全又易於使用的錢包應用程式,絕非易事。通常,為了給予使用者完整的控制權與高度的安全性,我們最終不得不仰賴複雜且不切實際的機制。另一方面,若要提供直覺且愉快的使用者體驗,往往只能要求使用者放棄對私鑰的控制權。
透過 InvisibleKeys,我們以 MetaMask 備受肯定的使用者體驗為基礎,提出一種在使用者體驗與安全性之間的新平衡方案,讓 MetaMask 能夠連接至一個創新的外部多雲端錢包。
如何實現?InvisibleKeys 的多雲端錢包將使用者的私鑰儲存於兩個或多個雲端儲存服務(例如 Google Drive、Dropbox 等),即使其中一個服務遭到入侵,私鑰也絕不會外洩。 這種多雲端方式遵循一項已獲專利的工作流程(專利號 US20190095628),在確保極高安全性的同時,也支援多裝置同步。
若要從雲端匯入帳戶,MetaMask 會讀取您 Google Drive(或任何其他儲存服務)中的一個 JSON 檔案,其中包含您公鑰的陣列。接著,若要簽署交易,您的私鑰(PK)會在外部網頁應用程式中暫時且僅在本地端重建,該應用程式接收 MetaMask 的簽署請求、簽署交易,並將已簽署的交易回傳給 MetaMask。私鑰不會被持久化儲存,僅暫時存在於記憶體中。此外,為了保留使用者的完整控制權,MetaMask 擴充功能永遠不會存取私鑰。即使錢包已解鎖或遭到入侵,私鑰也無法被竊取。
InvisibleKeys 讓使用者能夠從世界任何地方存取其錢包,因為所有資料都儲存在雲端,這補充了傳統 MetaMask 的方式,同時維持 MetaMask 在易用性與使用者介面上的高標準。
架構
InvisibleKeys 由兩個主要元件組成:MetaMask 錢包與外部網頁應用程式。
由於我們打算在錢包外部簽署交易,我們利用了程式碼中硬體錢包連接/簽署的部分。我們不是將交易傳送至特定硬體進行簽署,而是開啟一個包含我們網頁應用程式的彈出視窗,在其中執行簽署。
在網頁應用程式中,使用者以其雲端憑證進行驗證,私鑰被取回,交易被簽署,然後私鑰從記憶體中清除。最後,彈出視窗關閉,使用者回到處於等待狀態的 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,該類別負責網頁應用程式的連接與通訊。
./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 discussions 和 Discord 上與我們保持聯繫
請留意我們的團隊將出現在您附近的下一場黑客松!祝 BUIDL 愉快 ⚒️
免責聲明:MetaMask Snaps 通常由 Consensys Software 以外的第三方開發。使用第三方開發的 MetaMask Snaps 須由您自行判斷與承擔風險,並同意您將對因此類活動所造成的任何損失或損害負全部責任。Consensys 對第三方開發的 MetaMask Snaps 不作任何明示或暗示的保證,並對第三方開發的 MetaMask Snaps 免除一切責任。使用區塊鏈相關軟體存在風險,使用 MetaMask Snaps 即表示您完全承擔這些風險。