[Android]あれ?リリースビルドのキーのパスワードなんだっけ?
はじめに
Android アプリのリリースビルドを作成する際に必要なKeyStoreのパスワードを忘れてしまった経験はありませんか?
私も先日、開発中のアプリで KeyStorePassword と KeyPassword の両方を忘れてしまい、リリースビルドができない状況に陥りました。幸い、Android Studio の「remember password」機能でパスワードを保存していた PC があったのですが、そこからパスワードを復旧する方法を調べてみました。
この記事では、PKCS12 形式のキーストアに対して Python を使った辞書攻撃でパスワードを復旧する方法を紹介します。他人のファイルを攻撃しないでください
前提条件
- PKCS12 形式のキーストアファイル(
.jks) - パスワードに使用されそうな単語のリスト
- Python 3.x + cryptography ライブラリ
Android Studio からの直接復旧は困難
まず、Android Studio の「remember password」機能で保存されたパスワードの直接復旧を試みました。
試した方法
-
Google 公式が出しているリカバリツール
Google 公式だし解決するかなと思ったのですが、.keystore ファイルを対象にしてそうであり、今回は.jksなので利用できませんでした。(もしかしたら方法はあるのかもしれませんが見つけれませんでした) -
AndroidStudio のログを確認
~/Library/Logs/Google/{AndroidStudioのバージョン}このフォルダを適当なエディタで開いて文字列検索で
passwordやkeystoreなどの単語で検索してみましたが、全て*でマスクされており判別ができませんでした。
結果として、これらの方法では直接的な復旧は困難でした。
そこで思ったのが、ある程度単語は予想できているし総当たりで解決できたりしないかなと考えて総当たりで確かめるプログラムを Claude くんに書いてもらいました。
キーストア形式の確認
次に、キーストアファイルの形式を確認しました。
file /path/to/your/keystore.jks
file コマンドで形式を確認しようとしたのですが、dataとしか表示されず Claude に相談したところ、hextdump コマンドでヘッダの部分を見ると判別できるよとのことだったので、以下のコマンド実行して Claude に見せてみることに
hexdump -C /path/to/your/keystore.jks | head -3
このコマンドの結果を見せたところPKCS12という形式らしいので以降はこれが正しいと仮定しました。
Python によるブルートフォース攻撃ツールの作成
アイデア
- 辞書ファイルから基本単語を読み込み
- パスワードバリエーションを自動生成
- マルチスレッドで高速化
- ストアパスワードとキーパスワードの両方に対応
===これより下は大部分を AI が書いています。===
パスワードバリエーション生成の工夫
基本単語から以下のようなバリエーションを生成:
# 元の単語: "myapp"
variants = [
"myapp", # 元の単語
"MyApp", # キャピタライズ
"MYAPP", # 全て大文字
"myapp123", # 数字付き
"myapp2024", # 年号付き
"myapp!", # 記号付き
"myapp_android", # 単語結合
# ... 数百〜数千のバリエーション
]
実装のポイント
1. PKCS12 形式への対応
from cryptography.hazmat.primitives.serialization import pkcs12
def test_store_password(self, password):
try:
private_key, cert, additional_certs = pkcs12.load_key_and_certificates(
self.keystore_data,
password.encode('utf-8')
)
return True, (private_key, cert, additional_certs)
except Exception:
return False, None
2. マルチスレッド処理
def worker_thread_store(self, password_queue):
while not self.stop_flag.is_set():
password = password_queue.pop(0) if password_queue else None
if not password:
break
success, keystore_data = self.test_store_password(password)
if success:
self.found_store_password = password
self.stop_flag.set()
break
3. 進捗表示とパフォーマンス
# 1000回ごとに進捗表示
if self.attempts % 1000 == 0:
elapsed = time.time() - self.start_time
rate = self.attempts / elapsed
print(f"⏳ 試行回数: {self.attempts:,} | 速度: {rate:.1f}/秒")
<details>
<summary>完全なPythonコード(クリックして展開)</summary>
#!/usr/bin/env python3
"""
PKCS12 Keystore Password Brute Force Tool
パスワード辞書から様々なバリエーションを生成してブルートフォース攻撃を実行
"""
import itertools
import threading
import time
from pathlib import Path
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import pkcs12
import sys
import argparse
class PKCS12BruteForce:
def __init__(self, keystore_path, wordlist_path, max_threads=4, mode='store'):
self.keystore_path = Path(keystore_path)
self.wordlist_path = Path(wordlist_path)
self.max_threads = max_threads
self.mode = mode # 'store', 'key', or 'both'
self.found_store_password = None
self.found_key_password = None
self.attempts = 0
self.start_time = None
self.stop_flag = threading.Event()
# キーストアファイルを読み込み
try:
with open(self.keystore_path, 'rb') as f:
self.keystore_data = f.read()
except Exception as e:
print(f"❌ キーストアファイルの読み込みエラー: {e}")
sys.exit(1)
def load_wordlist(self):
"""辞書ファイルから単語リストを読み込み"""
try:
with open(self.wordlist_path, 'r', encoding='utf-8') as f:
words = [line.strip() for line in f if line.strip()]
print(f"📖 辞書から {len(words)} 個の基本単語を読み込みました")
return words
except Exception as e:
print(f"❌ 辞書ファイルの読み込みエラー: {e}")
sys.exit(1)
def generate_password_variants(self, words, max_combinations=2):
"""
基本単語から様々なパスワードバリエーションを生成
Args:
words: 基本単語リスト
max_combinations: 最大結合数
"""
variants = set()
print("🔄 パスワードバリエーションを生成中...")
for word in words:
# 元の単語
variants.add(word)
# 大文字小文字のバリエーション
variants.add(word.lower())
variants.add(word.upper())
variants.add(word.capitalize())
variants.add(word.title())
# 数字付きバリエーション
for i in range(10):
variants.add(f"{word}{i}")
variants.add(f"{i}{word}")
# 年号付きバリエーション
for year in ['2020', '2021', '2022', '2023', '2024', '2025']:
variants.add(f"{word}{year}")
variants.add(f"{year}{word}")
# 記号付きバリエーション
for symbol in ['!', '@', '#', '$', '%', '123', '!!!']:
variants.add(f"{word}{symbol}")
variants.add(f"{symbol}{word}")
# 単語の組み合わせ(2つまで)
if max_combinations >= 2:
for word1, word2 in itertools.combinations(words, 2):
variants.add(f"{word1}{word2}")
variants.add(f"{word2}{word1}")
variants.add(f"{word1}_{word2}")
variants.add(f"{word2}_{word1}")
variants.add(f"{word1}-{word2}")
variants.add(f"{word2}-{word1}")
# 3つの組み合わせ(オプション)
if max_combinations >= 3 and len(words) <= 20: # 計算量制限
for word1, word2, word3 in itertools.combinations(words, 3):
variants.add(f"{word1}{word2}{word3}")
variants.add(f"{word1}_{word2}_{word3}")
variants_list = list(variants)
print(f"🎯 {len(variants_list)} 個のパスワード候補を生成しました")
return variants_list
def test_store_password(self, password):
"""ストアパスワードのテスト"""
if self.stop_flag.is_set():
return False
try:
# PKCS12キーストアを開く試行
private_key, cert, additional_certs = pkcs12.load_key_and_certificates(
self.keystore_data,
password.encode('utf-8')
)
return True, (private_key, cert, additional_certs)
except Exception:
return False, None
def test_key_password(self, store_password, key_password):
"""キーパスワードのテスト(ストアパスワード判明後)"""
if self.stop_flag.is_set():
return False
try:
# まずストアを開く
private_key, cert, additional_certs = pkcs12.load_key_and_certificates(
self.keystore_data,
store_password.encode('utf-8')
)
# 秘密鍵が暗号化されている場合のテスト
if private_key:
try:
# 秘密鍵をPEMエンコードして再度読み込みテスト
if hasattr(private_key, 'private_bytes'):
pem_data = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
return True
except Exception:
# 追加のキーパスワードが必要な場合の処理
pass
return True # PKCS12では通常ストアパスワード=キーパスワード
except Exception:
return False
def worker_thread_store(self, password_queue):
"""ストアパスワード用ワーカースレッド"""
while not self.stop_flag.is_set():
try:
password = password_queue.pop(0) if password_queue else None
if not password:
break
self.attempts += 1
success, keystore_data = self.test_store_password(password)
if success:
self.found_store_password = password
self.keystore_contents = keystore_data
self.stop_flag.set()
print(f"\n🎉 ストアパスワードが見つかりました: {password}")
break
# 進捗表示
if self.attempts % 1000 == 0:
elapsed = time.time() - self.start_time
rate = self.attempts / elapsed if elapsed > 0 else 0
print(f"⏳ 試行回数: {self.attempts:,} | 速度: {rate:.1f}/秒 | 現在: {password[:20]}...")
except IndexError:
break
except Exception as e:
print(f"⚠️ エラー: {e}")
def worker_thread_key(self, password_queue, store_password):
"""キーパスワード用ワーカースレッド"""
while not self.stop_flag.is_set():
try:
password = password_queue.pop(0) if password_queue else None
if not password:
break
self.attempts += 1
if self.test_key_password(store_password, password):
self.found_key_password = password
self.stop_flag.set()
print(f"\n🎉 キーパスワードが見つかりました: {password}")
break
# 進捗表示
if self.attempts % 1000 == 0:
elapsed = time.time() - self.start_time
rate = self.attempts / elapsed if elapsed > 0 else 0
print(f"⏳ 試行回数: {self.attempts:,} | 速度: {rate:.1f}/秒 | 現在: {password[:20]}...")
except IndexError:
break
except Exception as e:
print(f"⚠️ エラー: {e}")
def worker_thread(self, password_queue):
"""従来のワーカースレッド(後方互換)"""
self.worker_thread_store(password_queue)
def brute_force_store(self, passwords):
"""ストアパスワードのブルートフォース"""
print(f"\n🚀 ストアパスワードのブルートフォース攻撃を開始 ({self.max_threads} スレッド)")
print(f"📊 総候補数: {len(passwords):,}")
print("-" * 60)
self.start_time = time.time()
self.attempts = 0
self.stop_flag.clear()
password_queue = passwords.copy()
# ワーカースレッド作成・開始
threads = []
for i in range(self.max_threads):
thread = threading.Thread(target=self.worker_thread_store, args=(password_queue,))
thread.daemon = True
thread.start()
threads.append(thread)
# スレッド終了待機
for thread in threads:
thread.join()
elapsed = time.time() - self.start_time
return self.found_store_password, elapsed
def brute_force_key(self, passwords, store_password):
"""キーパスワードのブルートフォース"""
print(f"\n🔑 キーパスワードのブルートフォース攻撃を開始 ({self.max_threads} スレッド)")
print(f"📊 総候補数: {len(passwords):,}")
print("-" * 60)
self.start_time = time.time()
self.attempts = 0
self.stop_flag.clear()
password_queue = passwords.copy()
# ワーカースレッド作成・開始
threads = []
for i in range(self.max_threads):
thread = threading.Thread(target=self.worker_thread_key, args=(password_queue, store_password))
thread.daemon = True
thread.start()
threads.append(thread)
# スレッド終了待機
for thread in threads:
thread.join()
elapsed = time.time() - self.start_time
return self.found_key_password, elapsed
def run(self, max_combinations=2):
"""メイン実行"""
print("🔐 PKCS12 キーストア パスワード ブルートフォース ツール")
print("=" * 60)
words = self.load_wordlist()
passwords = self.generate_password_variants(words, max_combinations)
if self.mode == 'store' or self.mode == 'both':
# ストアパスワードの解析
store_password, elapsed = self.brute_force_store(passwords)
if store_password:
print(f"\n✅ ストアパスワード見つかりました: {store_password}")
print(f"⏱️ 所要時間: {elapsed:.2f}秒")
print(f"🔢 試行回数: {self.attempts:,}")
# ストアが開けたか詳細確認
try:
private_key, cert, additional_certs = pkcs12.load_key_and_certificates(
self.keystore_data,
store_password.encode('utf-8')
)
print(f"\n📋 キーストア情報:")
print(f" - 秘密鍵: {'あり' if private_key else 'なし'}")
print(f" - 証明書: {'あり' if cert else 'なし'}")
print(f" - 追加証明書: {len(additional_certs) if additional_certs else 0}個")
# PKCS12では通常ストアパスワード=キーパスワード
if private_key and self.mode == 'both':
print(f"\n💡 PKCS12では通常、ストアパスワード=キーパスワードです")
print(f" キーパスワードも '{store_password}' である可能性が高いです")
# 念のためキーパスワードもテスト
print(f"\n🔍 念のためキーパスワードの独立解析を実行...")
key_password, key_elapsed = self.brute_force_key(passwords, store_password)
if key_password:
print(f"\n✅ キーパスワード見つかりました: {key_password}")
print(f"⏱️ 所要時間: {key_elapsed:.2f}秒")
else:
print(f"\n💡 キーパスワードはストアパスワードと同じ可能性が高いです")
self.found_key_password = store_password
except Exception as e:
print(f"⚠️ キーストア詳細確認エラー: {e}")
else:
print(f"\n❌ ストアパスワードが見つかりませんでした")
print(f"⏱️ 所要時間: {elapsed:.2f}秒")
print(f"🔢 総試行回数: {self.attempts:,}")
elif self.mode == 'key':
print("❌ キーパスワードのみの解析にはストアパスワードが必要です")
print(" まず --mode store でストアパスワードを見つけてください")
return None
self._print_summary()
return self.found_store_password, self.found_key_password
def _print_summary(self):
"""結果サマリーを表示"""
print(f"\n" + "=" * 60)
print("📊 最終結果")
print("=" * 60)
if self.found_store_password:
print(f"🔐 ストアパスワード: {self.found_store_password}")
else:
print(f"❌ ストアパスワード: 見つかりませんでした")
if self.found_key_password:
print(f"🔑 キーパスワード: {self.found_key_password}")
elif self.found_store_password:
print(f"🔑 キーパスワード: ストアパスワードと同じ可能性が高い")
else:
print(f"❌ キーパスワード: 未解析")
if not self.found_store_password:
print(f"\n💡 ヒント:")
print(f" - word.txt により多くの候補を追加してください")
print(f" - --combinations を増やしてみてください(時間がかかります)")
print(f" - パスワードに使われそうな他の単語を考えてみてください")
def main():
parser = argparse.ArgumentParser(
description='PKCS12キーストアのパスワードブルートフォースツール'
)
parser.add_argument('-k', '--keystore', required=True,
help='キーストアファイルのパス')
parser.add_argument('-w', '--wordlist', required=True,
help='辞書ファイル(word.txt)のパス')
parser.add_argument('-t', '--threads', type=int, default=4,
help='スレッド数 (デフォルト: 4)')
parser.add_argument('-c', '--combinations', type=int, default=2,
help='最大単語結合数 (デフォルト: 2)')
parser.add_argument('-m', '--mode', choices=['store', 'key', 'both'],
default='both',
help='解析モード: store(ストアパスワードのみ), key(キーパスワードのみ), both(両方) (デフォルト: both)')
args = parser.parse_args()
# 必要なライブラリチェック
try:
import cryptography
except ImportError:
print("❌ cryptographyライブラリが必要です:")
print(" pip install cryptography")
sys.exit(1)
# ブルートフォース実行
brute_forcer = PKCS12BruteForce(args.keystore, args.wordlist, args.threads, args.mode)
store_password, key_password = brute_forcer.run(args.combinations)
if store_password or key_password:
print(f"\n🎯 解析完了!")
if store_password:
print(f" ストアパスワード: {store_password}")
if key_password:
print(f" キーパスワード: {key_password}")
return True
else:
return False
if __name__ == "__main__":
main()
</details>
使用方法
1. 辞書ファイルの準備
word.txt
password
myapp
kotlin
quiz
android
2025
secret
key
store
2. ツールの実行
# 必要なライブラリのインストール
pip install cryptography
# ブルートフォース実行
python3 pkcs12_brute.py \
-k "/path/to/keystore.jks" \
-w "./word.txt" \
-m both \
-t 8 \
-c 2
3. 実行結果
🔐 PKCS12 キーストア パスワード ブルートフォース ツール
📖 辞書から 10 個の基本単語を読み込みました
🎯 2,847 個のパスワード候補を生成しました
🚀 ストアパスワードのブルートフォース攻撃を開始 (8 スレッド)
⏳ 試行回数: 1,000 | 速度: 1,250.3/秒
🎉 ストアパスワードが見つかりました: myapp2024!
✅ ストアパスワード見つかりました: myapp2024!
📋 キーストア情報:
- 秘密鍵: あり
- 証明書: あり
💡 PKCS12では通常、ストアパスワード=キーパスワードです
成功のポイント
1. 適切な辞書の準備
- アプリ名、プロジェクト名
- よく使う単語やフレーズ
- 作成時期に関連する年号
2. バリエーション生成の充実
- 大文字小文字の変換
- 数字や記号の追加
- 単語の組み合わせ
3. パフォーマンスの最適化
- マルチスレッド処理
- 効率的なパスワード候補生成
- 進捗の可視化
注意事項とセキュリティ
⚠️ 重要な注意点
-
自分のキーストアにのみ使用
- 他人のキーストアへの攻撃は違法行為です
- 必ず自分が作成したキーストアにのみ使用してください
まとめ
忘れたキーストアパスワードの復旧は決して簡単ではありませんが、適切なツールと辞書があれば復旧できる可能性があります。
この記事が同じような状況に陥った開発者の助けになれば幸いです。ただし、必ず自分のキーストアにのみ使用し、セキュリティベストプラクティスを守ることを忘れないでください。
Discussion