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

# はじめに
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を使っています。


```C#
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を使いました。

```C#
// 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配列で読み込むメソッドがなぜか用意されてないので、自分で作ります。

```C#
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メソッドを定義しておきます。

```C#
// 暗号化データを初期化ベクトルとパスワードデータに分離
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」を導入しました。

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

```C#
// 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;
}
```

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

```C#
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に保存されているログインデータを取得できてしまいました。
パスワードを忘れてしまった際に便利だと思いますが、これだと危険なので、パスワードなどの重要なデータの保存にはより工夫が必要だと思いました。

# 参考文献
* [System.Security.Cryptography.CryptographicException: “The parameter is incorrect” / Aes 256 Gcm Decryption](https://stackoverflow.com/questions/60640302/system-security-cryptography-cryptographicexception-the-parameter-is-incorrect)
* [How to Recover Saved Passwords in Google Chrome](https://xenarmor.com/how-to-recover-saved-passwords-google-chrome/)
* [初期化ベクトルとは？暗号化で知っておくべき基礎知識を解説！](https://it-trend.jp/encryption/article/64-0091)