├── .gitignore ├── cmd └── main.go ├── browserCookie_test.go ├── README.md ├── LICENSE ├── chrome_darwin.go └── browserCookie.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | ".." 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | func main() { 10 | cookieJar, err := browsercookie.LoadCookieJarFromChrome("https://bing.com") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | fmt.Println(cookieJar) 15 | } 16 | -------------------------------------------------------------------------------- /browserCookie_test.go: -------------------------------------------------------------------------------- 1 | package browsercookie 2 | 3 | import ( 4 | "testing" 5 | "fmt" 6 | "time" 7 | "os/user" 8 | ) 9 | 10 | func TestReadChromeCookies(t *testing.T) { 11 | cookieJar, _ := LoadCookieJarFromChrome("") 12 | fmt.Println("CookieJar=", cookieJar) 13 | 14 | usr, _ := user.Current() 15 | cookiesFile := fmt.Sprintf("%s/Library/Application Support/Google/Chrome/Default/Cookies", usr.HomeDir) 16 | cookies, _ := ReadChromeCookies(cookiesFile, "", "", time.Time{}) 17 | fmt.Println("Cookies=", cookies) 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-browsercookie 2 | 3 | - Port [Browsercookie](https://pypi.org/project/browsercookie/) from Python to Golang 4 | 5 | ## Install 6 | 7 | ```golang 8 | go get -u github.com/onyas/go-browsercookie 9 | ``` 10 | 11 | ## Usage 12 | 13 | ``` 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/onyas/go-browsercookie" 19 | "log" 20 | ) 21 | 22 | func main() { 23 | cookieJar, error := browsercookie.Chrome("https://google.com") 24 | if error != nil { 25 | log.Fatal(error) 26 | } 27 | 28 | log.Println(cookieJar) 29 | } 30 | 31 | ``` 32 | 33 | 34 | ## Thanks/references 35 | 36 | It's a Wrapper for [zellyn/kooky](https://github.com/zellyn/kooky), all the glory should belongs to [@zellyn]() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 morty 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chrome_darwin.go: -------------------------------------------------------------------------------- 1 | package browsercookie 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/sha1" 7 | "errors" 8 | "fmt" 9 | 10 | "golang.org/x/crypto/pbkdf2" 11 | 12 | keychain "github.com/keybase/go-keychain" 13 | ) 14 | 15 | // Thanks to https://gist.github.com/dacort/bd6a5116224c594b14db. 16 | 17 | const ( 18 | salt = "saltysalt" 19 | iv = " " 20 | length = 16 21 | iterations = 1003 22 | ) 23 | 24 | // keychainPassword is a cache of the password read from the keychain. 25 | var keychainPassword []byte 26 | 27 | // setChromeKeychainPassword exists so tests can avoid trying to read 28 | // the Keychain. 29 | func setChromeKeychainPassword(password []byte) []byte { 30 | oldPassword := keychainPassword 31 | keychainPassword = password 32 | return oldPassword 33 | } 34 | 35 | // getKeychainPassword retrieves the Chrome Safe Storage password, 36 | // caching it for future calls. 37 | func getKeychainPassword() ([]byte, error) { 38 | if keychainPassword == nil { 39 | password, err := keychain.GetGenericPassword("Chrome Safe Storage", "Chrome", "", "") 40 | if err != nil { 41 | return nil, fmt.Errorf("error reading 'Chrome Safe Storage' keychain password: %v", err) 42 | } 43 | keychainPassword = password 44 | } 45 | return keychainPassword, nil 46 | } 47 | 48 | func decryptValue(encrypted []byte) (string, error) { 49 | if len(encrypted) == 0 { 50 | return "", errors.New("empty encrypted value") 51 | } 52 | 53 | if len(encrypted) <= 3 { 54 | return "", fmt.Errorf("too short encrypted value (%d<=3)", len(encrypted)) 55 | } 56 | 57 | encrypted = encrypted[3:] 58 | 59 | password, err := getKeychainPassword() 60 | if err != nil { 61 | return "", err 62 | } 63 | 64 | key := pbkdf2.Key(password, []byte(salt), iterations, length, sha1.New) 65 | block, err := aes.NewCipher(key) 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | decrypted := make([]byte, len(encrypted)) 71 | cbc := cipher.NewCBCDecrypter(block, []byte(iv)) 72 | cbc.CryptBlocks(decrypted, encrypted) 73 | 74 | plainText, err := aesStripPadding(decrypted) 75 | if err != nil { 76 | return "", err 77 | } 78 | return string(plainText), nil 79 | } 80 | 81 | // In the padding scheme the last bytes 82 | // have a value equal to the padding length, always in (1,16] 83 | func aesStripPadding(data []byte) ([]byte, error) { 84 | if len(data)%length != 0 { 85 | return nil, fmt.Errorf("decrypted data block length is not a multiple of %d", length) 86 | } 87 | paddingLen := int(data[len(data)-1]) 88 | if paddingLen > 16 { 89 | return nil, fmt.Errorf("invalid last block padding length: %d", paddingLen) 90 | } 91 | return data[:len(data)-paddingLen], nil 92 | } 93 | 94 | -------------------------------------------------------------------------------- /browserCookie.go: -------------------------------------------------------------------------------- 1 | package browsercookie 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/cookiejar" 7 | "net/url" 8 | "os/user" 9 | "time" 10 | 11 | "github.com/go-sqlite/sqlite3" 12 | ) 13 | 14 | func Chrome(cookileUrl string) (http.CookieJar, error) { 15 | jar, _ := cookiejar.New(nil) 16 | 17 | usr, _ := user.Current() 18 | cookiesFile := fmt.Sprintf("%s/Library/Application Support/Google/Chrome/Default/Cookies", usr.HomeDir) 19 | cookies, err := ReadChromeCookies(cookiesFile, "", "", time.Time{}) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | cookieURL, _ := url.Parse(cookileUrl) 25 | jar.SetCookies(cookieURL, cookies) 26 | 27 | return jar, nil 28 | } 29 | 30 | func ReadChromeCookies(filename string, domainFilter string, nameFilter string, expireAfter time.Time) ([]*http.Cookie, error) { 31 | var cookies []*http.Cookie 32 | var unexpectedCols bool 33 | db, err := sqlite3.Open(filename) 34 | if err != nil { 35 | return nil, err 36 | } 37 | defer db.Close() 38 | 39 | if err := db.VisitTableRecords("cookies", func(rowId *int64, rec sqlite3.Record) error { 40 | if rowId == nil { 41 | return fmt.Errorf("unexpected nil RowID in Chrome sqlite database") 42 | } 43 | cookie := &http.Cookie{} 44 | 45 | if len(rec.Values) != 14 { 46 | unexpectedCols = true 47 | return nil 48 | } 49 | 50 | domain, ok := rec.Values[1].(string) 51 | if !ok { 52 | return fmt.Errorf("expected column 2 (host_key) to to be string; got %T", rec.Values[1]) 53 | } 54 | name, ok := rec.Values[2].(string) 55 | if !ok { 56 | return fmt.Errorf("expected column 3 (name) in cookie(domain:%s) to to be string; got %T", domain, rec.Values[2]) 57 | } 58 | value, ok := rec.Values[3].(string) 59 | if !ok { 60 | return fmt.Errorf("expected column 4 (value) in cookie(domain:%s, name:%s) to to be string; got %T", domain, name, rec.Values[3]) 61 | } 62 | path, ok := rec.Values[4].(string) 63 | if !ok { 64 | return fmt.Errorf("expected column 5 (path) in cookie(domain:%s, name:%s) to to be string; got %T", domain, name, rec.Values[4]) 65 | } 66 | var expires_utc int64 67 | switch i := rec.Values[5].(type) { 68 | case int64: 69 | expires_utc = i 70 | case int: 71 | if i != 0 { 72 | return fmt.Errorf("expected column 6 (expires_utc) in cookie(domain:%s, name:%s) to to be int64 or int with value=0; got %T with value %v", domain, name, rec.Values[5], rec.Values[5]) 73 | } 74 | default: 75 | return fmt.Errorf("expected column 6 (expires_utc) in cookie(domain:%s, name:%s) to to be int64 or int with value=0; got %T with value %v", domain, name, rec.Values[5], rec.Values[5]) 76 | } 77 | encrypted_value, ok := rec.Values[12].([]byte) 78 | if !ok { 79 | return fmt.Errorf("expected column 13 (encrypted_value) in cookie(domain:%s, name:%s) to to be []byte; got %T", domain, name, rec.Values[12]) 80 | } 81 | 82 | var expiry time.Time 83 | if expires_utc != 0 { 84 | expiry = chromeCookieDate(expires_utc) 85 | } 86 | //creation := chromeCookieDate(*rowId) 87 | 88 | if domainFilter != "" && domain != domainFilter { 89 | return nil 90 | } 91 | 92 | if nameFilter != "" && name != nameFilter { 93 | return nil 94 | } 95 | 96 | if !expiry.IsZero() && expiry.Before(expireAfter) { 97 | return nil 98 | } 99 | 100 | cookie.Domain = domain 101 | cookie.Name = name 102 | cookie.Path = path 103 | cookie.Expires = expiry 104 | //cookie.Creation = creation 105 | cookie.Secure = rec.Values[6] == 1 106 | cookie.HttpOnly = rec.Values[7] == 1 107 | 108 | if len(encrypted_value) > 0 { 109 | decrypted, err := decryptValue(encrypted_value) 110 | if err != nil { 111 | return fmt.Errorf("decrypting cookie %v: %v", cookie, err) 112 | } 113 | cookie.Value = decrypted 114 | } else { 115 | cookie.Value = value 116 | } 117 | cookies = append(cookies, cookie) 118 | 119 | return nil 120 | }); err != nil { 121 | return nil, err 122 | } 123 | 124 | if cookies == nil && unexpectedCols { 125 | return nil, fmt.Errorf("no cookies found, but we skipped rows with an unexpected number of columns (not 14)") 126 | } 127 | return cookies, nil 128 | } 129 | 130 | // See https://cs.chromium.org/chromium/src/base/time/time.h?l=452&rcl=fceb9a030c182e939a436a540e6dacc70f161cb1 131 | const windowsToUnixMicrosecondsOffset = 11644473600000000 132 | 133 | // chromeCookieDate converts microseconds to a time.Time object, 134 | // accounting for the switch to Windows epoch (Jan 1 1601). 135 | func chromeCookieDate(timestamp_utc int64) time.Time { 136 | if timestamp_utc > windowsToUnixMicrosecondsOffset { 137 | timestamp_utc -= windowsToUnixMicrosecondsOffset 138 | } 139 | 140 | return time.Unix(timestamp_utc/1000000, (timestamp_utc%1000000)*1000) 141 | } 142 | --------------------------------------------------------------------------------