LoginSignup
10
10

More than 3 years have passed since last update.

Chromeに保存されているパスワードを解読してみる

Last updated at Posted at 2020-05-24

はじめに

Google Chromeでサイトにログインするとき、多くの人がアカウント情報をChromeに保存していると思います。
しかし、Chromeはこのログインデータをどのように保存しているのだろうか、と気になったので、調べてみました。

Chromeのログインデータの保存手順

調べてみたところ、ログインデータは
ユーザーフォルダ\AppData\Local\Google\Chrome\User Data\Default\Login Data
にSQLiteデータベース形式で保存されていることがわかりました。

また、パスワードは以下の方法で暗号化されていることがわかりました。

  1. 32バイトのランダムデータ(マスターキー)を生成する
  2. Windows DPAPIを使ってそれを暗号化する
  3. この暗号化されたキーの最初に文字列「DPAPI」を挿入する
  4. Base64でエンコードし、ユーザーフォルダ\AppData\Local\Google\Chrome\User Data\Local Stateに保存する
  5. 12バイトの初期化ベクトルを生成する
  6. 上記のマスターキーと初期化ベクトルを使って、パスワードをAES-256-GCMで暗号化する
  7. 暗号化されたパスワードの最初に文字列「v10」、初期化ベクトルを挿入する

Windows DPAPIというのは、Windows アカウント パスワードを使って暗号化するAPIです。
初期化ベクトルというのはランダムに生成されるビット列であり、これを利用して暗号化することでデータを解読しにくくすることができます。

ログインデータを読み込んでみる

よって、ログインデータを読み込む手順は以下のようになります。

  1. Local Stateファイルから暗号化されたマスターキーを読み込む
  2. マスターキーをBase64デコードし、「DPAPI」を削除する
  3. Windows DPAPIを使ってそれを復号化する
  4. Login DataファイルをSQLiteデータベースとして読み込み、URL、ユーザー名、暗号化されたパスワードを読み込む
  5. 暗号化されたパスワードから「v10」を削除し、初期化ベクトルとパスワードデータに分ける
  6. それらを使って、パスワードをAES-256-GCMで復号化する

それでは、C#で実装してみます。

マスターキーを取得する

パスワードを復号化するにはマスターキーが必要なので、Local Stateファイルから読み込んで復号化します。
このファイル自体はJsonで記録されており、os_cryptのencrypted_keyの値が暗号化されたマスターキーになります。
このコードではJsonファイルを読み込むためにNewtonsoft.Jsonを使っています。

public static byte[] GetKey() {
    // AppDataのパスを取得
    var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
    // Local Stateのパスを取得
    var path = Path.GetFullPath(appdata + "\\..\\Local\\Google\\Chrome\\User Data\\Local State");

    // Local StateをJsonとして読み込む
    string v = File.ReadAllText(path);
    dynamic json = JsonConvert.DeserializeObject(v);
    string key = json.os_crypt.encrypted_key;

    // Base64エンコード
    byte[] src = Convert.FromBase64String(key);
    // 文字列「DPAPI」をスキップ
    byte[] encryptedKey = src.Skip(5).ToArray();

    // DPAPIで復号化
    byte[] decryptedKey = ProtectedData.Unprotect(encryptedKey, null, DataProtectionScope.CurrentUser);

    return decryptedKey;
}

Login Dataの読み込み

次に、Login Dataファイルを読み込みます。
SQLiteデータベース形式で保存されているので、今回はこれを読み込むためにSystem.Data.SQLite.Coreを使いました。

// AppDataのパスを取得
var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
// Login Dataのパスを取得
var p = Path.GetFullPath(appdata + "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data");

if (File.Exists(p)) {
    Process[] chromeInstances = Process.GetProcessesByName("chrome");
    foreach (Process proc in chromeInstances)
        // Chromeを強制終了
        // これをやらないと「database is locked」エラーになる
        proc.Kill();

    // Login Dataファイルを読み込む
    using (var conn = new SQLiteConnection($"Data Source={p};")) {
        conn.Open();
        using (var cmd = conn.CreateCommand()) {
            cmd.CommandText = "SELECT action_url, username_value, password_value FROM logins";
            using (var reader = cmd.ExecuteReader()) {
                if (reader.HasRows) {
                    // マスターキーを取得
                    byte[] key = GetKey();

                    while (reader.Read()) {
                        // 空のデータは無視
                        if (reader[0].ToString() == "") continue;
                        // 暗号化されたパスワードをbyte配列で読み込む
                        byte[] encryptedData = GetBytes(reader, 2);
                        // 初期化ベクトルとパスワードデータに分離
                        byte[] nonce, ciphertextTag;
                        Prepare(encryptedData, out nonce, out ciphertextTag);
                        // パスワードの復号化
                        string password = Decrypt(ciphertextTag, key, nonce);

                        var url = reader.GetString(0);
                        var username = reader.GetString(1);

                        Console.WriteLine("Url : " + url);
                        Console.WriteLine("Username : " + username);
                        Console.WriteLine("Password : " + password + "\n");
                    }
                }
            }
        }
    conn.Close();
    Console.ReadKey(true);
    }
} else {
    throw new FileNotFoundException("Login Dataファイルが見つかりません");
}

SQLiteからbyte配列で読み込むメソッドがなぜか用意されてないので、自分で作ります。

private static byte[] GetBytes(SQLiteDataReader reader, int columnIndex) {
    const int CHUNK_SIZE = 2 * 1024;
    byte[] buffer = new byte[CHUNK_SIZE];
    long bytesRead;
    long fieldOffset = 0;
    using (MemoryStream stream = new MemoryStream()) {
        while ((bytesRead = reader.GetBytes(columnIndex, fieldOffset, buffer, 0, buffer.Length)) > 0) {
            stream.Write(buffer, 0, (int)bytesRead);
            fieldOffset += bytesRead;
        }
        return stream.ToArray();
    }
}

読み込んだ暗号化データを初期化ベクトルとパスワードデータに分離するPrepareメソッドを定義しておきます。

// 暗号化データを初期化ベクトルとパスワードデータに分離
public static void Prepare(byte[] encryptedData, out byte[] nonce, out byte[] ciphertextTag) {
    nonce = new byte[12];
    ciphertextTag = new byte[encryptedData.Length - 3 - nonce.Length];

    System.Array.Copy(encryptedData, 3, nonce, 0, nonce.Length);
    System.Array.Copy(encryptedData, 3 + nonce.Length, ciphertextTag, 0, ciphertextTag.Length);
}

復号化

次に、復号化処理を見ていきます。
.NET FrameworkにはAES-256-GCMの復号化を行うクラスはないので、今回は暗号化・復号化用パッケージ「Bouncy Castle」を導入しました。

以下が復号化のコードです。
正直、このコードは海外のサイトからコピーしたものなので、自分でもよく理解していません。

// AES-256-GCM 復号化処理
// 暗号化されたパスワード、マスターキー、初期化ベクトルを指定
public static string Decrypt(byte[] encryptedBytes, byte[] key, byte[] iv) {
    string sR = "";
    try {
        GcmBlockCipher cipher = new GcmBlockCipher(new AesFastEngine());
        AeadParameters parameters = new AeadParameters(new KeyParameter(key), 128, iv, null);

        cipher.Init(false, parameters);
        byte[] plainBytes = new byte[cipher.GetOutputSize(encryptedBytes.Length)];
        Int32 retLen = cipher.ProcessBytes(encryptedBytes, 0, encryptedBytes.Length, plainBytes, 0);
        cipher.DoFinal(plainBytes, retLen);

        sR = Encoding.UTF8.GetString(plainBytes).TrimEnd("\r\n\0".ToCharArray());
    }
    catch (Exception ex) {
        Console.WriteLine(ex.Message);
        Console.WriteLine(ex.StackTrace);
    }

    return sR;
}

ログインデータを読み込むプログラム

ログインデータを読み込んで出力するプログラムは以下のようになります。

using Newtonsoft.Json;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Data.SQLite;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

public class GetLoginData
{
    public static void Main()
    {
        // AppDataのパスを取得
        var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        // Login Dataのパスを取得
        var p = Path.GetFullPath(appdata + "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data");

        if (File.Exists(p))
        {
            Process[] chromeInstances = Process.GetProcessesByName("chrome");
            foreach (Process proc in chromeInstances)
                // Chromeを強制終了
                // これをやらないと「database is locked」エラーになる
                proc.Kill();

            // Login Dataファイルを読み込む
            using (var conn = new SQLiteConnection($"Data Source={p};"))
            {
                conn.Open();
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = "SELECT action_url, username_value, password_value FROM logins";
                    using (var reader = cmd.ExecuteReader())
                    {

                        if (reader.HasRows)
                        {
                            // マスターキーを取得
                            byte[] key = GetKey();

                            while (reader.Read())
                            {
                                // 空のデータは無視
                                if (reader[0].ToString() == "") continue;
                                // 暗号化されたパスワードをbyte配列で読み込む
                                byte[] encryptedData = GetBytes(reader, 2);
                                // 初期化ベクトルとパスワードデータに分離
                                byte[] nonce, ciphertextTag;
                                Prepare(encryptedData, out nonce, out ciphertextTag);
                                // パスワードの復号化
                                string password = Decrypt(ciphertextTag, key, nonce);

                                var url = reader.GetString(0);
                                var username = reader.GetString(1);

                                Console.WriteLine("Url : " + url);
                                Console.WriteLine("Username : " + username);
                                Console.WriteLine("Password : " + password + "\n");
                            }
                        }
                    }
                }
                conn.Close();
                Console.ReadKey(true);
            }

        }
        else
        {
            throw new FileNotFoundException("Login Dataファイルが見つかりません");
        }
    }

    public static byte[] GetKey()
    {
        // AppDataのパスを取得
        var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
        // Local Stateのパスを取得
        var path = Path.GetFullPath(appdata + "\\..\\Local\\Google\\Chrome\\User Data\\Local State");

        // Local StateをJsonとして読み込む
        string v = File.ReadAllText(path);
        dynamic json = JsonConvert.DeserializeObject(v);
        string key = json.os_crypt.encrypted_key;

        // Base64エンコード
        byte[] src = Convert.FromBase64String(key);
        // 文字列「DPAPI」をスキップ
        byte[] encryptedKey = src.Skip(5).ToArray();

        // DPAPIで復号化
        byte[] decryptedKey = ProtectedData.Unprotect(encryptedKey, null, DataProtectionScope.CurrentUser);

        return decryptedKey;
    }

    // AES-256-GCM 復号化処理
    public static string Decrypt(byte[] encryptedBytes, byte[] key, byte[] iv)
    {
        string sR = "";
        try
        {
            GcmBlockCipher cipher = new GcmBlockCipher(new AesFastEngine());
            AeadParameters parameters = new AeadParameters(new KeyParameter(key), 128, iv, null);

            cipher.Init(false, parameters);
            byte[] plainBytes = new byte[cipher.GetOutputSize(encryptedBytes.Length)];
            Int32 retLen = cipher.ProcessBytes(encryptedBytes, 0, encryptedBytes.Length, plainBytes, 0);
            cipher.DoFinal(plainBytes, retLen);

            sR = Encoding.UTF8.GetString(plainBytes).TrimEnd("\r\n\0".ToCharArray());
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.Message);
            Console.WriteLine(ex.StackTrace);
        }

        return sR;
    }

    // 暗号化データを初期化ベクトルとパスワードデータに分離
    public static void Prepare(byte[] encryptedData, out byte[] nonce, out byte[] ciphertextTag)
    {
        nonce = new byte[12];
        ciphertextTag = new byte[encryptedData.Length - 3 - nonce.Length];

        System.Array.Copy(encryptedData, 3, nonce, 0, nonce.Length);
        System.Array.Copy(encryptedData, 3 + nonce.Length, ciphertextTag, 0, ciphertextTag.Length);
    }

    // SQLiteデータをbyte配列で読み込む
    private static byte[] GetBytes(SQLiteDataReader reader, int columnIndex)
    {
        const int CHUNK_SIZE = 2 * 1024;
        byte[] buffer = new byte[CHUNK_SIZE];
        long bytesRead;
        long fieldOffset = 0;
        using (MemoryStream stream = new MemoryStream())
        {
            while ((bytesRead = reader.GetBytes(columnIndex, fieldOffset, buffer, 0, buffer.Length)) > 0)
            {
                stream.Write(buffer, 0, (int)bytesRead);
                fieldOffset += bytesRead;
            }
            return stream.ToArray();
        }
    }
}

このプログラムを実行すると、以下のようにログインデータが出力されます。

Url : https://accounts.google.com/signin/v2/challenge/password/empty
Username : qwerty123456@gmail.com
Password : zettaibarenaipassword

Url : https://id.unity.com/ja/conversations/0a0a3de1-8dd7-4c4b-81fe-49e7460614f2301f
Username : admin314159@gmail.com
Password : UnityPassword

Operaのログインデータについて

Opera BrowserはGoogle Chromeと同じChromiumをベースにしているので、同じ方法でログインデータを取得することができます。

上記のコードの20行目

var p = Path.GetFullPath(appdata + "\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data");

var p = Path.GetFullPath(appdata + "\\..\\Roaming\\Opera Software\\Opera Stable\\Login Data");

に書き換え、
83行目の

var path = Path.GetFullPath(appdata + "\\..\\Local\\Google\\Chrome\\User Data\\Local State");

var path = Path.GetFullPath(appdata + "\\..\\Roaming\\Opera Software\\Opera Stable\\Local State");

に書き換えて実行してみると、同じようにOperaのログインデータが出力されるはずです。

まとめ

ということで、割と簡単にChromeに保存されているログインデータを取得できてしまいました。
パスワードを忘れてしまった際に便利だと思いますが、これだと危険なので、パスワードなどの重要なデータの保存にはより工夫が必要だと思いました。

参考文献

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10