7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

nem / symbolAdvent Calendar 2024

Day 5

BLOCKCHAIN の秘密鍵をライトにブラウザで安全に取り扱う

Posted at

ブロックチェーンアプリケーションを開発する際に直面する典型的な課題の一つが、秘密鍵の管理です。この記事では、その問題を解決するための一つの手法をご紹介します。新しい方法ではなく既存で存在する方法ですが、 Web3Auth のドキュメントに存在しない、サポートされていないチェーンを繋ぐ際の方法としてどなかたの役に立てば幸いです。

注意事項

  1. ここで紹介する方法は、あくまでも一つの手段に過ぎません。より安全な方法が多く存在するため、特に企業がユーザーにウォレットを提供する際は、FireblocksやMPC(マルチパーティ計算)などの技術を活用することをお勧めします
  2. 提供するコードはサンプルであり、実際に公開する際には次の点を適切に実装してください
    1. 秘密鍵(Private Key)は決して簡単に露出させないでください。このサンプルコードでは、デモ用として alert を用いて即秘密鍵を表示していますが、実際の運用ではこれを避けるべきです
    2. Buffer や process などの Node.js 依存のモジュールは、使用するフレームワークに応じて適切にバンドルしてください
  3. 本件の方法がカストディな運用に相当してしまうのかは、ご自身で金融庁へお問い合わせください
  4. 本件はブラウザをリセットすると SSS のシェアも一部消失してしまいます。実際に公開する際には Web3Auth のドキュメントに従って、復旧用のデバイスを設定するか、復旧用のシェアをユーザーにテキストで保管させる、事業者側で保管する、等、シェアの過半数を維持出来るようにして下さい

紹介

本記事は Web3Auth を使用して、ブロックチェーンの秘密鍵を SSS によって一時的に合成、署名し、多少安全に秘密鍵を使用する方法を紹介します。この方法の立ち位置は、秘密鍵をブラウザストレージに暗号化して保存するよりはもう少し安全な、運用方法となります。ライトな運用方法であり、小銭の管理用 WALLET やちょっとしたゲームの秘密鍵等、資産管理程の重要性を求めない、かつ UX を高めたい場合に良いでしょう。(UX とセキュリティのバランスを慎重に検討下さい。)

前提

  • Web3Auth のアカウントを作成した
  • Web3Auth で ClientID を発行した
  • SSS を使用します(MPC ではありません)
  • Web3Auth 上での種別は OTHER
  • フロントエンド上で秘密鍵を取り扱います
  • Node.js vite

インストール

package.json を以下の通りにしましょう

npm install @vitejs/plugin-react @web3auth/base @web3auth/modal empty-module buffer process symbol-sdk 
{
  "name": "web3auth-vite",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
  },
  "dependencies": {
    "@web3auth/base": "^9.4.5",
    "@web3auth/modal": "^9.4.5",
    "buffer": "^6.0.3",
    "empty-module": "^0.0.2",
    "process": "^0.11.10",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "symbol-sdk": "^3.2.3"
  },
  "devDependencies": {
    "@eslint/js": "^9.15.0",
    "@types/react": "^18.3.12",
    "@types/react-dom": "^18.3.1",
    "@vitejs/plugin-react": "^4.3.4",
    "@vitejs/plugin-react-swc": "^3.5.0",
    "eslint": "^9.15.0",
    "eslint-plugin-react-hooks": "^5.0.0",
    "eslint-plugin-react-refresh": "^0.4.14",
    "globals": "^15.12.0",
    "typescript": "~5.6.2",
    "typescript-eslint": "^8.15.0",
    "vite": "^6.0.1"
  }
}

依存関係の解決

Web3Auth のライブラリは一部ブラウザには存在しない API を参照する箇所があり、エラーとなります。 index.html を以下のように置き換えてください。

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- ここね -->
    <script type="module">
      import { Buffer } from "buffer";
      import process from "process";
      window.Buffer = Buffer;
      window.process = process;
    </script>
    <!-- ここまで -->
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + React + TS</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

vite.config.ts も少し修正します。

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      crypto: "empty-module",
    },
  },
  define: {
    global: "globalThis",
    "process.env": {},
  },
  build: {
    target: "esnext",
  },
});

コード

取り敢えず component 1つにぎゅっと収めておきました。コピペで動くと思います。 ClientID のみ Web3Auth で発行したものにしてください。

import "./App.css";

import { CHAIN_NAMESPACES, IProvider, WEB3AUTH_NETWORK } from "@web3auth/base";
import { CommonPrivateKeyProvider } from "@web3auth/base-provider";
import { Web3Auth } from "@web3auth/modal";
import { useEffect, useState } from "react";
import { PrivateKey } from "symbol-sdk";
import { SymbolFacade } from "symbol-sdk/symbol";

const chainConfig = {
  chainNamespace: CHAIN_NAMESPACES.OTHER,
  chainId: "0x1",
  rpcTarget: "RPC_URL",
};

const privateKeyProvider = new CommonPrivateKeyProvider({
  config: { chainConfig },
});

const web3auth = new Web3Auth({
  clientId: ここに client id を入れてね,
  web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET,
  privateKeyProvider,
});

function App() {
  const [provider, setProvider] = useState<IProvider | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);

  useEffect(() => {
    const init = async () => {
      try {
        await web3auth.initModal();
        setProvider(web3auth.provider);

        if (web3auth.connected) {
          setLoggedIn(true);
        }
      } catch (error) {
        console.error(error);
      }
    };

    init();
  }, []);

  const login = async () => {
    const web3authProvider = await web3auth.connect();
    setProvider(web3authProvider);
    if (web3auth.connected) {
      setLoggedIn(true);
    }
  };

  const getUserInfo = async () => {
    const user = await web3auth.getUserInfo();
    uiConsole(user);
  };

  const logout = async () => {
    await web3auth.logout();
    setProvider(null);
    setLoggedIn(false);
    uiConsole("logged out");
  };

  const getAccount = async () => {
    if (!provider) {
      return;
    }
    const privateKey = await provider.request({ method: "private_key" });

    if (typeof privateKey === "string") {
      const account = new SymbolFacade("testnet").createAccount(
        new PrivateKey(privateKey)
      );
      alert({
        address: account.address,
        publicKey: account.publicKey,
        privateKey: privateKey,
      });
    }
  };

  function uiConsole(...args: unknown[]): void {
    const el = document.querySelector("#console>p");
    if (el) {
      el.innerHTML = JSON.stringify(args || {}, null, 2);
      console.log(...args);
    }
  }

  const loggedInView = (
    <>
      <div className="flex-container">
        <div>
          <button onClick={getUserInfo} className="card">
            Get User Info
          </button>
        </div>
        <div>
          <button onClick={getAccount} className="card">
            Get Account
          </button>
        </div>
        <div>
          <button onClick={logout} className="card">
            Log Out
          </button>
        </div>
      </div>
    </>
  );

  const unloggedInView = (
    <button onClick={login} className="card">
      Login
    </button>
  );

  return (
    <div className="container">
      <div className="grid">{loggedIn ? loggedInView : unloggedInView}</div>
      <div id="console" style={{ whiteSpace: "pre-line" }}>
        <p style={{ whiteSpace: "pre-line" }}></p>
      </div>
    </div>
  );
}

export default App;

使い方

PrivateKey は Web3Auth の sdk 側で生成される為、 symbol-sdk 側で作成する必要はありません。また、保存する必要もありません。SSS のシェアとして、ブラウザや、Web3Auth のサーバー側、SSS Node 側に分散して保存されます。alert で表示されたアカウントに対して faucet からテスト用のトークンを送信し、送金等のテストを行ってみてください。

通常の symbol-sdk で発行した秘密鍵と同様にトランザクションの作成を行えます。

ポイント

安全性を追求すると、ユーザー自身での秘密鍵の管理や事業者側での管理(カストディ)、MPC + 分散署名等、より良い方法がありますが、資産管理が目的でないアプリケーションでは、もう少し軽量に運用・使用できる方法が適しているケースもあります。ご自身の開発するアプリケーションに合わせたツール選定を行いましょう。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?