ブロックチェーンアプリケーションを開発する際に直面する典型的な課題の一つが、秘密鍵の管理です。この記事では、その問題を解決するための一つの手法をご紹介します。新しい方法ではなく既存で存在する方法ですが、 Web3Auth のドキュメントに存在しない、サポートされていないチェーンを繋ぐ際の方法としてどなかたの役に立てば幸いです。
注意事項
- ここで紹介する方法は、あくまでも一つの手段に過ぎません。より安全な方法が多く存在するため、特に企業がユーザーにウォレットを提供する際は、FireblocksやMPC(マルチパーティ計算)などの技術を活用することをお勧めします
- 提供するコードはサンプルであり、実際に公開する際には次の点を適切に実装してください
- 秘密鍵(Private Key)は決して簡単に露出させないでください。このサンプルコードでは、デモ用として alert を用いて即秘密鍵を表示していますが、実際の運用ではこれを避けるべきです
- Buffer や process などの Node.js 依存のモジュールは、使用するフレームワークに応じて適切にバンドルしてください
- 本件の方法がカストディな運用に相当してしまうのかは、ご自身で金融庁へお問い合わせください
- 本件はブラウザをリセットすると 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 + 分散署名等、より良い方法がありますが、資産管理が目的でないアプリケーションでは、もう少し軽量に運用・使用できる方法が適しているケースもあります。ご自身の開発するアプリケーションに合わせたツール選定を行いましょう。