- Implement `Browser` interface and related functions - Add `Passwords` method to `chromium` for exporting and returning passwords - Ignore `/docs` directoryfeat/library
							parent
							
								
									3eaa2dd2bf
								
							
						
					
					
						commit
						aa2e1af5b5
					
				| @ -0,0 +1,86 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| type Browser interface { | ||||
| 	BrowserData | ||||
| 
 | ||||
| 	Init() error | ||||
| } | ||||
| 
 | ||||
| func NewBrowser(b browser, options ...BrowserOption) (Browser, error) { | ||||
| 	browser := browsers[b] | ||||
| 	if setter, ok := browser.(browserOptionsSetter); ok { | ||||
| 		for _, option := range options { | ||||
| 			option(setter) | ||||
| 		} | ||||
| 	} | ||||
| 	if err := browser.Init(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return browser, nil | ||||
| } | ||||
| 
 | ||||
| type browser string | ||||
| 
 | ||||
| type BrowserData interface { | ||||
| 	Passwords() ([]Password, error) | ||||
| 
 | ||||
| 	Cookies() ([]Cookie, error) | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) BrowsingData(items []browserDataType) ([]BrowserData, error) { | ||||
| 	for _, item := range items { | ||||
| 		_ = item | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) AllBrowsingData() ([]BrowserData, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) BrowsingData(items []browserDataType) (BrowserData, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	Chrome  browser = "chrome" | ||||
| 	Firefox browser = "firefox" | ||||
| 	Yandex  browser = "yandex" | ||||
| ) | ||||
| 
 | ||||
| type browserType int | ||||
| 
 | ||||
| const ( | ||||
| 	browserTypeChromium browserType = iota + 1 | ||||
| 	browserTypeFirefox | ||||
| 	browserTypeYandex | ||||
| ) | ||||
| 
 | ||||
| func (b browser) Type() browserType { | ||||
| 	switch b { | ||||
| 	case Firefox: | ||||
| 		return browserTypeYandex | ||||
| 	case Yandex: | ||||
| 		return browserTypeFirefox | ||||
| 	default: | ||||
| 		return browserTypeChromium | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	browsers = map[browser]Browser{ | ||||
| 		Chrome: &chromium{ | ||||
| 			name:          Chrome, | ||||
| 			storage:       chromeStorageName, | ||||
| 			profilePath:   chromeProfilePath, | ||||
| 			supportedData: []browserDataType{TypePassword}, | ||||
| 		}, | ||||
| 		Firefox: &firefox{ | ||||
| 			name:        "", | ||||
| 			storage:     "", | ||||
| 			profilePath: "", | ||||
| 		}, | ||||
| 		Yandex: &chromium{}, | ||||
| 	} | ||||
| ) | ||||
| @ -0,0 +1,60 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| type browserDataType int | ||||
| 
 | ||||
| const ( | ||||
| 	TypePassword browserDataType = iota + 1 | ||||
| 	TypeCookie | ||||
| 	TypeHistory | ||||
| 	TypeBookmark | ||||
| 	TypeCreditCard | ||||
| 	TypeDownload | ||||
| 	TypeExtensions | ||||
| 	TypeSessionStorage | ||||
| 	TypeLocalStorage | ||||
| ) | ||||
| 
 | ||||
| func (i browserDataType) Filename(b browser) string { | ||||
| 	switch b.Type() { | ||||
| 	case browserTypeChromium: | ||||
| 		return i.chromiumFilename() | ||||
| 	case browserTypeFirefox: | ||||
| 		return i.firefoxFilename() | ||||
| 	case browserTypeYandex: | ||||
| 		return i.yandexFilename() | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (i browserDataType) chromiumFilename() string { | ||||
| 	switch i { | ||||
| 	case TypePassword: | ||||
| 		return "Login Data" | ||||
| 	case TypeCookie: | ||||
| 		return "Cookies" | ||||
| 	case TypeHistory: | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (i browserDataType) yandexFilename() string { | ||||
| 	switch i { | ||||
| 	case TypePassword: | ||||
| 		return "Login State" | ||||
| 	case TypeCookie: | ||||
| 		return "Cookies" | ||||
| 	case TypeHistory: | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| 
 | ||||
| func (i browserDataType) firefoxFilename() string { | ||||
| 	switch i { | ||||
| 	case TypePassword: | ||||
| 		return "logins.json" | ||||
| 	case TypeCookie: | ||||
| 		return "cookies.sqlite" | ||||
| 	case TypeHistory: | ||||
| 	} | ||||
| 	return "" | ||||
| } | ||||
| @ -0,0 +1,139 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/sha1" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"io/fs" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"golang.org/x/crypto/pbkdf2" | ||||
| 
 | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/fileutil" | ||||
| ) | ||||
| 
 | ||||
| type chromium struct { | ||||
| 	name               browser | ||||
| 	storage            string | ||||
| 	profilePath        string | ||||
| 	profilePaths       []string | ||||
| 	disableFindAllUser bool | ||||
| 	masterKey          []byte | ||||
| 	supportedData      []browserDataType | ||||
| 	supportedDataMap   map[browserDataType]struct{} | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) Init() error { | ||||
| 	if err := c.initBrowserData(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err := c.initProfile(); err != nil { | ||||
| 		return fmt.Errorf("profile path '%s' does not exist %w", c.profilePath, ErrBrowserNotExists) | ||||
| 	} | ||||
| 	if err := c.initMasterKey(); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) initBrowserData() error { | ||||
| 	if c.supportedDataMap == nil { | ||||
| 		c.supportedDataMap = make(map[browserDataType]struct{}) | ||||
| 	} | ||||
| 	for _, v := range c.supportedData { | ||||
| 		c.supportedDataMap[v] = struct{}{} | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) initProfile() error { | ||||
| 	if !fileutil.IsDirExists(c.profilePath) { | ||||
| 		return ErrBrowserNotExists | ||||
| 	} | ||||
| 	if !c.disableFindAllUser { | ||||
| 		profilesPaths, err := c.findAllProfiles() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		c.profilePaths = profilesPaths | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) findAllProfiles() ([]string, error) { | ||||
| 	var profiles []string | ||||
| 	root := fileutil.ParentDir(c.profilePath) | ||||
| 	err := filepath.Walk(root, func(path string, info fs.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		// if the path ends with "History", add it to the list
 | ||||
| 		if strings.HasSuffix(path, "History") { | ||||
| 			if !strings.Contains(path, "System Profile") { | ||||
| 				profiles = append(profiles, filepath.Dir(path)) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// calculate the depth of the current path
 | ||||
| 		depth := len(strings.Split(path, string(filepath.Separator))) - len(strings.Split(root, string(filepath.Separator))) | ||||
| 
 | ||||
| 		// if the depth is more than 2 and it's a directory, skip it
 | ||||
| 		if info.IsDir() && path != root && depth >= 2 { | ||||
| 			return filepath.SkipDir | ||||
| 		} | ||||
| 
 | ||||
| 		return err | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return profiles, err | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) initMasterKey() error { | ||||
| 	var ( | ||||
| 		stdout, stderr bytes.Buffer | ||||
| 	) | ||||
| 	args := []string{"find-generic-password", "-wa", strings.TrimSpace(c.storage)} | ||||
| 	cmd := exec.Command("security", args...) //nolint:gosec
 | ||||
| 	cmd.Stdout = &stdout | ||||
| 	cmd.Stderr = &stderr | ||||
| 	if err := cmd.Run(); err != nil { | ||||
| 		return fmt.Errorf("run security command failed: %w, message %s", err, stderr.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	if stderr.Len() > 0 { | ||||
| 		if strings.Contains(stderr.String(), "could not be found") { | ||||
| 			return ErrCouldNotFindInKeychain | ||||
| 		} | ||||
| 		return errors.New(stderr.String()) | ||||
| 	} | ||||
| 
 | ||||
| 	secret := bytes.TrimSpace(stdout.Bytes()) | ||||
| 	if len(secret) == 0 { | ||||
| 		return ErrNoPasswordInOutput | ||||
| 	} | ||||
| 	salt := []byte("saltysalt") | ||||
| 	// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
 | ||||
| 	key := pbkdf2.Key(secret, salt, 1003, 16, sha1.New) | ||||
| 	if key == nil { | ||||
| 		return ErrWrongSecurityCommand | ||||
| 	} | ||||
| 	c.masterKey = key | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) setProfilePath(p string) { | ||||
| 	c.profilePath = p | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) setEnableAllUsers(e bool) { | ||||
| 	c.disableFindAllUser = e | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) setStorageName(s string) { | ||||
| 	c.storage = s | ||||
| } | ||||
| @ -0,0 +1,18 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestChromium_Init(t *testing.T) { | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func BenchmarkChromium_Init(b *testing.B) { | ||||
| 	chromium := browsers[Chrome] | ||||
| 	for i := 0; i < b.N; i++ { | ||||
| 		if err := chromium.Init(); err != nil { | ||||
| 			b.Fatal(err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,38 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	chromeStorageName     = "Chrome" | ||||
| 	chromeBetaStorageName = "Chrome" | ||||
| 	chromiumStorageName   = "Chromium" | ||||
| 	edgeStorageName       = "Microsoft Edge" | ||||
| 	braveStorageName      = "Brave" | ||||
| 	operaStorageName      = "Opera" | ||||
| 	vivaldiStorageName    = "Vivaldi" | ||||
| 	coccocStorageName     = "CocCoc" | ||||
| 	yandexStorageName     = "Yandex" | ||||
| 	arcStorageName        = "Arc" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	homeDir, _ = os.UserHomeDir() | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	chromeProfilePath     = homeDir + "/Library/Application Support/Google/Chrome/Default/" | ||||
| 	chromeBetaProfilePath = homeDir + "/Library/Application Support/Google/Chrome Beta/Default/" | ||||
| 	chromiumProfilePath   = homeDir + "/Library/Application Support/Chromium/Default/" | ||||
| 	edgeProfilePath       = homeDir + "/Library/Application Support/Microsoft Edge/Default/" | ||||
| 	braveProfilePath      = homeDir + "/Library/Application Support/BraveSoftware/Brave-Browser/Default/" | ||||
| 	operaProfilePath      = homeDir + "/Library/Application Support/com.operasoftware.Opera/Default/" | ||||
| 	operaGXProfilePath    = homeDir + "/Library/Application Support/com.operasoftware.OperaGX/Default/" | ||||
| 	vivaldiProfilePath    = homeDir + "/Library/Application Support/Vivaldi/Default/" | ||||
| 	coccocProfilePath     = homeDir + "/Library/Application Support/Coccoc/Default/" | ||||
| 	yandexProfilePath     = homeDir + "/Library/Application Support/Yandex/YandexBrowser/Default/" | ||||
| 	arcProfilePath        = homeDir + "/Library/Application Support/Arc/User Data/Default" | ||||
| 
 | ||||
| 	firefoxProfilePath = homeDir + "/Library/Application Support/Firefox/Profiles/" | ||||
| ) | ||||
| @ -0,0 +1,15 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| type Cookie struct { | ||||
| 	Domain     string | ||||
| 	Expiration float64 | ||||
| 	Value      string | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) Cookies() ([]Cookie, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) Cookies() ([]Cookie, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
| @ -0,0 +1,12 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"errors" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrBrowserNotExists       = errors.New("browser not exists") | ||||
| 	ErrWrongSecurityCommand   = errors.New("wrong security command") | ||||
| 	ErrNoPasswordInOutput     = errors.New("no password in output") | ||||
| 	ErrCouldNotFindInKeychain = errors.New("could not be find in keychain") | ||||
| ) | ||||
| @ -0,0 +1,50 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	hkb "github.com/moond4rk/hackbrowserdata" | ||||
| ) | ||||
| 
 | ||||
| func main() { | ||||
| 	chrome, err := hkb.NewBrowser(hkb.Chrome) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	passwords, err := chrome.Passwords() | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	for _, pass := range passwords { | ||||
| 		fmt.Printf("%+v\n", pass) | ||||
| 	} | ||||
| 	// cookies, err := chrome.Cookies()
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	panic(err)
 | ||||
| 	// }
 | ||||
| 
 | ||||
| 	// creditCards, err := browser.CreditCards()
 | ||||
| 	// bookmarks, err := browser.Bookmarks()
 | ||||
| 	// downloads, err := browser.Downloads()
 | ||||
| 	// extensions, err := browser.Extensions()
 | ||||
| 	// history, err := browser.History()
 | ||||
| 	// localStorage, err := browser.LocalStorage()
 | ||||
| 	// sessionStorage, err := browser.SessionStorage()
 | ||||
| 
 | ||||
| 	// items := []hkb.browsingDataType{hkb.Cookie, hkb.Password, hkb.History, hkb.Bookmark}
 | ||||
| 	// browsingData, err := browser.BrowsingDatas(items)
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	panic(err)
 | ||||
| 	// }
 | ||||
| 	//
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	panic(err)
 | ||||
| 	// }
 | ||||
| 	// _ = cookies
 | ||||
| 	// items := []hkb.browsingDataType{hkb.Cookie}
 | ||||
| 	// datas, err := browser.BrowsingDatas(items)
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	panic(err)
 | ||||
| 	// }
 | ||||
| 	// _ = datas
 | ||||
| } | ||||
| @ -0,0 +1,25 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| type firefox struct { | ||||
| 	name          string | ||||
| 	storage       string | ||||
| 	profilePath   string | ||||
| 	enableAllUser bool | ||||
| 	masterKey     []byte | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) Init() error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) setEnableAllUsers(e bool) { | ||||
| 	f.enableAllUser = e | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) setProfilePath(p string) { | ||||
| 	f.profilePath = p | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) setStorageName(s string) { | ||||
| 	f.storage = s | ||||
| } | ||||
| @ -0,0 +1,33 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"path/filepath" | ||||
| ) | ||||
| 
 | ||||
| type BrowserOption func(browserOptionsSetter) | ||||
| 
 | ||||
| type browserOptionsSetter interface { | ||||
| 	setProfilePath(string) | ||||
| 
 | ||||
| 	setEnableAllUsers(bool) | ||||
| 
 | ||||
| 	setStorageName(string) | ||||
| } | ||||
| 
 | ||||
| func WithProfilePath(p string) BrowserOption { | ||||
| 	return func(b browserOptionsSetter) { | ||||
| 		b.setProfilePath(filepath.Clean(p)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithEnableAllUsers(e bool) BrowserOption { | ||||
| 	return func(b browserOptionsSetter) { | ||||
| 		b.setEnableAllUsers(e) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithStorageName(s string) BrowserOption { | ||||
| 	return func(b browserOptionsSetter) { | ||||
| 		b.setStorageName(s) | ||||
| 	} | ||||
| } | ||||
| @ -0,0 +1,111 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	// import sqlite3 driver
 | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
| 
 | ||||
| 	"github.com/moond4rk/hackbrowserdata/crypto" | ||||
| 	"github.com/moond4rk/hackbrowserdata/item" | ||||
| 	"github.com/moond4rk/hackbrowserdata/log" | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/fileutil" | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/typeutil" | ||||
| ) | ||||
| 
 | ||||
| type Password struct { | ||||
| 	Profile     string | ||||
| 	Username    string | ||||
| 	Password    string | ||||
| 	encryptPass []byte | ||||
| 	encryptUser []byte | ||||
| 	LoginURL    string | ||||
| 	CreateDate  time.Time | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) Passwords() ([]Password, error) { | ||||
| 	if _, ok := c.supportedDataMap[TypePassword]; !ok { | ||||
| 		return nil, errors.New("password for c.name is not supported") | ||||
| 	} | ||||
| 	var fullPass []Password | ||||
| 	for _, profile := range c.profilePaths { | ||||
| 		passFile := filepath.Join(profile, TypePassword.Filename(c.name)) | ||||
| 		if !fileutil.IsFileExists(passFile) { | ||||
| 			fmt.Println(passFile) | ||||
| 			return nil, errors.New("password file does not exist") | ||||
| 		} | ||||
| 		if err := fileutil.CopyFile(passFile, item.TempChromiumPassword); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		passwords, err := c.exportPasswords(profile, item.TempChromiumPassword) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if len(passwords) > 0 { | ||||
| 			fullPass = append(fullPass, passwords...) | ||||
| 		} | ||||
| 	} | ||||
| 	return fullPass, nil | ||||
| } | ||||
| 
 | ||||
| func (c *chromium) exportPasswords(profile, dbfile string) ([]Password, error) { | ||||
| 	db, err := sql.Open("sqlite3", dbfile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer db.Close() | ||||
| 	rows, err := db.Query(queryChromiumLogin) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	var passwords []Password | ||||
| 	for rows.Next() { | ||||
| 		var ( | ||||
| 			url, username         string | ||||
| 			encryptPass, password []byte | ||||
| 			create                int64 | ||||
| 		) | ||||
| 		if err := rows.Scan(&url, &username, &encryptPass, &create); err != nil { | ||||
| 			log.Warn(err) | ||||
| 		} | ||||
| 		pass := Password{ | ||||
| 			Profile:     filepath.Base(profile), | ||||
| 			Username:    username, | ||||
| 			encryptPass: encryptPass, | ||||
| 			LoginURL:    url, | ||||
| 		} | ||||
| 		if len(encryptPass) > 0 { | ||||
| 			if len(c.masterKey) == 0 { | ||||
| 				password, err = crypto.DPAPI(encryptPass) | ||||
| 			} else { | ||||
| 				password, err = crypto.DecryptPass(c.masterKey, encryptPass) | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				log.Error(err) | ||||
| 			} | ||||
| 		} | ||||
| 		if create > time.Now().Unix() { | ||||
| 			pass.CreateDate = typeutil.TimeEpoch(create) | ||||
| 		} else { | ||||
| 			pass.CreateDate = typeutil.TimeStamp(create) | ||||
| 		} | ||||
| 		pass.Password = string(password) | ||||
| 		passwords = append(passwords, pass) | ||||
| 	} | ||||
| 	return passwords, nil | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins` | ||||
| ) | ||||
| 
 | ||||
| func (f *firefox) Passwords() ([]Password, error) { | ||||
| 	if f.masterKey != nil { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
					Loading…
					
					
				
		Reference in new issue