How to read encrypted Google Chrome cookies in C#

A web browser with several tabs and icons is displayed.

Recently at work I needed to write a few bots/scrapers for websites that do not have an official API or bot support. Emulating browser-based logins without triggering anti-bot checks is challenging, to get around this issue, we login from a web browser on the Windows Server and copy its cookies from the SQLite Database storing them. This blog post explains how to read encrypted Google Chrome cookies in C# programs.

Reading cookies from Google Chrome (or other web browsers installed on the system) is controversial in some programming communities given the risk of this knowledge being used in malware. As a result some communities are not inclined to provide an answer to this question on ethical grounds. I see the knowledge as a tool and how you use it is your decision.

System Requirements

This blog post is written assuming you have Google Chrome (or a fork such as Brave Browser) installed built on Chromium 80 or newer. Additionally it’s written for Windows 10 users as the Windows Data Protection API is used to protect cookies. While it’s written with .NET core in mind, you probably won’t be able to run this code on macOS or Linux. I’ve only tested this code on Windows 10 and Windows Server 2019 and significant changes will probably be needed to use it on those platforms. I did most of my testing on Brave (then switched the paths to Google Chrome on the server).

How cookies were encrypted in Chrome version 79 and lower

Prior to the release of Google Chrome version 80, the software relied directly on the Windows Data Protection API to encrypt and decrypt the value of cookies. Any time you needed to encrypt or decrypt a cookie, you would pass the value to the Windows Data Protection API and await its response.

The encryption is designed to prevent other users on the same computer from copying your cookies and using them to access your online accounts. Your Windows password and some other local data is used to derive a key for use with the Windows Data Protection API (DPAPI) and without your Windows password only a local administrator could access data protected with DPAPI.

You were able to use the following code snippet to decrypt a cookie in Chrome 79 and lower. You’ll of course need to fetch it from SQLite although that’s outside the scope of this blog post.

using System.Security.Cryptography;
...
ProtectedData.Unprotect(cookie.EncryptedValue, null, DataProtectionScope.CurrentUser);
...

How Google Chrome version 80 changes the cookie encryption process

According to Arun on StackOverflow: “Starting Chrome 80 version, cookies are encrypted using the AES256-GCM algorithm, and the AES encryption key is encrypted with the DPAPI encryption system, and the encrypted key is stored inside the ‘Local State’ file.”.

This means that passing a cookie to DPAPI directly will not work anymore. Instead only the encryption key is encrypted using DPAPI. To decrypt a cookie’s encrypted value you will need to get the encryption key from the ‘Local State’ file, decrypt it with DPAPI, and then use other tools to run AES256-GCM decryption. These changes were made to improve the security of the Chromium platform although are breaking to many third party tools that rely on data from Chromium databases.

C# has a thriving package ecosystem and finding packages to do this for me was an easy process. Your resulting code should look something like the following…

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using Newtonsoft.Json.Linq;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;

namespace BraveBrowserCookieReaderDemo
{
    public class BraveCookieReader
    {
        public IEnumerable<Tuple<string, string>> ReadCookies(string hostName)
        {
            if (hostName == null) throw new ArgumentNullException("hostName");

            using var context = new BraveCookieDbContext();

            var cookies = context
                .Cookies
                .Where(c => c.HostKey.Equals(hostName))
                .AsNoTracking();

            // Big thanks to https://stackoverflow.com/a/60611673/6481581 for answering how Chrome 80 and up changed the way cookies are encrypted.

            string encKey = File.ReadAllText(System.Environment.GetEnvironmentVariable("LOCALAPPDATA") + @"\BraveSoftware\Brave-Browser\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);

            foreach (var cookie in cookies)
            {

                var data = cookie.EncryptedValue;

                var decodedValue = _decryptWithKey(data, decodedKey, 3);


                yield return Tuple.Create(cookie.Name, decodedValue);
            }
        }


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

Solution

You can access my full & final solution on GitHub (@irlcatgirl/BraveCookieReaderDemo) where I used the techniques discussed in this post to write a full application which reads cookies and their encrypted values from Brave Browser (a privacy friendly fork of Google Chrome). It includes the unexplained things (such as using EF Core to access Google Chrome’s SQLite database and how to create a temp copy). I hope you found this post informative and helpful.

References

This blog post would not of been possible without help from the following resources and individuals.

%d bloggers like this: