Steamログイン用パスワードの暗号化メカニズム解析と実装復元

Steamプラットフォームの認証フローでは、送信前のクレデンシャルに対してクライアントサイドでの非対称暗号化が強制されている。通信パケットを観察すると、RSA方式による公開鍵暗号化が採用されており、公開鍵パラメータはセッション単位で動的に配布される。ここでは通信手順の解析結果に基づき、暗号化ロジックを復元し、Python環境からJavaScript実行Engine経由で呼び出す構成を構築する手順を解説する。

認証プロキシの通信手順

ログインリクエストが実際に送信される前には、以下のような二段階の手順が実行されていることが確認できる。

  • 公開鍵のパラメータ取得: POST /login/getrsakey/ に対してキャッシュ回避用のタイムスタンプパラメータとアカウント名を送付すると、JSONレスポンスとして publickey_mod(モジュール値)と publickey_exp(公開指数)が返却される。
  • 認証情報の送付: 取得した公開鍵を用いてパスワードを暗号化し、POST /login/dologin/ エンドポイントにフォームデータとして送信する。

公開鍵は毎回のセッションで再発行されるため、ハードコーディングではなく都度フェッチする Architecture が必須となる。

フロントエンドにおける暗号化アーキテクチャ

ECMAScriptにはネイティブの大整数演算型が存在しないため、クライアント側のソースコードには任意精度数値演算を行う自作ライブラリ(BigInteger)がバンドルされている。このライブラリはビット単位のシフト、乗算、および pow(e, n) モジュラー演算をサポートしており、RSAの核心計算を支えている。

さらに、以下の補助モジュールが実装されている:

  • Base64トランスフォーマー: バイナリ配列をASCIIベースのエンコード文字列に変換する。
  • Hexエンコーダー/デコーダー: 16進数表現とバイト列間の変換を行う。
  • PKCS#1 v1.5 パディング機構: RSA入力長の制約に対応するため、先頭に固定バイト列(0x00 0x02)と終端ゼロ、そして中間部に乱数バイトを配置して指定サイズまで拡張する。pkcs1pad2 メソッドがこの仕様を満たす。

暗号化ルーチンのリファクタリング

既存の実装構造を整理し、スコープ分離と可読性向上を図った構成例を示す。関数名・変数名を変更し、即時実行関数形式でモジュール化している。

const CryptoModule = (() => {
  const RSA = {
    createPublicKey(modHex, expHex) {
      return { modulus: new BigInteger(modHex, 16), encryptionExponent: new BigInteger(expHex, 16) };
    },
    applyPkcs1Padding(text, keyBitSize) {
      const byteLimit = Math.ceil(keyBitSize / 8);
      if (byteLimit <= text.length + 11) throw new Error('Key length insufficient');
      const buffer = [];
      let cursor = text.length - 1;
      while (cursor >= 0 && byteLimit > 0) buffer[--byteLimit] = text.charCodeAt(cursor--);
      buffer[--byteLimit] = 0x00;
      while (byteLimit > 2) buffer[--byteLimit] = Math.floor(Math.random() * 254) + 1;
      buffer[--byteLimit] = 0x02;
      buffer[--byteLimit] = 0x00;
      return new BigInteger(buffer);
    },
    performEncryption(clearText, pubKey) {
      const paddedValue = this.applyPkcs1Padding(clearText, pubKey.modulus.bitLength());
      const cipherBigInt = paddedValue.modPowInt(pubKey.encryptionExponent, pubKey.modulus);
      let hexResult = cipherBigInt.toString(16);
      if (hexResult.length % 2 !== 0) hexResult = '0' + hexResult;
      return Base64.encode(Hex.decode(hexResult));
    }
  };

  return {
    encryptLoginCredential: (inputPw, modStr, expStr) => {
      const keyObj = RSA.createPublicKey(modStr, expStr);
      return RSA.performEncryption(inputPw, keyObj);
    }
  };
})();

Python環境における実装統合

Python側では HTTPセッションの維持管理、公開鍵の動的取得、およびJSエンジンへの委譲を明確に分離したクラス構成とする。外部依存を最小限に抑えつつ、エラーハンドリングとリトライ対応を強化した。

import time
import requests
import execjs

class SteamAuthEncoder:
    def __init__(self, account_name, raw_password):
        self.key_fetch_uri = "https://store.steampowered.com/login/getrsakey/"
        self.http_client = requests.Session()
        self.http_client.headers.update({
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
        })
        self.account = account_name
        self.plaintext_pw = raw_password
        self.js_runtime = None
        self.initialize_engine()

    def initialize_engine(self):
        try:
            self.js_runtime = execjs.get()
        except RuntimeError as e:
            raise EnvironmentError("JavaScript実行環境(PyExecJS/Node)が検出されません:", e)

    def acquire_rsa_components(self):
        cache_buster = str(int(time.time() * 1000))
        payload = {"donotcache": cache_buster, "username": self.account}
        response = self.http_client.post(self.key_fetch_uri, data=payload)
        response.raise_for_status()
        json_data = response.json()
        return json_data["publickey_mod"], json_data["publickey_exp"]

    def generate_cipher_text(self):
        mod_val, exp_val = self.acquire_rsa_components()
        script_context = self.js_runtime.compile(open("./rsa_logic.min.js").read())
        evaluation_string = f"CryptoModule.encryptLoginCredential('{self.plaintext_pw}', '{mod_val}', '{exp_val}')"
        return script_context.eval(evaluation_string)

if __name__ == "__main__":
    credential_processor = SteamAuthEncoder("test@example.com", "secure_password_123")
    encrypted_result = credential_processor.generate_cipher_text()
    print("生成された暗号化文字列:", encrypted_result)

実装に関する技術的制約

自動化スクリプトを安定運用するには、HTTP Cookieの同期とタイムアウト制御が重要になる。公開鍵の有効期間は非常に短く設計されているため、フェッチと暗号化処理は同一スレッド内で連続実行し、ネットワーク遅延による無効化を回避する必要がある。また、JavaScript環境のMath.random()実装や時刻関数の挙動はランタイムによって差異が生じるため、テスト環境での検証結果に基づき polyfill またはNative Node.js APIへの置き換えを検討することが推奨される。

タグ: RSA javascript PyExecJS クレデンシャル自動化 PKCS1v5パディング

10月26日 21:19 投稿