Skip to content

Instantly share code, notes, and snippets.

@dacort
Created September 23, 2014 06:51
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save dacort/bd6a5116224c594b14db to your computer and use it in GitHub Desktop.
Save dacort/bd6a5116224c594b14db to your computer and use it in GitHub Desktop.
Simple script to extract (encrypted) cookies out of Chrome OS X cookie store. Usage: ./cookiemonster domain.com
package main
import (
"code.google.com/p/go.crypto/pbkdf2"
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"database/sql"
"fmt"
"log"
"os"
"os/exec"
"os/user"
"strings"
_ "github.com/mattn/go-sqlite3"
)
// Inpsiration
// http://n8henrie.com/2013/11/use-chromes-cookies-for-easier-downloading-with-python-requests/
// Chromium Mac os_crypt: http://dacort.me/1ynPMgx
var (
salt = "saltysalt"
iv = " "
length = 16
password = ""
iterations = 1003
)
// Cookie - Items for a cookie
type Cookie struct {
Domain string
Key string
Value string
EncryptedValue []byte
}
// DecryptedValue - Get the unencrypted value of a Chrome cookie
func (c *Cookie) DecryptedValue() string {
if c.Value > "" {
return c.Value
}
if len(c.EncryptedValue) > 0 {
encryptedValue := c.EncryptedValue[3:]
return decryptValue(encryptedValue)
}
return ""
}
func usage() {
fmt.Fprintf(os.Stderr, "usage: %s [domain]\n", os.Args[0])
os.Exit(2)
}
func main() {
if len(os.Args) != 2 {
usage()
}
domain := os.Args[1]
password = getPassword()
// TODO: Output in Netscape format
for _, cookie := range getCookies(domain) {
fmt.Printf("%s/%s: %s\n", cookie.Domain, cookie.Key, cookie.DecryptedValue())
}
}
func decryptValue(encryptedValue []byte) string {
key := pbkdf2.Key([]byte(password), []byte(salt), iterations, length, sha1.New)
block, err := aes.NewCipher(key)
if err != nil {
log.Fatal(err)
}
decrypted := make([]byte, len(encryptedValue))
cbc := cipher.NewCBCDecrypter(block, []byte(iv))
cbc.CryptBlocks(decrypted, encryptedValue)
plainText, err := aesStripPadding(decrypted)
if err != nil {
fmt.Println("Error decrypting:", err)
return ""
}
return string(plainText)
}
// In the padding scheme the last <padding length> bytes
// have a value equal to the padding length, always in (1,16]
func aesStripPadding(data []byte) ([]byte, error) {
if len(data)%length != 0 {
return nil, fmt.Errorf("decrypted data block length is not a multiple of %d", length)
}
paddingLen := int(data[len(data)-1])
if paddingLen > 16 {
return nil, fmt.Errorf("invalid last block padding length: %d", paddingLen)
}
return data[:len(data)-paddingLen], nil
}
func getPassword() string {
parts := strings.Fields("security find-generic-password -wga Chrome")
cmd := parts[0]
parts = parts[1:len(parts)]
out, err := exec.Command(cmd, parts...).Output()
if err != nil {
log.Fatal("error finding password ", err)
}
return strings.Trim(string(out), "\n")
}
func getCookies(domain string) (cookies []Cookie) {
usr, _ := user.Current()
cookiesFile := fmt.Sprintf("%s/Library/Application Support/Google/Chrome/Default/Cookies", usr.HomeDir)
db, err := sql.Open("sqlite3", cookiesFile)
if err != nil {
log.Fatal(err)
}
defer db.Close()
rows, err := db.Query("SELECT name, value, host_key, encrypted_value FROM cookies WHERE host_key like ?", fmt.Sprintf("%%%s%%", domain))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var name, value, hostKey string
var encryptedValue []byte
rows.Scan(&name, &value, &hostKey, &encryptedValue)
cookies = append(cookies, Cookie{hostKey, name, value, encryptedValue})
}
return
}
@DinoChiesa
Copy link

Thanks for this, very helpful. The imports need updating now that it's 2016. But this worked great.

import (
    //"code.google.com/p/go.crypto/pbkdf2"
    // "github.com/golang/crypto/pbkdf2"
    "golang.org/x/crypto/pbkdf2"

    "crypto/aes"
    "crypto/cipher"
    "crypto/sha1"
    "database/sql"
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/user"
    "strings"

    _ "github.com/mattn/go-sqlite3"
)

@AddaxSoft
Copy link

Anything for windows?

@dolmen
Copy link

dolmen commented Feb 20, 2018

Please give a license on this code. I would like to reuse it / improve it.

@dacort
Copy link
Author

dacort commented Apr 30, 2018

@dolmen - Apologies, just saw your comment. License is MIT.

@DukeyToo
Copy link

https://github.com/zellyn/kooky has a version that works in Safari too. I think they used yours as the basis for the Chrome piece. Example app (same syntax/params as cookiemonster.go):

package main

import (
  "fmt"
  "os/user"
  "os"
  "time"

  "github.com/zellyn/kooky"
)

func usage() {
	fmt.Fprintf(os.Stderr, "usage: %s [domain]\n", os.Args[0])
	os.Exit(2)
}

func main() {
  if len(os.Args) != 2 {
    usage()
  }

  domain := os.Args[1]

  var cookies []*kooky.Cookie
  var err error

  usr, _ := user.Current()

  // safari first
  cookiesFile := fmt.Sprintf("%s/Library/Cookies/Cookies.binarycookies", usr.HomeDir)
  cookies, err = kooky.ReadSafariCookies(cookiesFile, domain, "", time.Time{})
  if len(cookies) == 0 {
    // safari had none, try chrome
    cookiesFile := fmt.Sprintf("%s/Library/Application Support/Google/Chrome/Default/Cookies", usr.HomeDir)
    cookies, err = kooky.ReadChromeCookies(cookiesFile, domain, "", time.Time{})
  }

  if err != nil {
    return
  }

  for _, cookie := range cookies {
    fmt.Printf("%s/%s: %s\n", cookie.Domain, cookie.Name, cookie.Value)
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment