GoogleChromeのCookieをAES256-GCMで読み取る
雑な概要とあらすじ
C#
にて、GoogleChromeのCookieファイルを読み込んで、任意の値を取得した処理を実装・稼働させていた。
しかし いつ頃からか再び機能しなくなったので、色々調べてメモがてら記事に。
暗号化の仕様変更が原因
必死にググっていると、以下のページを見つけた。 ぶっちゃけ諸々理解している人なら、このリンク先だけ読めば事足りると思います
翻訳掛けつつ要約すると
Chromeのv80から Cookieの暗号化方法が AES256-GCM
という仕組み(アルゴリズム)に置き換わり、
更にその AES256-GCM
で使われる暗号化キー自体が Local State
というファイルに書き出され管理されているそうな。
ただ、全てが AES256-GCM
で管理されている訳でもなさそうで、(多分)従来の DPAPI
?というもので暗号化されている事も普通にあるのだそう。
多分 DPAPI
であれば、従来の方法で Cookie値を読みだせるのかな?
ちなみに判別方法として、暗号化されたcookie値の先頭文字(3文字分?)が "v10" とか "v12" になっていれば、
AES256-GCM
で変換されたもの。
...という解釈でした、自分は。
対応コード
ほぼ参考元の引用転載です
byte[] encryptedData = <data stored in cookie file>
string encKey = File.ReadAllText(localAppDataPath + @"\Google\Chrome\User Data\Local State");
encKey = JObject.Parse(encKey)["os_crypt"]["encrypted_key"].ToString();
var decodedKey = System.Security.Cryptography.ProtectedData.Unprotect(Convert.FromBase64String(encKey).Skip(5).ToArray(), null, System.Security.Cryptography.DataProtectionScope.LocalMachine);
_cookie = _decryptWithKey(encryptedData, decodedKey, 3);
汎用として下の _decryptWithKey
メソッドを用意しつつ、
上の処理でそれを使う..みたいな感じですね
private string _decryptWithKey(byte[] message, byte[] key, int nonSecretPayloadLength)
{
const int KEY_BIT_SIZE = 256;
const int MAC_BIT_SIZE = 128;
const int NONCE_BIT_SIZE = 96;
if (key == null || key.Length != KEY_BIT_SIZE / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
if (message == null || message.Length == 0)
throw new ArgumentException("Message required!", "message");
using (var cipherStream = new MemoryStream(message))
using (var cipherReader = new BinaryReader(cipherStream))
{
var nonSecretPayload = cipherReader.ReadBytes(nonSecretPayloadLength);
var nonce = cipherReader.ReadBytes(NONCE_BIT_SIZE / 8);
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce);
cipher.Init(false, parameters);
var cipherText = cipherReader.ReadBytes(message.Length);
var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];
try
{
var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
cipher.DoFinal(plainText, len);
}
catch (InvalidCipherTextException)
{
return null;
}
return Encoding.Default.GetString(plainText);
}
}
このソースを流用するには
Newtonsoft JSON .net
および
Bouncy Castle Crypto package
のライブラリ(パッケージ)が必要
自分は VisualStudioCommunity で組んでいるので、 Nuget でこれらのパッケージを突っ込んで使いました
仕様変更のあおりを受けた時の覚書
エラーに気づいた時は CryptographicException
とかの例外を貰った記憶があります。
「パラメータが間違っている」みたいな説明もあったかな、とも。
↓ それまではこういう感じで比較的(?)サクっと取れてました。
var decodedData = System.Security.Cryptography.ProtectedData.Unprotect(encryptedData, null, System.Security.Cryptography.DataProtectionScope.CurrentUser);
var plainText = Encoding.ASCII.GetString(decodedData);
DOBON.NET - DPAPIを使用して暗号化する Microsoft .NET - ProtectedData.Unprotect()
しかし、Chrome v80 からの暗号化仕様変更を受けてからはエラーに.. orz
従来は ProtectedData
でほぼ完結していたものが、v80からは Local State
ファイルを咬ませるようになったので、
セキュリティ的には強くなっているのだと思います(素人並みの感想)
おまけのCookie読み取りコード
雑で申し訳ないですが、簡単なクラスとしてこさえてみました。
// --- Cookie読み出し -----------------------------
Console.WriteLine("Cookie読み込み開始");
var LocalApplicationDataPath = System.Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var _cookieReader = new GoogleChromeCookieReader();
_cookieReader.CookieStorePath = LocalApplicationDataPath + @"\Google\Chrome\User Data\Default\Cookies";
_cookieReader.LocalStatePath = LocalApplicationDataPath + @"\Google\Chrome\User Data\Local State";
_cookieReader.TargetDomain = ".pixiv.net";
Dictionary<string, string> _cookieDict = _cookieReader.GetDict();
下側のクラスを用意しておき、上のコードで使う..みたいな意図です。
本例だと、上記コードにて pixiv.net の Cookieをターゲットに読み込んでいます。
返ってくる Dictionary<string , string>
の _cookieDict
に、key-value のペアが入る感じです
// GoogleChromeのv80~(AES256-GCM) "だけ" をターゲットにしたCookie読み出しクラス
// 従来の DPAPI は考慮していない
class GoogleChromeCookieReader
{
public GoogleChromeCookieReader()
{
}
public string CookieStorePath = string.Empty;
public string TargetDomain = string.Empty;
public string LocalStatePath = string.Empty;
/// <summary>
/// Cookieのリストを返す
/// </summary>
/// <returns></returns>
public Dictionary<String, String> GetDict()
{
if (!System.IO.File.Exists(CookieStorePath)) throw new System.IO.FileNotFoundException("Cookieファイルが無いっす", CookieStorePath);
if (String.IsNullOrEmpty(LocalStatePath) == false)
{
if (!System.IO.File.Exists(LocalStatePath)) throw new System.IO.FileNotFoundException("Local State ファイルが無いっす", LocalStatePath);
}
if (String.IsNullOrEmpty(TargetDomain)) throw new ArgumentOutOfRangeException("抽出対象のドメインを入力してください");
var _dict = new Dictionary<String, String>();
using (var conn = new SQLiteConnection("Data Source=" + CookieStorePath + ";Version=3;Connection Lifetime=5"))
{
using (var cmd = conn.CreateCommand())
{
try
{
cmd.CommandText = "SELECT host_key,name,encrypted_value,value FROM cookies WHERE host_key like '" + TargetDomain + "'";
conn.Open();
using (SQLiteDataReader reader = cmd.ExecuteReader())
{
while (reader.Read())
{
String keyName = (String)reader["name"];
Byte[] encryptedData = (Byte[])reader["encrypted_value"];
if (encryptedData.Length == 0)
{
continue;
}
string encKey = File.ReadAllText(LocalStatePath);
encKey = JObject.Parse(encKey)["os_crypt"]["encrypted_key"].ToString();
var decodedKey = System.Security.Cryptography.ProtectedData.Unprotect(Convert.FromBase64String(encKey).Skip(5).ToArray(), null, System.Security.Cryptography.DataProtectionScope.LocalMachine);
// v10とかv12 であるかと、その判定はどうしよう?
var plainText = _decryptWithKey(encryptedData, decodedKey, 3);
_dict.Add(keyName, plainText);
}
}
}
catch (Exception exc)
{
MessageBox.Show(exc.Message, "Cookie検索処理エラー");
Console.WriteLine(exc.Message);
}
finally
{
conn.Close();
conn.Dispose();
}
}
}
return _dict;
}
private string _decryptWithKey(byte[] message, byte[] key, int nonSecretPayloadLength)
{
const int KEY_BIT_SIZE = 256;
const int MAC_BIT_SIZE = 128;
const int NONCE_BIT_SIZE = 96;
if (key == null || key.Length != KEY_BIT_SIZE / 8)
throw new ArgumentException(String.Format("Key needs to be {0} bit!", KEY_BIT_SIZE), "key");
if (message == null || message.Length == 0)
throw new ArgumentException("Message required!", "message");
using (var cipherStream = new MemoryStream(message))
using (var cipherReader = new BinaryReader(cipherStream))
{
var nonSecretPayload = cipherReader.ReadBytes(nonSecretPayloadLength);
var nonce = cipherReader.ReadBytes(NONCE_BIT_SIZE / 8);
var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), MAC_BIT_SIZE, nonce);
cipher.Init(false, parameters);
var cipherText = cipherReader.ReadBytes(message.Length);
var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];
try
{
var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
cipher.DoFinal(plainText, len);
}
catch (InvalidCipherTextException)
{
return null;
}
return Encoding.Default.GetString(plainText);
}
}
}