鍵の保管と認証
BitTool Signer は、各ウォレットのリカバリーフレーズ(と、設定されている場合は BIP39 パスフレーズ)を、それぞれ専用の暗号化ファイルに保存します。ファイルを開くには、独立した 2 つの鍵が必要です。1 つはウォレットパスワードから導出した鍵、もう 1 つはその端末上だけで iOS が保管する鍵です。
このページは iPhone 版を基準に説明しています。Android 版 BitTool Signer は、同じ 2 層の暗号化を Android 自身の鍵保管の仕組みで使います。Android 版を参照してください。
Vault
各ウォレットは、アプリ専用の Library/Application Support/BitToolSigner/ ディレクトリ内の Vaults/<UUID>.bsvault というファイルです。作成の手順は次のとおりです。
SecRandomCopyBytesで新しい 32 バイトのソルトを生成します。- 256 ビットの パスワード鍵 を導出します。PBKDF2-HMAC-SHA256、600,000 回の反復(CommonCrypto)を使います。
- 秘密情報(フレーズ + パスフレーズを JSON にしたもの)を、パスワード鍵を使って AES-256-GCM で暗号化します。96 ビットの新しい nonce と 128 ビットのタグを使います。
- その暗号文を、ランダムな 256 ビットの デバイス鍵 を使って AES-256-GCM で再度暗号化します。
- デバイス鍵を
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly属性でキーチェーンに保存します。iCloud 同期はオフです。iPhone にパスコードが設定されていない場合、ウォレットの作成は失敗します。より弱い方式へのフォールバックはありません。 - 完全なファイル保護、所有者のみのアクセス権、バックアップ除外を設定して、ファイルをアトミックに書き込みます。
vault file
├── header (not secret, but authenticated)
│ format version 2 · KDF parameters · salt · wallet ID · name · created at
└── AES-GCM(device key,
AES-GCM(PBKDF2-SHA256(password, salt, 600 000),
{ mnemonic, passphrase }))
AES-GCM の 2 つの層はいずれも、保存されたヘッダーのバイト列を、ドメインラベル(com.bittool.signer.vault.v2/<layer>)を前置した関連データ(associated data)として認証します。ヘッダーのバイトを 1 つでも編集、再エンコード、並べ替えすると、復号は失敗します。
ウォレットパスワード はローカルの Vault を暗号化するものです。BIP39 パスフレーズではなく、アドレスにも影響しません。ルール:8〜64 文字で、大文字、小文字、数字、記号のうち 3 種類以上を含み、制御文字は使えません。入力したとおりにそのまま使われます(前後の空白の除去や正規化は行いません)。
デバイス鍵は端末から出ることがなく、バックアップからも除外されるため、コピーした Vault ファイルを他の場所で開くことはできません。iPhone の紛失、アプリの削除、パスコードの解除によって、Vault が読めなくなる可能性があります。唯一のバックアップは、書き留めたリカバリーフレーズ(とパスフレーズ)です。
なぜ Argon2id ではなく PBKDF2 なのか
Argon2id はメモリハードであり、パスワード KDF としてはより強力です。このバージョンでは、ネイティブの暗号ライブラリへの依存を増やさないために、CommonCrypto の PBKDF2 を使っています(反復回数は、HMAC-SHA256 に対する現在の OWASP 推奨値である 600,000 回)。KDF のパラメーターはバージョン付きのヘッダーに保存されているため、将来移行する際にも新しいファイルレイアウトは必要ありません。
Opus 5 の セキュリティレビュー は、この選択を弱点ではなく「情報」(BTS-OP5-03)と評価しました。デバイス鍵による 2 層目の暗号化があるため、Vault ファイルだけをコピーした人は、パスワードの探索を始めることすらできないからです。レビューでは、デバイス鍵まで侵害された場合に備えた多層防御として、将来の Vault フォーマットで Argon2id を使うことを提案しています。
パスワードの試行
パスワードの失敗回数は、ウォレットごとに、暗号化・認証された .attempts ファイルで記録されます。回数は時間のかかる鍵導出の 前に 保存されるため、アプリを強制終了してもリセットされません。
| 失敗回数 | 次の試行までの待ち時間 |
|---|---|
| 1–4 | なし |
| 5 | 2 秒 |
| 6 | 4 秒 |
| n | min(2n−4 秒, 5 分) |
正しいパスワードを入力するとカウンターはリセットされます。パスワードを間違えたことを理由に、アプリがウォレットを削除することはありません。これはアプリケーションレベルの制御であり、ハードウェアのカウンターではありません。アプリのファイルをロールバックできる人はリセットできてしまいます。暗号学的な保護を担っているのは KDF です。
パスワード不要の公開データ
アドレス、ペアリング QR コード、アクティビティは、公開鍵しか必要としないため、パスワードなしで利用できます。ウォレットを作成またはインポートしたときに、エンジンは 公開インデックス を一度だけ導出し、Vault の外に保存します。
- 6 つの Bitcoin アカウントノード(BIP84、BIP86、BIP49、BIP44、そして BIP48 の native および nested マルチシグ)
- EVM と TRON のアカウントノード、および TRON アドレス 0〜99
- 3 つの Solana 公開鍵(導出パス を参照)
公開インデックスのファイルには、別のランダムなキーチェーン鍵による HMAC-SHA256 が付いています。検証に失敗した場合(誰かが編集した、または別のウォレットのものである場合)、アプリはファイルが存在しないものとして扱い、再構築のためにパスワードを一度だけ求めます。そのため、アプリのファイルに書き込める攻撃者であっても、自分のアドレスにすり替えることはできません。
承認
- 「ロック解除済み」の状態やグローバルなセッションはありません。署名のたびに、そのウォレットのパスワードが求められます。 リカバリーフレーズの表示、パスワードの変更、ウォレットの削除でも同様です。
- App Lock(任意、Face ID)が保護するのはアプリへのアクセスだけです。署名を承認することはありません。App Lock をオンにすると、アプリはコールドスタート時と、バックグラウンドで 120 秒経過した後にロックされます。この時間は、iPhone のスリープ中もカウントを続ける時計で計測されます。
- アプリがバックグラウンドに移ると、入力されたパスワード、リスクの確認、表示中のリカバリーフレーズは直ちに破棄されます。アプリスイッチャーや画面収録中は、プライバシー用のカバーが画面を隠します。
メモリの衛生管理
BIP39 シードはネイティブコードで計算され、一時バッファーは memset_s で上書きされます。秘密情報や信頼できない入力を扱う各処理は、新しい JavaScriptCore コンテキスト で実行され、処理後に破棄されます。これにより秘密情報がメモリ上に残る時間は短くなりますが、Swift や JavaScript の文字列が確実に消去される保証はないため、アプリは完全なゼロ化をうたっていません。
Android 版
Vault は同じ 2 層構造で、認証されたヘッダーも同じです。異なるのは、デバイス鍵の置き場所と、プラットフォームがアプリをどう保護するかです。
- パスワード層。 PBKDF2-HMAC-SHA256(600,000 回)をネイティブコードで計算し(プロセッサーに ARMv8 の SHA-256 命令があれば使用)、その後、暗号化のたびに新しい 12 バイトのナンスと新しい 32 バイトのソルトで AES-256-GCM を使います。
- デバイス層。 ウォレットごとに Android Keystore の内部で生成され、決してエクスポートできない AES-GCM 鍵です(端末にあれば StrongBox)。Vault には画面ロックが必要です。
- ファイル。 アプリ専用の保存領域にアトミックに書き込まれ、クラウドバックアップと端末間の移行から除外されます。
- パスワードの試行。 iPhone 版と同じく、時間のかかる鍵導出の前にウォレットごとに数えます。カウンターが壊れていれば、すべての試行を拒否します。
- 承認。 署名には毎回ウォレットのパスワードが必要です。アプリロック(任意、指紋または顔)が署名を承認することはありません。すべてのウィンドウはスクリーンショット、画面録画、履歴画面に映らず、Android 12 以降では他のアプリのオーバーレイを隠します。また、ほかのウィンドウが覆っている間、「署名」ボタンとリスクの確認はタップを受け付けません。
- メモリ。 秘密情報は、使い終わると閉じて消去される入れ物に保持され、処理がキャンセルされた場合も同様です。秘密情報を扱う処理はすべて新しい QuickJS ランタイムで動きます。リカバリーフレーズはアプリ独自のキーボードで入力され、どの入力方式からも見えません。JVM ではすべてのコピーの消去を保証できないため、ここでもアプリは完全な消去をうたっていません。
Opus 5.5 のレビューは、この層について深刻度「低」の指摘を 2 件挙げています。Android 8〜11 で Keystore のエラーが起きると、公開ウォレットデータを認証する鍵が作り直されることがある点(BTS-13)と、2 つの補助的な鍵が画面ロックの有無を確認しない点(BTS-14)です。セキュリティレビューを参照してください。