プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

GoogleChromeのCookieをAES256-GCMで読み取る

雑な概要とあらすじ

C# にて、GoogleChromeのCookieファイルを読み込んで、任意の値を取得した処理を実装・稼働させていた。
しかし いつ頃からか再び機能しなくなったので、色々調べてメモがてら記事に。

暗号化の仕様変更が原因

必死にググっていると、以下のページを見つけた。 ぶっちゃけ諸々理解している人なら、このリンク先だけ読めば事足りると思います

https://stackoverflow.com/questions/60230456/dpapi-fails-with-cryptographicexception-when-trying-to-decrypt-chrome-cookies

翻訳掛けつつ要約すると

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);
        }
    }

}