├── README.md ├── go.mod ├── go.sum └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # Gookies 2 | 3 | Similar to the excellent SharpChrome project, the tool will decrypt the Chrome cookie store and display them either in a JSON format ready for import into your cookie manager, for example EditThisCookie or alternately a canonicalised cookie header for use within command line tools like curl. 4 | 5 | The tool currently supports multiple Chrome profiles, domain listing, domain filter and output format and also support from Chrome 80+ which has a different cookie encryption scheme to version 79 or below. 6 | 7 | ```bash 8 | Usage of gookies.exe: 9 | -domain string 10 | Show the canonicalised cookie value for a specific domain 11 | -format string 12 | The output format: string, json (default) (default "json") 13 | -profile int 14 | Which Chrome profile index to extract cookie data from, uses Default when not specified 15 | -profiles 16 | Lists the profile names and index to Chrome profiles under this account 17 | -storeId string 18 | The storeId to embed in exported JSON, incognito is storeId 1 in Chrome (default "0") 19 | ``` 20 | Currently gookies only supports Windows, but the plan is to add support for Linux and MacOS with the added bonus of no dependency binaries produced by Go. 21 | 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/CCob/gookies 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/mattn/go-sqlite3 v2.0.3+incompatible 7 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= 2 | github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 3 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 4 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= 5 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 6 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 7 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 8 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "database/sql" 8 | "encoding/base64" 9 | "encoding/json" 10 | "flag" 11 | "fmt" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "sort" 16 | "syscall" 17 | "unsafe" 18 | 19 | _ "github.com/mattn/go-sqlite3" 20 | "golang.org/x/net/publicsuffix" 21 | ) 22 | 23 | var ( 24 | dllcrypt32 = syscall.NewLazyDLL("Crypt32.dll") 25 | dllkernel32 = syscall.NewLazyDLL("Kernel32.dll") 26 | 27 | pCryptUnprotectData = dllcrypt32.NewProc("CryptUnprotectData") 28 | pLocalFree = dllkernel32.NewProc("LocalFree") 29 | ) 30 | 31 | type DATA_BLOB struct { 32 | cbData uint32 33 | pbData *byte 34 | } 35 | 36 | // Cookie - Items for a cookie 37 | type Cookie struct { 38 | Domain string `json:"domain"` 39 | ExpirationDate int64 `json:"expirationDate"` 40 | HostOnly bool `json:"hostOnly"` 41 | HttpOnly bool `json:"httpOnly"` 42 | Name string `json:"name"` 43 | Path string `json:"path"` 44 | SameSite string `json:"sameSite"` 45 | Secure bool `json:"secure"` 46 | Session bool `json:"session"` 47 | Value string `json:"value"` 48 | StoreId string `json:"storeId"` 49 | ID int `json:"id"` 50 | EncryptedValue []byte `json:"-"` 51 | } 52 | 53 | var profile, storeId string 54 | var aesKey []byte 55 | 56 | func ifThenElse(condition bool, a interface{}, b interface{}) interface{} { 57 | if condition { 58 | return a 59 | } 60 | return b 61 | } 62 | 63 | func newBlob(d []byte) *DATA_BLOB { 64 | if len(d) == 0 { 65 | return &DATA_BLOB{} 66 | } 67 | return &DATA_BLOB{ 68 | pbData: &d[0], 69 | cbData: uint32(len(d)), 70 | } 71 | } 72 | 73 | func (b *DATA_BLOB) toByteArray() []byte { 74 | d := make([]byte, b.cbData) 75 | copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:]) 76 | return d 77 | } 78 | 79 | func (c *Cookie) decryptCookie() { 80 | if c.Value > "" { 81 | return 82 | } 83 | 84 | if len(c.EncryptedValue) > 0 { 85 | var decryptedValue, _ = decryptValue(c.EncryptedValue) 86 | c.Value = string(decryptedValue) 87 | } 88 | } 89 | 90 | func getAesGCMKey() []byte { 91 | 92 | var encryptedKey []byte 93 | var path, _ = os.UserCacheDir() 94 | var localStateFile = fmt.Sprintf("%s\\Google\\Chrome\\User Data\\Local State", path) 95 | 96 | data, _ := ioutil.ReadFile(localStateFile) 97 | var localState map[string]interface{} 98 | json.Unmarshal(data, &localState) 99 | 100 | if localState["os_crypt"] != nil { 101 | 102 | encryptedKey, _ = base64.StdEncoding.DecodeString(localState["os_crypt"].(map[string]interface{})["encrypted_key"].(string)) 103 | 104 | if bytes.Equal(encryptedKey[0:5], []byte{'D', 'P', 'A', 'P', 'I'}) { 105 | encryptedKey, _ = decryptValue(encryptedKey[5:]) 106 | } else { 107 | fmt.Print("encrypted_key does not look like DPAPI key\n") 108 | } 109 | } 110 | 111 | return encryptedKey 112 | } 113 | 114 | func decryptValue(data []byte) ([]byte, error) { 115 | 116 | if bytes.Equal(data[0:3], []byte{'v', '1', '0'}) { 117 | 118 | aesBlock, _ := aes.NewCipher(aesKey) 119 | aesgcm, _ := cipher.NewGCM(aesBlock) 120 | 121 | nonce := data[3:15] 122 | encryptedData := data[15:] 123 | 124 | plaintext, _ := aesgcm.Open(nil, nonce, encryptedData, nil) 125 | 126 | return plaintext, nil 127 | 128 | } else { 129 | 130 | var outblob DATA_BLOB 131 | r, _, err := pCryptUnprotectData.Call(uintptr(unsafe.Pointer(newBlob(data))), 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&outblob))) 132 | if r == 0 { 133 | return nil, err 134 | } 135 | defer pLocalFree.Call(uintptr(unsafe.Pointer(outblob.pbData))) 136 | return outblob.toByteArray(), nil 137 | } 138 | } 139 | 140 | func getDomains() []string { 141 | 142 | cookies := getCookies(nil) 143 | domains := make(map[string]bool) 144 | 145 | for _, cookie := range cookies { 146 | if cookie.Domain[0] == '.' { 147 | cookie.Domain = cookie.Domain[1:] 148 | } 149 | domains[cookie.Domain] = true 150 | } 151 | 152 | keys := make([]string, 0, len(domains)) 153 | for k := range domains { 154 | keys = append(keys, k) 155 | } 156 | 157 | sort.Strings(keys) 158 | return keys 159 | } 160 | 161 | func getCookies(domain *string) (cookies []Cookie) { 162 | 163 | var rows *sql.Rows 164 | var path, err = os.UserCacheDir() 165 | var cookiesFile = fmt.Sprintf("%s\\Google\\Chrome\\User Data\\%s\\Cookies", path, profile) 166 | 167 | db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&mode=ro", cookiesFile)) 168 | if err != nil { 169 | log.Fatal(err) 170 | } 171 | db.SetMaxOpenConns(1) 172 | defer db.Close() 173 | 174 | if domain != nil { 175 | rows, err = db.Query("SELECT host_key as Domain, expires_utc as ExpirationDate, is_httponly as HttpOnly, name as Name, path as Path, samesite as SameSite, "+ 176 | "is_secure as Secure, is_persistent as Session, value as Value, encrypted_value as EncryptedValue FROM cookies WHERE Domain LIKE ?", fmt.Sprintf("%%%s", *domain)) 177 | } else { 178 | rows, err = db.Query("SELECT name, value, host_key, encrypted_value FROM cookies") 179 | } 180 | 181 | if err != nil { 182 | log.Fatal(err) 183 | } 184 | 185 | defer rows.Close() 186 | index := int(1) 187 | for rows.Next() { 188 | 189 | var cookie Cookie 190 | var sameSite, secure, session, httpOnly int 191 | 192 | rows.Scan(&cookie.Domain, &cookie.ExpirationDate, &httpOnly, &cookie.Name, 193 | &cookie.Path, &sameSite, &secure, &session, &cookie.Value, &cookie.EncryptedValue) 194 | 195 | cookie.SameSite = "unspecified" 196 | cookie.Session = session == 0 197 | cookie.HttpOnly = httpOnly == 1 198 | cookie.Secure = secure == 1 199 | cookie.StoreId = storeId 200 | cookie.ID = index 201 | cookie.ExpirationDate = (cookie.ExpirationDate / 1000000) - 11644473600 202 | cookie.decryptCookie() 203 | 204 | cookies = append(cookies, cookie) 205 | index++ 206 | } 207 | 208 | return 209 | } 210 | 211 | func getCanonicalCookieValue(domain string) string { 212 | 213 | tld, _ := publicsuffix.EffectiveTLDPlusOne(domain) 214 | 215 | if tld != "" { 216 | tld = fmt.Sprintf(".%s", tld) 217 | } 218 | 219 | cookies := append(getCookies(&tld), getCookies(&domain)...) 220 | result := "" 221 | 222 | for index, cookie := range cookies { 223 | lastCookie := index == len(cookies)-1 224 | result += fmt.Sprintf("%s=%s%s", cookie.Name, cookie.Value, ifThenElse(lastCookie, "", "; ")) 225 | } 226 | 227 | return result 228 | } 229 | 230 | func getProfiles() (result map[int]string) { 231 | 232 | result = make(map[int]string) 233 | result[0] = "Default" 234 | var path, _ = os.UserCacheDir() 235 | var userDataFolder = fmt.Sprintf("%s\\Google\\Chrome\\User Data", path) 236 | 237 | files, _ := ioutil.ReadDir(userDataFolder) 238 | 239 | for _, file := range files { 240 | var profileID int 241 | if file.IsDir() { 242 | _, err := fmt.Sscanf(file.Name(), "Profile %d", &profileID) 243 | if err == nil { 244 | 245 | data, _ := ioutil.ReadFile(fmt.Sprintf("%s\\%s\\Preferences", userDataFolder, file.Name())) 246 | var preferences map[string]interface{} 247 | json.Unmarshal(data, &preferences) 248 | result[profileID] = preferences["profile"].(map[string]interface{})["name"].(string) 249 | } 250 | } 251 | } 252 | 253 | return result 254 | } 255 | 256 | func main() { 257 | 258 | var domainName, format string 259 | profiles := false 260 | profileIndex := 0 261 | 262 | flag.BoolVar(&profiles, "profiles", false, "Lists the profile names and index to Chrome profiles under this account") 263 | flag.IntVar(&profileIndex, "profile", 0, "Which Chrome profile index to extract cookie data from, uses Default when not specified") 264 | flag.StringVar(&domainName, "domain", "", "Show the canonicalised cookie value for a specific domain") 265 | flag.StringVar(&storeId, "storeId", "0", "The storeId to embed in exported JSON, incognito is storeId 1 in Chrome") 266 | flag.StringVar(&format, "format", "json", "The output format: string, json (default)") 267 | 268 | flag.Parse() 269 | 270 | if profiles { 271 | profileMap := getProfiles() 272 | 273 | for profileID, profileName := range profileMap { 274 | fmt.Printf("%d: %s\n", profileID, profileName) 275 | } 276 | 277 | return 278 | } 279 | 280 | if profileIndex == 0 { 281 | profile = "Default" 282 | } else { 283 | profile = fmt.Sprintf("Profile %d", profileIndex) 284 | } 285 | 286 | if domainName == "" { 287 | 288 | domains := getDomains() 289 | 290 | for _, domain := range domains { 291 | fmt.Printf("Domain: %s\n", domain) 292 | } 293 | 294 | } else { 295 | 296 | aesKey = getAesGCMKey() 297 | 298 | if format == "json" { 299 | result, _ := json.Marshal(getCookies(&domainName)) 300 | fmt.Print(string(result)) 301 | } else { 302 | fmt.Printf("Cookies: %s\n", getCanonicalCookieValue(domainName)) 303 | } 304 | } 305 | } 306 | --------------------------------------------------------------------------------