ブロックチェーンにおける ニーモニック(Mnemonic) とは、秘密鍵を安全かつ人間が扱いやすい形で表現するために使われる一連の単語リストです。この仕組みは主にウォレットのバックアップやリカバリーの際に利用され、暗号資産を管理する上で非常に重要な役割を果たします。
一般的にブロックチェーンに使用されるニーモニックは24単語です。
mammal east seek giraffe
scale detail rocket meadow
cigar clever tourist learn
walnut camp hill speak
dial stool club useful
real tornado couch drastic
24単語なのは秘密鍵と同程度以上の強度を保つためと考えられます。
//秘密鍵の組み合わせ数
BigInt(Math.pow(256,32))
115792089237316195423570985008687907853269984665640564039457584007913129639936n
//ニーモニックの組み合わせ数
BigInt(Math.pow(2048,24))
29642774844752946028434172162224104410437116074403984394101141506025761187823616n
十分なセキュリティを確保するのはもちろん重要ですが、
長すぎるニーモニックは逆にその利用機会を減らしてしまう原因にもなりかねません。
また、ブロックチェーンの社会実装を考えたときに、
「家に帰ってマイナンバーカードを見つけるまでの間だけ」セキュリティが確保されていればよい、といった要件があることも想像できます。
そこで、今回はなじみのある日本語で短いニーモニックを作成する方法をご紹介します。
単に作成するだけではなく、BIP-39という標準化されたニーモニックに対応したライブラリでも扱えるようにするのが目標です。
日本語ニーモニック
BIP-39では日本語ニーモニックも定義されています。
しかし、企業による社会実装を考えたときに好ましい単語のチョイスとは言い難いリストです。
そこでSIP-3939というSymbol改善提案をしようと思っています。
以下のような仕様を検討中です。
BIP-39
BIP-39 で使用するひらがなの数 74文字
一単語あたりの文字数は3文字から6文字
SIP-3939
SIP-3939 で使用するひらがなの数 46文字(濁音や促音を除外)
一単語あたりの文字数は3文字か4文字のみ
固有名詞、動詞などの活用語を除外、小学生の辞書から抽出。
ニーモニック生成体験
それでは短い日本語ニーモニックを生成してみて、BIP-39に準拠させてみましょう。
ワードリストの作成
Githubに公開されているワードリストを読み込みます。
async function fetchWordlist(urlPath) {
const response = await fetch(urlPath);
const data = await response.text();
const wordlist = data.split('\n').map(line => line.trim()).filter(line => line !== "");
return wordlist;
}
cutomWordlistUrl = "https://raw.githubusercontent.com/kicnft/SIPs/refs/heads/main/SIPS/sip-3939/japanese_positive.txt";
wordlistUrl = "https://raw.githubusercontent.com/bitcoin/bips/refs/heads/master/bip-0039/english.txt"
customWordlist = await fetchWordlist(cutomWordlistUrl);
wordlist = await fetchWordlist(wordlistUrl);
console.log(customWordlist);
console.log(wordlist);
wordlistがBIP-39で定義されている標準化されたニーモニック、customWordlistが私の提案する日本語ノンネガティブニーモニックです。
5単語のニーモニックリストを作成する
乱数を発生させて0〜2047までの範囲の数字を5つ生成し、customWordlistを使用して単語リストを生成します。
mnemonic = []
for (let i = 0; i < 5; i++) {
const randomArray = new Uint32Array(1); // 必要な範囲の乱数生成
crypto.getRandomValues(randomArray);
const randomIndex = randomArray[0] % customWordlist.length; // 範囲を調整
console.log(randomIndex)
mnemonic.push(customWordlist[randomIndex]);
}
console.log(mnemonic);
ワードリストから変換
生成したワードリストからindex値のリスト、ビット配列を生成します。
indices = mnemonic.map(word => customWordlist.indexOf(word));
initialEntropyBits = mnemonic.map(word => customWordlist.indexOf(word).toString(2).padStart(11, '0')).join('');
console.log(indices);
console.log(initialEntropyBits);
indecesが0-2047までの配列、initialEntropyBitsがビット列です。
BIP-39への準拠
BIP-39に必要な量の残りのビット列を生成します。固定シード値にすることで誰でも生成可能にしておきます。
// バイナリデータをビットストリングに変換
function bytesToBinary(bytes) {
return Array.from(bytes)
.map(byte => byte.toString(2).padStart(8, '0'))
.join('');
}
// イベントIDから追加のエントロピーを生成
eventID = "nemsymboladventcalendar2024";
encoder = new TextEncoder();
hashes = await import('https://cdn.skypack.dev/@noble/hashes/sha256');
eventEntropy = await hashes.sha256(encoder.encode(eventID));
eventEntropyBits = bytesToBinary(eventEntropy).slice(0, (256 - initialEntropyBits.length));
ここでは、"nemsymboladventcalendar2024"という固定値をハッシュ化して利用することにしました。
チェックサムの追加
5単語ニーモニックから変換したビット列とBIP-39に準拠させるために固定値から生成したビット列を足し合わせてチェックサムを計算し、末尾に追加します。
// チェックサムを計算して付加する関数
async function addChecksum(entropyBits) {
const entropyBytes = binaryToBytes(entropyBits);
const hash = await hashes.sha256(entropyBytes);
const checksumBits = bytesToBinary(hash).slice(0, entropyBytes.length * 8 / 32);
return entropyBits + checksumBits;
}
// バイナリデータをビットストリングに変換
function binaryToBytes(binary) {
const bytes = [];
for (let i = 0; i < binary.length; i += 8) {
bytes.push(parseInt(binary.slice(i, i + 8), 2));
}
return new Uint8Array(bytes);
}
// 組み合わせたエントロピーを計算
totalEntropyBits = initialEntropyBits + eventEntropyBits;
entropyWithChecksum = await addChecksum(totalEntropyBits);
シード値の出力
少し脱線してBIP-39準拠のニーモニックビット列を16進数変換してみます。
Array.from(binaryToBytes(entropyWithChecksum))
.map(byte => byte.toString(16).padStart(2, '0')) // 各バイトを16進数に変換し、2桁に調整
.join(''); // 全てのバイトを連結して1つの文字列にする
この変換作業はBIP-39に準拠したビット列に対してのみ行ってください。たとえば5単語ニーモニックをビット列に変換した場合、11ビット列 x 5 なので偶数個にならず、末尾の情報が欠損してしまいます。
フルサイズのニーモニックに変換
最後に一般的なBIP-39対応ライブラリで読み込めるように24文字ニーモニックに変換します。
function generateMnemonic(entropyWithChecksum, wordlist) {
const chunkSize = 11; // 11ビットごとに分割
const mnemonic = [];
// 11ビットごとに分割して処理
for (let i = 0; i < entropyWithChecksum.length; i += chunkSize) {
const binaryChunk = entropyWithChecksum.slice(i, i + chunkSize); // 11ビットを取得
const index = parseInt(binaryChunk, 2); // 2進数を10進数に変換
mnemonic.push(wordlist[index]); // 単語リストから対応する単語を取得
}
return mnemonic;
}
fullMnemonic = generateMnemonic(entropyWithChecksum, wordlist).join(" ");
fullCustomMnemonic = generateMnemonic(entropyWithChecksum, customWordlist).join(" ");
console.log(fullMnemonic);
console.log(fullCustomMnemonic);
これで、5単語で生成したニーモニックをSymbol-SDKでも使用できるニーモニックに変換することができました。
まとめ
昨今、マイナンバーカードとブロックチェーンを紐づけるような試みが始まっていますが、ここで提示したのは少しベクトルの違う試みです。
「今、手元にカードがなくても、後日紐づけるまでの間を完全にトレースできる状態にしておく」とか「マイナンバーカードを持ち歩かずに、いつでも思い出せるサイズのニーモニックに対してマイナンバーの鍵で署名する」とか、私が考えているのはそういった未来です。何か心に刺さった人がいれば幸いです。
補足
5単語ニーモニックのセキュリティ強度の検証をします。
10万人が集まるイベントで5単語のニーモニックを配布し、1秒に1万回の攻撃で誰かのニーモニックを特定できる確率が5%になる期間を調査しました。
強度計算には誕生日の衝突問題をベースにした試行回数の計算に基づいています。
1. 衝突確率の計算
誕生日の問題では、総組み合わせ数 ( N ) と要素数 ( m ) に基づき、成功確率 ( P(k) ) に達するための試行回数 ( k ) は以下のように表されます:
1 - \exp\left(-\frac{k \cdot m}{N}\right) = P(k)
ここで:
- ( N ): 総組み合わせ数(ニーモニックの全パターン数、( 2048^5 ))
- ( m ): 配布されているニーモニックの数(10万人 = ( 100,000 ))
- ( k ): 試行回数
- ( P(k) ): 衝突の成功確率(今回は5% = ( 0.05 ))
2. 試行回数 ( k ) を求める
式を試行回数 ( k ) について解くと:
k = -\frac{N}{m} \ln(1 - P(k))
ここで、以下の値を代入します:
- ( N = 2048^5 = 1,125,899,906,842,624 )
- ( m = 100,000 )
- ( P(k) = 0.05 )
\ln(1 - P(k)) = \ln(0.95) \approx -0.0513
3. 時間計算
試行回数 ( k ) が得られたら、攻撃者の試行速度(1秒間に1万回)を用いて時間を計算します:
\text{時間 (秒)} = \frac{k}{\text{試行速度}}
試行速度が1万回/秒の場合:
\text{時間 (秒)} = \frac{k}{10,000}
さらに、日数に変換するには:
\text{時間 (日)} = \frac{\text{時間 (秒)}}{60 \cdot 60 \cdot 24}
4. 実際の計算
計算結果に基づき、10万人に5単語のニーモニックを配布し、攻撃者が1秒間に1万回の試行を行った場合、誰か1人のニーモニックが特定されるまでに5%の成功確率に達する試行回数と時間は以下の通りです:
- 必要な試行回数: 約 184.8億回
- 試行にかかる時間(秒): 約 1,848,036秒
- 試行にかかる時間(日): 約 21.39日
解釈
- 攻撃者が試行を続けた場合、約 21日間で5%の成功確率に達することが予想されます
- 4単語ニーモニックにした場合、5%の成功確率に達するのは15分となり、現実的な数字ではなくなります