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

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

## 注意事項

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

## 紹介

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

https://web3auth.io/?gad_source=1&gclid=CjwKCAiA9vS6BhA9EiwAJpnXw23LJHyjUIrAPpXm5twWdtMVZ8s1XrLz7E3UzkLH9eNojCbZ5ezjXRoCEN8QAvD_BwE

## 前提

* 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 
```

```json
{
  "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 を以下のように置き換えてください。

```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` も少し修正します。

```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 で発行したものにしてください。

```tsx
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 + 分散署名等、より良い方法がありますが、資産管理が目的でないアプリケーションでは、もう少し軽量に運用・使用できる方法が適しているケースもあります。ご自身の開発するアプリケーションに合わせたツール選定を行いましょう。