Extracting Chrome Cookies with Clojure

Introduction

Did you know that it was possible to extract your google chrome cookies to use them in a script? This can be a great way of automating interaction with a website when you don't want to bother automating the login flow. There is a great python library to do that called browsercookie. Browsercookie works on Linux, Mac OSX, and Windows and can extract cookies from Chrome and Firefox.

I have been learning Clojure for the past month and I decided to reimplement the same functionality as browsercookie as an exercise! I built a command line tool to print decrypted Chrome cookies as a JSON on OSX. This article will walk you through the implementation. Even if you don't know a thing about Clojure you should be able to understand the process and learn a few things along the way; enjoy! The full code for this project is available on github.

Context: how does Chrome store cookies?

Chrome store its cookies in a SQLite database, in the cookies table. The actual content of the cookies is encrypted with AES using a key stored in the user's keychain.

Architecture

Here is a diagram of the architecture (in orange, are the side effects and green pure computation): <figure style="text-align:center"> <img style="display: block; margin: 0 auto" alt="Architecture of the code" src ="/assets/archi-cookie.svg" /> <figcaption>High-level architecture of the code</figcaption> </figure>

This translates to what the main function does:

(defn -main
  [& args]
  (if (not= 1 (count args))
    (.println *err* "Usage ./program <site-url>")
    (let [site-url (first args)
          aes-key (build-decryption-key (get-chrome-password-osx))
          db-spec {:dbtype "sqlite" :dbname (temp-file-copy-of cookies-file-osx)}
          query (str "select * from cookies where host_key like '" site-url "'")
          cookies (jdbc/query db-spec [query])]
      (println
       (json/write-str
        (map (partial decrypt-cookie aes-key) cookies)))
      (System/exit 0))))

Let's explore how each part works, dividing the process into three steps:

Step 1 — Building the cookies decryption key

This is a two step process: reading the key from the keychain and making it suitable to use for AES. To read a key from the keychain, MacOS provide a command line utility called security:

To get the chrome key you can do:

security find-generic-password -a "Chrome" -w

Which can express this in Clojure:

(defn get-chrome-rawkey-osx
  "Get the chrome raw decryption key using the `security` cli on OSX"
  []
  (-> (shell/sh "security" "find-generic-password" "-a" "Chrome" "-w" :out-enc "UTF-8")
      (:out)
      (clojure.string/trim)
      (.toCharArray)))

The Clojure syntax (-> x (a) (b k)) would translate to b(a(x), k) in python, it allows chaining of operations in a very readable way. Note how we can easily mix Clojure function and Java functions that start with a . character.

We can then build a key suitable for AES decryption:

(defn build-decryption-key
  "Given a chrome raw key, construct a decryption key for the cookies"
  [raw-key]
  (let [salt (.getBytes "saltysalt")]
  (-> (SecretKeyFactory/getInstance "pbkdf2withhmacsha1")
      (.generateSecret (PBEKeySpec. raw-key salt 1003 128))
      (.getEncoded)
      (SecretKeySpec. "AES"))))

To decrypt AES, we need the key and a salt. saltysalt is the salt used by Chrome (I copied this from browsercookie). Again note that methods starting with . are Java methods. We use the Java interop and the javax.crypto library to build this key.

Now that we have a decryption key, let's see how we can read the cookies and decrypt them!

Step 2 — Reading the encrypted cookies

The cookies are stored in a SQLite database at a known location on OSX. Each row in the cookies table corresponds to a cookie:

  • if the encrypted_value fields start with v10, it is an encrypted cookie (again according to the reference implementation browsercookie).
  • otherwise, the value field contains the value of the cookie.

Let's start with a utility function to return the path of the cookie database:

(def cookies-file-osx
  "Return the path to the cookie store on OSX"
  (str
   (System/getProperty "user.home")
   "/Library/Application Support/Google/Chrome/Default/Cookies"))

Since we don't want to work directly on the real cookie database, we can make a copy of it to a temp file using another function:

(defn temp-file-copy-of
  "Create a copy of file as a temp file that will be removed when
  the program exits, return its path"
  [file]
  (let [tempfile (java.io.File/createTempFile "store" "db")]
    (.deleteOnExit tempfile)
    (io/copy (io/file file) (io/file tempfile))
    tempfile))

We can use these two functions to query all the cookies from the database matching the domain name the user requested:

(let [site-url (first args)
      db-spec {:dbtype "sqlite" :dbname (temp-file-copy-of cookies-file-osx)}
      query (str "select * from cookies where host_key like '" site-url "'")]
      ... ;; Do something with cookies)

At this point, we have extracted all the relevant cookies from the SQLite database and have a decryption key ready, let's decrypt and print the cookies!

Step 3 - Decrypting the cookies and printing them

In this step we will build the decrypt-cookie function. Given a cookie like:

{
  :path "/",
  :firstpartyonly 0,
  :name "tz",
  :persistent 0,
  :encrypted_value "v10garbage",
  :expires_utc 0,
  :host_key "github.com",
  :creation_utc 13147635893809723,
  :httponly 0,
  :priority 1,
  :last_access_utc 13147635892809723,
  :secure 1,
  :has_expires 0
}

The decrypt-cookie function returns a decrypted cookie:

{
  :path "/",
  :firstpartyonly 0,
  :name "tz",
  :persistent 0,
  :value "decrypted value",
  :expires_utc 0,
  :host_key "github.com",
  :creation_utc 13147635893809723,
  :httponly 0,
  :priority 1,
  :last_access_utc 13147635892809723,
  :secure 1,
  :has_expires 0
}

How do we know if a cookie is encrypted? As we said above, a cookie is encrypted if its encrypted_value field starts with v10. Let's use the -> macro to express this in the is-encrypted? function:

(defn is-encrypted?
  "Returns true if the provided encrypted value (bytes[]) is an encrypted
  value by Chrome that is, if it starts with v10"
  [cookie]
  (-> cookie
      (:encrypted_value)
      (String. "UTF-8")
      (clojure.string/starts-with? "v10")))

But how do we decrypt an encrypted value? Since we use CBC, we need to combine the AES key and an initialization vector (again taken from the reference implementation) and build a cipher. Using this cipher we decrypt the encrypted text in the decrypt function:

(defn decrypt [aeskey text]
  "Decrypt AES encrypted text given an aes key"
  (let [ivsbytes (-> (repeat 16 " ") (clojure.string/join) (.getBytes))
        iv       (IvParameterSpec. ivsbytes)
        cipher   (Cipher/getInstance "AES/CBC/PKCS5Padding")
        _        (.init cipher Cipher/DECRYPT_MODE aeskey iv)]
    (String. (.doFinal cipher text))))

This is a little terse but can easily be decomposed into three steps:

Building the initial vector for the AES decryption (let block omitted):

ivsbytes (-> (repeat 16 " ") (clojure.string/join) (.getBytes))
iv       (IvParameterSpec. ivsbytes)

Building the cipher

cipher   (Cipher/getInstance "AES/CBC/PKCS5Padding")
_ (.init cipher Cipher/DECRYPT_MODE aeskey iv)]

This is a little ugly, because .init is used only for its side effect and we ignore its result …

Decrypting the text:

(String. (.doFinal cipher text))

Now that we know how to decrypt a value and detect encrypted values, we can put it all together into decrypt-cookie, a function that decrypts a cookie:

(defn decrypt-cookie
  "Given a cookie, return a cookie with decrypted value"
  [aes-key cookie]
  (-> cookie
      (assoc :value
             (if (is-encrypted? cookie)
               ;; Drop 3 removes the leading "v10"
               (->> cookie (:encrypted_value) (drop 3) (byte-array) (decrypt aes-key))
               (-> cookie (:value))))
      ;; Remove the unused :encrypted_value entry
      (dissoc :encrypted_value)))

Note that we use the ->> macro instead of -> that we used before. Instead of threading the expression as the first argument of each function like ->, ->> threads the expression as the last argument.

Putting it all together

It all comes together in the main function that we showed above:

(defn -main
  [& args]
  (if (not= 1 (count args))
    (.println *err* "Usage ./program <site-url>")
    (let [site-url (first args)
          aes-key (build-decryption-key (get-chrome-password-osx))
          db-spec {:dbtype "sqlite" :dbname (temp-file-copy-of cookies-file-osx)}
          query (str "select * from cookies where host_key like '" site-url "'")
          cookies (jdbc/query db-spec [query])]
      (println
       (json/write-str
        (map (partial decrypt-cookie aes-key) cookies)))
      (System/exit 0))))

Here we map over all the relevant cookies from the database using a function to decrypt the cookies. We use partial to build a function with the first argument aes-key locked in.

Conclusion

In this guide, we covered how to extract and decrypt Chrome cookies. We also looked at some Clojure idioms. Again if you want to use this in python you should check out browsercookie. The full code for this project is available on github.