Invisible Keys Snap: マルチクラウド秘密鍵ストレージ

ETHLisbon ハッカソン受賞者

8分で読める
Invisible Keys Snap: マルチクラウド秘密鍵ストレージ

MetaMask Snapsは、MetaMaskを世界で最も拡張性の高いウォレットにするためのロードマップです。開発者として、まったく新しい方法でMetaMaskに機能やAPIを追加することができます。Web3開発者はこの成長の中核を担っており、本シリーズでは現在開発中の革新的なMetaMask Snapsを紹介することを目的としています。

Invisible Keys Snap

なぜ開発したのですか?

外部所有アカウント(EOA)は、Ethereumエコシステムにおけるほぼすべてのdappやサービスの中核を成しています。しかし、安全かつ使いやすいウォレットアプリケーションを設計・実装することは、決して簡単ではありません。一般的に、ユーザーへの完全なコントロールと高いセキュリティレベルを提供しようとすると、複雑で実用的でない仕組みに頼らざるを得なくなります。一方、シンプルで快適なユーザー体験を実現しようとすると、ユーザーに鍵の管理権限を手放すよう求めることになります。

InvisibleKeysでは、MetaMaskの優れたユーザー体験を基盤として、ユーザー体験とセキュリティの新たな妥協点を提案し、MetaMaskが革新的な外部マルチクラウドウォレットと接続できるようにしました。

どのように機能するのか?InvisibleKeysのマルチクラウドウォレットは、ユーザーの秘密鍵を2つ以上のクラウドストレージサービス(例:Google Drive、Dropboxなど)に分散して保存します。これにより、仮にそのうちの1つのサービスが侵害されても、鍵が漏洩することはありません。 このマルチクラウドアプローチは特許取得済みのワークフロー(特許番号:US20190095628)に基づいており、非常に高いセキュリティレベルを確保しながら、マルチデバイス間の同期を可能にします。

クラウドからアカウントをインポートするには、MetaMaskがGoogle Drive(またはその他のストレージサービス)内のJSONファイルを読み取ります。このファイルには公開鍵の配列が含まれています。次に、トランザクションに署名する際、秘密鍵(PK)は外部Webアプリ上で一時的かつローカルに再構成されます。このWebアプリはMetaMaskからの署名リクエストを受け取り、トランザクションに署名し、署名済みトランザクションをMetaMaskに返します。鍵は永続的に保存されることなく、メモリ上に一時的にのみ存在します。さらに、ユーザーの完全なコントロールを維持するため、PKはMetaMask拡張機能からアクセスされることはありません。ウォレットがロック解除された場合や侵害された場合でも、PKが盗まれることはありません。

InvisibleKeysでは、すべてがクラウドに保存されているため、世界中どこからでもウォレットにアクセスできます。これは従来のMetaMaskのアプローチを補完するものであり、MetaMaskの高い使いやすさとUIインターフェースの基準を維持しています。

アーキテクチャ

InvisibleKeysは、MetaMaskウォレットと外部Webアプリという2つの主要コンポーネントで動作します。

ウォレットの外部でトランザクションに署名することを意図しているため、コードのハードウェアウォレット接続・署名部分を活用しました。トランザクションを特定のハードウェアに送信して署名する代わりに、署名処理を実行するWebアプリのポップアップが開きます。

Webアプリでは、ユーザーがクラウドの認証情報で認証を行い、秘密鍵が取得され、トランザクションが署名され、鍵がメモリから消去されます。最後にポップアップが閉じられ、ユーザーは署名済みトランザクションの送信を待機していたMetaMaskウォレットに戻ります。

2つのコンポーネントはブラウザのwindow web APIを通じて通信します。両者の間でプライベートな情報がやり取りされることはないため、このコンテキストではこれで十分です。

UIの変更

まず、InvisibleKeysの接続ストラテジーをMetaMaskに追加するために、いくつかのUI変更を行いました。これは、新しいサポート対象ハードウェアウォレットをアプリに追加する方法と同様に追加されます。

以下は、コードに加えられた変更の一部です。新しいデバイスとタイプ、およびUI用の新しいボタンが追加されています。

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に実装しました。最初のメソッドはポップアップを開き、アドレスの配列を含むメッセージを待ちます。2番目のメソッドは、メッセージにエンコードされた未署名のトランザクションとともにポップアップを開きます。その後、エンコードされた署名済みトランザクションを待ち、それが返されてメインコントローラーが送信します。ポップアップが閉じられるのを待つ間、MetaMaskはローディング状態になります。

これらのメソッドは、Webアプリの接続と通信を担当する別のクラスInvisibleConnectを呼び出します。

./InvisibleConnect.js

export default class InvisibleConnect {

  {...}

  const appUrl = 'http://url-to-web-app';

  async getAccountsCloud() {
    let newAccounts = []

    //create a listener for the imported accounts
    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) {
          //when the popup is closed
          clearInterval(interval)
          this.accounts = newAccounts
          resolve(newAccounts)
        }
      }, 1000)
    })
  }

  async signTxCloud(transaction) {
    let signedTx = {}

    //create a listener for the 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') //open popup

      child.postMessage(
        //sends message to the popup
        {
          event_id: 'unsignedTx',
          data: {
            tx: transaction.serialize().toString('hex'),
          },
        },
        '*',
      )

      const interval = setInterval(() => {
        if (child.closed) {
          //when the popup is 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) //resolve the new 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))
      }
    })
  }

   {...}
}
コピー

外部アプリ

このアプリには2つのページがあります。1つはアカウントをインポートするページ、もう1つはトランザクションに署名するページです。

最初のページでは、ユーザーが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;
};
コピー

2番目のページでは、ユーザーはGoogle DriveとDropboxの両方に認証する必要があります(この例で使用したサービスです)。このアプローチはあらゆるストレージサービスと互換性があります。要件は、それらが互いに共謀しない独立したサービスであることです。認証後、それらのサービスから各情報が取得され、前述の特許に記載されたメカニズムに従って実装されたdecrypt()メソッドを使用して秘密鍵が再構成され、トランザクションが署名されます。その直後、秘密鍵はメモリから消去されます。最後に、署名済みトランザクションがMetaMaskウォレットに送り返されます。

以下はコードのステップバイステップの説明です。コードの関連部分のみを示しています。UIの状態や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; //erase from memory

     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;
}
コピー

あなた自身とチームについて少し教えてください。

私たちはポルトガルのブラガを拠点とする、分散システムに強いバックグラウンドを持つ熱意あるソフトウェアエンジニアのチームです。ブロックチェーンとWeb3空間における難しい問題の解決に特化したスタートアップ、Invisible Labを設立しました。

MetaMask Snapsを最初に知ったのはいつで、どのような体験でしたか?

MetaMask SnapsをはじめてSDKのローンチ時に知りました。それ以来、テクノロジーは大幅に進化しており、今も進化し続けています。

MetaMask Snapsが他のウォレットと異なる点は何ですか?

MetaMask Snapsウォレットは、何百万人ものユーザーがすでに毎日使い慣れているデザインを維持しながら、その上に機能を構築できる点が特徴です。

MetaMask SnapsでSnapsを構築することは、あなたとチームにとってどのような体験ですか?

私たちはMetaMask Snapsの予備的なバージョンを使用しましたが、このプロジェクトに必要なすべての機能がサポートされているわけではありませんでした。しかし、私たちが構築したすべてのものは、近い将来、Snapsを使ってより簡単かつスムーズに構築できるようになると確信しています。

MetaMask Snapsはあなたにとって何を意味しますか?

カスタマイズ性、アップグレード可能性、そしてMetaMask Walletを基盤としてイノベーションを構築するための無限の可能性を意味します。

MetaMask SnapsがWeb3空間にもたらす可能性について、どのような機会があると思いますか?

私たちが興味深いと感じる素晴らしい機会の一つは、追加のセキュリティストラテジーを構築することです。各ユーザーが望む経験とリスクレベルに応じて、異なるセキュリティレベルを作成することができます。これは、残念ながらこの分野で頻繁に発生しているセキュリティ問題や技術的なハッキングへのユーザーのエクスポージャーを減らすうえで、大きな影響をもたらす可能性があります。これらの問題を軽減することは、Web3の大衆普及に向けて不可欠です。

MetaMask Snapsを試したい開発者へのアドバイスはありますか?

ためらわずに飛び込んでください!MetaMask Snapsで何が実現できるか、きっと驚くはずです。新しいブロックチェーンの統合、機能の追加、セキュリティアプローチの実装など、可能性は無限大です!

MetaMask Snapsで構築を始める

MetaMask Snapsを始めるには:

  1. 開発者ドキュメントをご確認ください

  2. MetaMask Flaskをインストールしてください

  3. MetaMask Snapsガイドをご覧ください

  4. TwitterGitHubディスカッションDiscordで私たちとつながり続けてください

次回のハックソンであなたの近くで開催されるイベントにも、ぜひご注目ください!Happy BUIDLing ⚒️

免責事項:MetaMask Snapsは一般的に、Consensys Software以外のサードパーティによって開発されています。サードパーティが開発したMetaMask Snapsの使用は、ご自身の判断とリスクのもとで行われ、そのような活動から生じる損失や損害についてはお客様が単独で責任を負うことに同意したうえで使用するものとします。Consensysはサードパーティが開発したMetaMask Snapsに関して、口頭または書面による明示的または黙示的な保証を一切行わず、サードパーティが開発したMetaMask Snapsに関するすべての責任を否認します。ブロックチェーン関連ソフトウェアの使用にはリスクが伴い、MetaMask Snapsを使用する際にはそれらのリスクをすべて自己負担するものとします。

AIによる翻訳です。エラーが含まれている可能性があります。常に情報を確認してください。

翻訳を評価する
  • MetaMask
    MetaMask

    ConsenSysが構築した、Web3への入口となる主要なセルフカストディ型暗号資産ウォレット。

    すべての記事を読む